diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
new file mode 100644
index 0000000..c6bb036
--- /dev/null
+++ b/.github/workflows/maven.yml
@@ -0,0 +1,35 @@
+# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven
+
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+
+name: Java CI with Maven
+
+on:
+ push:
+ branches: [ "master" ]
+ pull_request:
+ branches: [ "master" ]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ cache: maven
+ - name: Build with Maven
+ run: mvn -B package --file pom.xml
+
+ # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive
+ - name: Update dependency graph
+ uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6
diff --git a/README.md b/README.md
index 54a9105..7cf146f 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,17 @@ It was originally developed SnowBro and later improved by Central MiB and Lab313
## Changelog
+### v0.23 (by hansbonini a.k.a Anime_World)
+- Add swizzle modes for texture visualizations as tile
+- Fix Block Dimensions setting up canvas dimensions, now they work individually.
+
+### v0.22 (by hansbonini a.k.a Anime_World)
+- Add support for Mesen and Exodus CRAM Dump as Palette
+- Add new codecs for visualization
+- Add support for custom tile dimensions
+- Add easy block size configuration
+- Add statusbar configuration for tile and block dimensions
+
### v0.21 (by toruzz)
- New themes. Old custom system removed
- Fractional scale support
diff --git a/pom.xml b/pom.xml
index a2af0ef..73dfc44 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
4.0.0
tm
tilemolester
- 1.0-SNAPSHOT
+ 0.23
jar
UTF-8
diff --git a/src/main/java/tm/TMFileResources.java b/src/main/java/tm/TMFileResources.java
index 8fb80b5..ffdd93d 100644
--- a/src/main/java/tm/TMFileResources.java
+++ b/src/main/java/tm/TMFileResources.java
@@ -163,6 +163,10 @@ public void addToBookmarksTree(Element e, FolderNode folder) {
// String palID = e.getAttribute("palette");
String codecID = e.getAttribute("codec");
TileCodec codec = ui.getTileCodecByID(codecID);
+ String swizzlePattern = e.getAttribute("swizzlepattern");
+ if (swizzlePattern == null || swizzlePattern.isEmpty()) {
+ swizzlePattern = TileCodec.SWIZZLE_NONE; // Default value for backward compatibility
+ }
String desc = XMLParser.getNodeValue(getChildTag(e, "description", 0));
BookmarkItemNode bookmark = new BookmarkItemNode(
offset,
@@ -175,6 +179,7 @@ public void addToBookmarksTree(Element e, FolderNode folder) {
mode,
palIndex,
codec,
+ swizzlePattern,
desc);
folder.add(bookmark);
}
diff --git a/src/main/java/tm/canvases/TMEditorCanvas.java b/src/main/java/tm/canvases/TMEditorCanvas.java
index 237e97a..c9aa9fb 100644
--- a/src/main/java/tm/canvases/TMEditorCanvas.java
+++ b/src/main/java/tm/canvases/TMEditorCanvas.java
@@ -65,7 +65,6 @@ public class TMEditorCanvas extends TMTileCanvas implements MouseInputListener {
private Point moveViewPoint;
private Point moveMousePoint;
- private int[] tempPixels = new int[8*8];
private Vector modifiedTiles = new Vector(); // tiles modified by operation
private Vector modifiedPixels = new Vector();
private Point[][] gridCoords;
@@ -73,6 +72,7 @@ public class TMEditorCanvas extends TMTileCanvas implements MouseInputListener {
private int blockWidth=1;
private int blockHeight=1;
private boolean rowInterleaved=false;
+ private String swizzlePattern = tm.tilecodecs.TileCodec.SWIZZLE_NONE;
private boolean showBlockGrid=false;
/**
@@ -646,7 +646,9 @@ private void drawLine(int x1, int y1, int x2, int y2, boolean trace) {
protected void setPixelTraceable(int x, int y, int argb) {
// mark the tile as modified
- tileModified(x/8, y/8);
+ int tileWidth = (codec != null) ? codec.getTileWidth() : 8;
+ int tileHeight = (codec != null) ? codec.getTileHeight() : 8;
+ tileModified(x/tileWidth, y/tileHeight);
setPixel(x, y, argb);
}
@@ -662,8 +664,12 @@ private void tileModified(int col, int row) {
if (!modifiedTiles.contains(p)) {
modifiedTiles.add(p);
// save original pixels
+ int tileWidth = (codec != null) ? codec.getTileWidth() : 8;
+ int tileHeight = (codec != null) ? codec.getTileHeight() : 8;
+ int tileSize = tileWidth * tileHeight;
+ int[] tempPixels = new int[tileSize];
copyTilePixelsToBuffer(col, row, tempPixels, 0);
- IntBuffer ib = IntBuffer.allocate(8*8);
+ IntBuffer ib = IntBuffer.allocate(tileSize);
ib.put(tempPixels);
modifiedPixels.add(ib);
}
@@ -677,12 +683,19 @@ private void tileModified(int col, int row) {
**/
public void copyTilePixelsToBuffer(int x, int y, int[] buf, int ofs) {
- int pixOfs = (y * 8 * canvasWidth) + (x * 8);
- for (int i=0; i<8; i++) {
- for (int j=0; j<8; j++) {
- buf[ofs++] = pixels[pixOfs++];
+ int tileWidth = (codec != null) ? codec.getTileWidth() : 8;
+ int tileHeight = (codec != null) ? codec.getTileHeight() : 8;
+ int pixOfs = (y * tileHeight * canvasWidth) + (x * tileWidth);
+ for (int i=0; ibits
* as the encoded tile data source.
-* Doesn't work quite right?
+* Uses default 8x8 tiles if codec is not set.
**/
public TMTileCanvas(byte[] bits, int cols, int rows) {
- super(bits, cols*8, rows*8);
+ super(bits, cols * 8, rows * 8); // default 8x8 tiles
this.cols = cols;
this.rows = rows;
}
@@ -98,7 +97,32 @@ public int getMode() {
**/
public void setCodec(TileCodec codec) {
+ // Preserve current tile dimensions when switching codecs
+ if (this.codec != null) {
+ codec.setTileDimensions(this.codec.getTileWidth(), this.codec.getTileHeight());
+ }
this.codec = codec;
+ updatePixdataSize();
+ }
+
+/**
+*
+* Updates the pixdata array size based on current tile dimensions.
+*
+**/
+
+ private void updatePixdataSize() {
+ if (codec != null) {
+ int tileSize = codec.getTileWidth() * codec.getTileHeight();
+ if (tileSize > 0 && (pixdata == null || pixdata.length != tileSize)) {
+ pixdata = new int[tileSize];
+ }
+ } else {
+ // Default to 8x8 if no codec is set
+ if (pixdata == null || pixdata.length != 64) {
+ pixdata = new int[64];
+ }
+ }
}
/**
@@ -158,9 +182,13 @@ public int getPalIndex() {
**/
public void setGridSize(int cols, int rows) {
- setCanvasSize(cols*8, rows*8);
+ int tileWidth = (codec != null) ? codec.getTileWidth() : 8;
+ int tileHeight = (codec != null) ? codec.getTileHeight() : 8;
+ setCanvasSize(cols * tileWidth, rows * tileHeight);
this.cols = cols;
this.rows = rows;
+ // Ensure pixdata array is properly sized
+ updatePixdataSize();
}
/**
@@ -175,27 +203,42 @@ public void packTile(int x, int y) {
// encode single atomic tile
bitsOfs = getTileBitsOffset(x, y);
if (bitsOfs >= 0) {
+ // Ensure pixdata array is properly sized
+ updatePixdataSize();
+
+ int tileWidth = codec.getTileWidth();
+ int tileHeight = codec.getTileHeight();
// copy pixels
- pixOfs = (y * 8 * canvasWidth) + (x * 8);
+ pixOfs = (y * tileHeight * canvasWidth) + (x * tileWidth);
pos = 0;
if (codec.getBitsPerPixel() <= 8) {
int colorCount = codec.getColorCount();
int colorIndex = palIndex * colorCount;
// map RGB values to palette indices
- for (int p=0; p<8; p++) {
- for (int q=0; q<8; q++) {
- pixdata[pos++] = palette.indexOf(colorIndex, pixels[pixOfs++]);
+ for (int p=0; p 0) && (getTileHeight() > 0)
+ && (getTileWidth() <= 256) && (getTileHeight() <= 256));
+ }
+
+}
diff --git a/src/main/java/tm/reversibleaction/ReversibleTileModifyAction.java b/src/main/java/tm/reversibleaction/ReversibleTileModifyAction.java
index 153d4fe..fe6b889 100644
--- a/src/main/java/tm/reversibleaction/ReversibleTileModifyAction.java
+++ b/src/main/java/tm/reversibleaction/ReversibleTileModifyAction.java
@@ -57,9 +57,12 @@ public ReversibleTileModifyAction(
public void undo() {
canvas.getView().gotoBookmark(bookmark);
+ int tileWidth = (canvas.getCodec() != null) ? canvas.getCodec().getTileWidth() : 8;
+ int tileHeight = (canvas.getCodec() != null) ? canvas.getCodec().getTileHeight() : 8;
+ int tileSize = tileWidth * tileHeight;
for (int i=0; i>= p;
}
codecs[i].encode(pixels, bits, ofs, stride);
diff --git a/src/main/java/tm/tilecodecs/DirectColorTileCodec.java b/src/main/java/tm/tilecodecs/DirectColorTileCodec.java
index 5b3cde4..077aaaa 100644
--- a/src/main/java/tm/tilecodecs/DirectColorTileCodec.java
+++ b/src/main/java/tm/tilecodecs/DirectColorTileCodec.java
@@ -111,12 +111,13 @@ private static int msb(int mask) {
**/
public int[] decode(byte[] bits, int ofs, int stride) {
+ int[] pixels = new int[tileWidth * tileHeight];
int v, r, g, b, a, s;
int pos=0;
stride *= bytesPerRow;
- for (int i=0; i<8; i++) {
+ for (int i=0; i> bitsPerPixel*m) & pixelMask;
+ // decode one pixel, but don't exceed tile width
+ if (pixelsInRow < tileWidth && pos < pixels.length) {
+ pixels[pos++] = (b >> bitsPerPixel*m) & pixelMask;
+ pixelsInRow++;
+ }
}
}
ofs += stride;
@@ -148,14 +153,18 @@ public int[] decode(byte[] bits, int ofs, int stride) {
public void encode(int[] pixels, byte[] bits, int ofs, int stride) {
int pos = 0;
stride *= bytesPerRow;
- for (int i=0; i<8; i++) {
+ for (int i=0; i> k) & 0x01) << (7-j);
+
+ for (int i = 0; i < tileHeight; i++) {
+ // Process pixels in 8-pixel blocks for this row
+ int pixelsProcessed = 0;
+ int rowOfs = ofs;
+
+ while (pixelsProcessed < tileWidth) {
+ // Reset bitplanes for this 8-pixel block
+ for (int j = 0; j < bitsPerPixel; j++) {
+ if (rowOfs + bpOffsets[j] < bits.length) {
+ bits[rowOfs + bpOffsets[j]] = 0;
+ }
+ }
+
+ // Encode up to 8 pixels in this block
+ int pixelsInThisBlock = Math.min(8, tileWidth - pixelsProcessed);
+ for (int j = 0; j < pixelsInThisBlock; j++) {
+ int p = (pos < pixels.length) ? pixels[pos++] : 0;
+
+ for (int k = 0; k < bitsPerPixel; k++) {
+ if (rowOfs + bpOffsets[k] < bits.length) {
+ bits[rowOfs + bpOffsets[k]] |= ((p >> k) & 0x01) << (7 - j);
+ }
+ }
}
- pos++;
+
+ pixelsProcessed += pixelsInThisBlock;
+
+ // Move to next 8-pixel block in the same row
+ rowOfs += bytesPerRow;
}
+
+ // Move to next row
ofs += stride;
}
}
diff --git a/src/main/java/tm/tilecodecs/SwizzleUtil.java b/src/main/java/tm/tilecodecs/SwizzleUtil.java
new file mode 100644
index 0000000..5d22b2c
--- /dev/null
+++ b/src/main/java/tm/tilecodecs/SwizzleUtil.java
@@ -0,0 +1,415 @@
+/*
+*
+* Copyright (C) 2024 Hans Bonini.
+*
+* This file is part of Tile Molester.
+*
+* Tile Molester is free software; you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation; either version 2 of the License, or
+* (at your option) any later version.
+*
+* Tile Molester is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+*/
+
+package tm.tilecodecs;
+
+/**
+*
+* Utility class for swizzle/deswizzle operations.
+* Supports various platform-specific swizzle patterns with optimized implementations.
+*
+* Each swizzle pattern is designed to work with specific tile dimensions for optimal results:
+* - BC, WII, SWITCH: 4x4 tiles (block-based formats)
+* - NDS, 3DS: 8x8 tiles (traditional tile formats)
+* - PSP: Variable dimensions (morton-based)
+* - Custom: User-configurable
+*
+**/
+
+public class SwizzleUtil {
+
+ // Platform-specific optimal tile dimensions
+ private static final int BLOCK_FORMAT_TILE_SIZE = 4; // BC, WII, SWITCH
+ private static final int NINTENDO_TILE_SIZE = 8; // NDS, 3DS
+
+ /**
+ * Applies swizzling to pixel coordinates based on the specified pattern.
+ *
+ * @param x X coordinate
+ * @param y Y coordinate
+ * @param width Tile width
+ * @param height Tile height
+ * @param pattern Swizzle pattern
+ * @return Swizzled linear index
+ */
+ public static int applySwizzle(int x, int y, int width, int height, String pattern) {
+ if (TileCodec.SWIZZLE_NONE.equals(pattern)) {
+ return y * width + x;
+ }
+
+ switch (pattern) {
+ case TileCodec.SWIZZLE_BC:
+ return applyBCSwizzle(x, y, width, height);
+ case TileCodec.SWIZZLE_PSP:
+ return applyPSPSwizzle(x, y, width, height);
+ case TileCodec.SWIZZLE_NDS:
+ return applyNDSSwizzle(x, y, width, height);
+ case TileCodec.SWIZZLE_3DS:
+ return apply3DSSwizzle(x, y, width, height);
+ case TileCodec.SWIZZLE_WII:
+ return applyWiiSwizzle(x, y, width, height);
+ case TileCodec.SWIZZLE_SWITCH:
+ return applySwitchSwizzle(x, y, width, height);
+ case TileCodec.SWIZZLE_CUSTOM:
+ return applyCustomSwizzle(x, y, width, height, 4, 4, true); // Default custom values
+ default:
+ return y * width + x; // No swizzling
+ }
+ }
+
+ /**
+ * Sets optimal tile dimensions for a given swizzle pattern.
+ *
+ * @param pattern Swizzle pattern
+ * @param codec TileCodec to update
+ */
+ public static void setOptimalTileDimensions(String pattern, TileCodec codec) {
+ if (codec == null) return;
+
+ switch (pattern) {
+ case TileCodec.SWIZZLE_SWITCH:
+ case TileCodec.SWIZZLE_BC:
+ case TileCodec.SWIZZLE_WII:
+ // Block-based formats work best with 4x4 tile dimensions
+ codec.setTileDimensions(BLOCK_FORMAT_TILE_SIZE, BLOCK_FORMAT_TILE_SIZE);
+ break;
+ case TileCodec.SWIZZLE_NDS:
+ case TileCodec.SWIZZLE_3DS:
+ // Nintendo handheld formats typically use 8x8 tiles
+ codec.setTileDimensions(NINTENDO_TILE_SIZE, NINTENDO_TILE_SIZE);
+ break;
+ // PSP and Custom don't change tile dimensions automatically
+ case TileCodec.SWIZZLE_PSP:
+ case TileCodec.SWIZZLE_CUSTOM:
+ case TileCodec.SWIZZLE_NONE:
+ default:
+ // Keep existing dimensions
+ break;
+ }
+ }
+
+ /**
+ * Applies swizzling with custom parameters.
+ *
+ * @param x X coordinate
+ * @param y Y coordinate
+ * @param width Tile width
+ * @param height Tile height
+ * @param pattern Swizzle pattern
+ * @param customBlockWidth Custom block width (for custom pattern)
+ * @param customBlockHeight Custom block height (for custom pattern)
+ * @param customMortonOrder Whether to use Morton order (for custom pattern)
+ * @return Swizzled linear index
+ */
+ public static int applySwizzle(int x, int y, int width, int height, String pattern,
+ int customBlockWidth, int customBlockHeight, boolean customMortonOrder) {
+ if (TileCodec.SWIZZLE_CUSTOM.equals(pattern)) {
+ return applyCustomSwizzle(x, y, width, height, customBlockWidth, customBlockHeight, customMortonOrder);
+ } else {
+ return applySwizzle(x, y, width, height, pattern);
+ }
+ }
+
+ /**
+ * Reverses swizzling to get original coordinates.
+ *
+ * @param index Linear index
+ * @param width Tile width
+ * @param height Tile height
+ * @param pattern Swizzle pattern
+ * @return Array containing {x, y} coordinates
+ */
+ public static int[] reverseSwizzle(int index, int width, int height, String pattern) {
+ if (TileCodec.SWIZZLE_NONE.equals(pattern)) {
+ return new int[] { index % width, index / width };
+ }
+
+ // For reverse operations, we need to find which x,y produces the given index
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ if (applySwizzle(x, y, width, height, pattern) == index) {
+ return new int[] { x, y };
+ }
+ }
+ }
+
+ // Fallback to linear
+ return new int[] { index % width, index / width };
+ }
+
+ /**
+ * Block Compression (BC) swizzle pattern.
+ * Used by GPU texture compression formats like BC1-BC7.
+ * Optimized for 4x4 tile dimensions.
+ */
+ private static int applyBCSwizzle(int x, int y, int width, int height) {
+ // BC formats use 4x4 blocks
+ final int blockSize = BLOCK_FORMAT_TILE_SIZE;
+
+ int blockX = x / blockSize;
+ int blockY = y / blockSize;
+ int inBlockX = x % blockSize;
+ int inBlockY = y % blockSize;
+
+ int blocksPerRow = (width + blockSize - 1) / blockSize;
+ int blockIndex = blockY * blocksPerRow + blockX;
+ int pixelInBlock = inBlockY * blockSize + inBlockX;
+
+ return blockIndex * (blockSize * blockSize) + pixelInBlock;
+ }
+
+ /**
+ * PSP swizzle pattern.
+ * PSP uses Morton order (Z-order curve) for better cache performance.
+ * Works with variable tile dimensions.
+ */
+ private static int applyPSPSwizzle(int x, int y, int width, int height) {
+ // PSP uses pure Morton order - interleave bits of x and y coordinates
+ return mortonEncode2D(x, y);
+ }
+
+ /**
+ * Nintendo DS swizzle pattern.
+ * NDS uses 8x8 tiles with Morton order within each tile.
+ * Optimized for 8x8 tile dimensions.
+ */
+ private static int applyNDSSwizzle(int x, int y, int width, int height) {
+ final int tileSize = NINTENDO_TILE_SIZE;
+
+ // Calculate which 8x8 tile this pixel belongs to
+ int tileX = x / tileSize;
+ int tileY = y / tileSize;
+ int inTileX = x % tileSize;
+ int inTileY = y % tileSize;
+
+ int tilesPerRow = (width + tileSize - 1) / tileSize;
+ int tileIndex = tileY * tilesPerRow + tileX;
+ int pixelInTile = mortonEncode2D(inTileX, inTileY);
+
+ return tileIndex * (tileSize * tileSize) + pixelInTile;
+ }
+
+ /**
+ * Nintendo 3DS swizzle pattern.
+ * 3DS uses 8x8 tiles with Morton order, similar to NDS but with enhanced bit arrangement.
+ * Optimized for 8x8 tile dimensions.
+ */
+ private static int apply3DSSwizzle(int x, int y, int width, int height) {
+ final int tileSize = NINTENDO_TILE_SIZE;
+
+ // Calculate which 8x8 tile this pixel belongs to
+ int tileX = x / tileSize;
+ int tileY = y / tileSize;
+ int inTileX = x % tileSize;
+ int inTileY = y % tileSize;
+
+ int tilesPerRow = (width + tileSize - 1) / tileSize;
+ int tileIndex = tileY * tilesPerRow + tileX;
+ int pixelInTile = mortonEncode2D(inTileX, inTileY);
+
+ return tileIndex * (tileSize * tileSize) + pixelInTile;
+ }
+
+ /**
+ * Nintendo Switch swizzle pattern.
+ * Switch uses block-linear format optimized for GPU memory access.
+ * Implements a simplified version of the Switch's GOB (Group of Blocks) structure.
+ * Optimized for 4x4 tile dimensions.
+ */
+ private static int applySwitchSwizzle(int x, int y, int width, int height) {
+ final int blockSize = BLOCK_FORMAT_TILE_SIZE;
+
+ // Calculate block coordinates
+ int blockX = x / blockSize;
+ int blockY = y / blockSize;
+ int pixelX = x % blockSize;
+ int pixelY = y % blockSize;
+
+ // Switch uses GOBs (Groups of Blocks) for memory layout optimization
+ // For simplicity, we'll use 8x2 blocks per GOB (32x8 pixels) which works well for most formats
+ final int gobWidthInBlocks = 8;
+ final int gobHeightInBlocks = 2;
+
+ int blocksPerRow = (width + blockSize - 1) / blockSize;
+
+ // GOB coordinates
+ int gobX = blockX / gobWidthInBlocks;
+ int gobY = blockY / gobHeightInBlocks;
+ int gobsPerRow = (blocksPerRow + gobWidthInBlocks - 1) / gobWidthInBlocks;
+
+ // Block position within GOB
+ int blockInGobX = blockX % gobWidthInBlocks;
+ int blockInGobY = blockY % gobHeightInBlocks;
+
+ // Switch uses a specific block ordering within GOBs for optimal memory access
+ int blockInGobIndex = blockInGobY * gobWidthInBlocks + blockInGobX;
+
+ // Pixel position within block (linear order)
+ int pixelInBlock = pixelY * blockSize + pixelX;
+
+ // Calculate final position
+ int gobIndex = gobY * gobsPerRow + gobX;
+ int pixelsPerGob = gobWidthInBlocks * gobHeightInBlocks * blockSize * blockSize;
+ int pixelsPerBlock = blockSize * blockSize;
+
+ return gobIndex * pixelsPerGob + blockInGobIndex * pixelsPerBlock + pixelInBlock;
+ }
+
+ /**
+ * Nintendo Wii swizzle pattern.
+ * Uses 4x4 blocks arranged in 2x2 super-blocks for optimal cache performance.
+ * Optimized for 4x4 tile dimensions.
+ */
+ private static int applyWiiSwizzle(int x, int y, int width, int height) {
+ final int blockSize = BLOCK_FORMAT_TILE_SIZE;
+
+ // Calculate which 4x4 block this pixel belongs to
+ int blockX = x / blockSize;
+ int blockY = y / blockSize;
+ int inBlockX = x % blockSize;
+ int inBlockY = y % blockSize;
+
+ int blocksPerRow = (width + blockSize - 1) / blockSize;
+
+ // Wii arranges 4x4 blocks in 2x2 super-blocks for better cache locality
+ int superBlockX = blockX / 2;
+ int superBlockY = blockY / 2;
+ int blockInSuperX = blockX % 2;
+ int blockInSuperY = blockY % 2;
+
+ int superBlocksPerRow = (blocksPerRow + 1) / 2;
+ int superBlockIndex = superBlockY * superBlocksPerRow + superBlockX;
+
+ // Within a super-block, blocks are arranged as:
+ // 0 1
+ // 2 3
+ int blockInSuper = blockInSuperY * 2 + blockInSuperX;
+
+ // Within a 4x4 block, pixels are stored linearly
+ int pixelInBlock = inBlockY * blockSize + inBlockX;
+
+ return superBlockIndex * (4 * blockSize * blockSize) + blockInSuper * (blockSize * blockSize) + pixelInBlock;
+ }
+
+ /**
+ * Custom swizzle pattern with configurable parameters.
+ *
+ * @param x X coordinate
+ * @param y Y coordinate
+ * @param width Tile width
+ * @param height Tile height
+ * @param blockWidth Block width
+ * @param blockHeight Block height
+ * @param useMortonOrder Whether to use Morton order within blocks
+ * @return Swizzled linear index
+ */
+ private static int applyCustomSwizzle(int x, int y, int width, int height,
+ int blockWidth, int blockHeight, boolean useMortonOrder) {
+ // Calculate which block this pixel belongs to
+ int blockX = x / blockWidth;
+ int blockY = y / blockHeight;
+ int inBlockX = x % blockWidth;
+ int inBlockY = y % blockHeight;
+
+ int blocksPerRow = (width + blockWidth - 1) / blockWidth;
+ int blockIndex = blockY * blocksPerRow + blockX;
+
+ int pixelInBlock;
+ if (useMortonOrder) {
+ // Use Morton order (Z-order) within the block
+ pixelInBlock = mortonEncode2D(inBlockX, inBlockY);
+ } else {
+ // Use linear order within the block
+ pixelInBlock = inBlockY * blockWidth + inBlockX;
+ }
+
+ return blockIndex * (blockWidth * blockHeight) + pixelInBlock;
+ }
+
+ /**
+ * Morton encoding (Z-order curve) for 2D coordinates.
+ * Efficiently interleaves bits of x and y coordinates using bit manipulation.
+ *
+ * @param x X coordinate (must be < 65536 for 16-bit processing)
+ * @param y Y coordinate (must be < 65536 for 16-bit processing)
+ * @return Morton-encoded value
+ */
+ private static int mortonEncode2D(int x, int y) {
+ // Use efficient bit interleaving for coordinates up to 16 bits
+ x = (x | (x << 8)) & 0x00FF00FF;
+ x = (x | (x << 4)) & 0x0F0F0F0F;
+ x = (x | (x << 2)) & 0x33333333;
+ x = (x | (x << 1)) & 0x55555555;
+
+ y = (y | (y << 8)) & 0x00FF00FF;
+ y = (y | (y << 4)) & 0x0F0F0F0F;
+ y = (y | (y << 2)) & 0x33333333;
+ y = (y | (y << 1)) & 0x55555555;
+
+ return x | (y << 1);
+ }
+
+ /**
+ * Gets the optimal tile width for a given swizzle pattern.
+ *
+ * @param pattern Swizzle pattern
+ * @return Optimal tile width, or -1 if no specific recommendation
+ */
+ public static int getOptimalTileWidth(String pattern) {
+ switch (pattern) {
+ case TileCodec.SWIZZLE_SWITCH:
+ case TileCodec.SWIZZLE_BC:
+ case TileCodec.SWIZZLE_WII:
+ return BLOCK_FORMAT_TILE_SIZE;
+ case TileCodec.SWIZZLE_NDS:
+ case TileCodec.SWIZZLE_3DS:
+ return NINTENDO_TILE_SIZE;
+ default:
+ return -1; // No specific recommendation
+ }
+ }
+
+ /**
+ * Gets the optimal tile height for a given swizzle pattern.
+ *
+ * @param pattern Swizzle pattern
+ * @return Optimal tile height, or -1 if no specific recommendation
+ */
+ public static int getOptimalTileHeight(String pattern) {
+ return getOptimalTileWidth(pattern); // All current patterns use square tiles
+ }
+
+ /**
+ * Checks if the given tile dimensions are optimal for the swizzle pattern.
+ *
+ * @param pattern Swizzle pattern
+ * @param width Current tile width
+ * @param height Current tile height
+ * @return true if dimensions are optimal, false otherwise
+ */
+ public static boolean areOptimalDimensions(String pattern, int width, int height) {
+ int optimalWidth = getOptimalTileWidth(pattern);
+ int optimalHeight = getOptimalTileHeight(pattern);
+
+ if (optimalWidth == -1 || optimalHeight == -1) {
+ return true; // No specific requirements
+ }
+
+ return width == optimalWidth && height == optimalHeight;
+ }
+}
diff --git a/src/main/java/tm/tilecodecs/TileCodec.java b/src/main/java/tm/tilecodecs/TileCodec.java
index 0df3931..02feeaa 100644
--- a/src/main/java/tm/tilecodecs/TileCodec.java
+++ b/src/main/java/tm/tilecodecs/TileCodec.java
@@ -20,7 +20,7 @@
/**
*
-* Abstract class for 8x8 ("atomic") tile codecs.
+* Abstract class for configurable size tile codecs.
* To add a new tile format, simply extend this class and implement decode() and encode().
*
**/
@@ -30,13 +30,31 @@ public abstract class TileCodec {
public static final int MODE_1D=1;
public static final int MODE_2D=2;
+ // Swizzle pattern constants
+ public static final String SWIZZLE_NONE = "None";
+ public static final String SWIZZLE_BC = "BC";
+ public static final String SWIZZLE_PSP = "PSP";
+ public static final String SWIZZLE_NDS = "NDS";
+ public static final String SWIZZLE_3DS = "3DS";
+ public static final String SWIZZLE_WII = "WII";
+ public static final String SWIZZLE_SWITCH = "SWITCH";
+ public static final String SWIZZLE_CUSTOM = "Custom";
+
private String id;
private String description;
protected int[] pixels; // destination for DEcoded tile data
protected int bitsPerPixel;
- protected int bytesPerRow; // row = 8 pixels
+ protected int bytesPerRow; // row = tileWidth pixels
protected long colorCount;
protected int tileSize; // size of one encoded tile
+ protected int tileWidth; // width of tile in pixels
+ protected int tileHeight; // height of tile in pixels
+ protected String swizzlePattern; // current swizzle pattern
+
+ // Custom swizzle parameters
+ protected int customBlockWidth = 4; // custom swizzle block width
+ protected int customBlockHeight = 4; // custom swizzle block height
+ protected boolean customMortonOrder = true; // use morton order within blocks
/**
*
@@ -47,13 +65,30 @@ public abstract class TileCodec {
**/
public TileCodec(String id, int bitsPerPixel, String description) {
+ this(id, bitsPerPixel, description, 8, 8);
+ }
+
+/**
+*
+* Constructor with configurable tile dimensions.
+*
+* @param bitsPerPixel Bits per pixel
+* @param tileWidth Width of tile in pixels
+* @param tileHeight Height of tile in pixels
+*
+**/
+
+ public TileCodec(String id, int bitsPerPixel, String description, int tileWidth, int tileHeight) {
this.id = id;
this.bitsPerPixel = bitsPerPixel;
this.description = description;
- bytesPerRow = bitsPerPixel; // because (bitsPerPixel*8)/8 = bitsPerPixel
- tileSize = bytesPerRow*8;
+ this.tileWidth = tileWidth;
+ this.tileHeight = tileHeight;
+ this.swizzlePattern = SWIZZLE_NONE; // default to no swizzling
+ bytesPerRow = (bitsPerPixel * tileWidth + 7) / 8; // round up to nearest byte
+ tileSize = bytesPerRow * tileHeight;
colorCount = 1 << bitsPerPixel;
- pixels = new int[8*8];
+ pixels = new int[tileWidth * tileHeight];
}
/**
@@ -89,7 +124,7 @@ public int getBitsPerPixel() {
/**
*
-* Gets the # of bytes per row (8 pixels) for the tile format.
+* Gets the # of bytes per row (tileWidth pixels) for the tile format.
*
**/
@@ -97,6 +132,79 @@ public int getBytesPerRow() {
return bytesPerRow;
}
+/**
+*
+* Gets the width of the tile in pixels.
+*
+**/
+
+ public int getTileWidth() {
+ return tileWidth;
+ }
+
+/**
+*
+* Gets the height of the tile in pixels.
+*
+**/
+
+ public int getTileHeight() {
+ return tileHeight;
+ }
+
+/**
+*
+* Sets the tile dimensions and recalculates dependent values.
+*
+**/
+
+ public void setTileDimensions(int width, int height) {
+ this.tileWidth = width;
+ this.tileHeight = height;
+ bytesPerRow = (bitsPerPixel * tileWidth + 7) / 8; // round up to nearest byte
+ tileSize = bytesPerRow * tileHeight;
+ pixels = new int[tileWidth * tileHeight];
+ }
+
+/**
+*
+* Gets the current swizzle pattern.
+*
+**/
+
+ public String getSwizzlePattern() {
+ return swizzlePattern;
+ }
+
+/**
+*
+* Sets the swizzle pattern.
+*
+**/
+
+ public void setSwizzlePattern(String swizzlePattern) {
+ this.swizzlePattern = swizzlePattern;
+ }
+
+/**
+*
+* Gets all available swizzle patterns.
+*
+**/
+
+ public static String[] getAvailableSwizzlePatterns() {
+ return new String[] {
+ SWIZZLE_NONE,
+ SWIZZLE_BC,
+ SWIZZLE_PSP,
+ SWIZZLE_NDS,
+ SWIZZLE_3DS,
+ SWIZZLE_WII,
+ SWIZZLE_SWITCH,
+ SWIZZLE_CUSTOM
+ };
+ }
+
/**
*
*
@@ -147,4 +255,46 @@ public String toString() {
return description;
}
+ /**
+ * Gets the custom swizzle block width.
+ */
+ public int getCustomBlockWidth() {
+ return customBlockWidth;
+ }
+
+ /**
+ * Sets the custom swizzle block width.
+ */
+ public void setCustomBlockWidth(int width) {
+ this.customBlockWidth = width;
+ }
+
+ /**
+ * Gets the custom swizzle block height.
+ */
+ public int getCustomBlockHeight() {
+ return customBlockHeight;
+ }
+
+ /**
+ * Sets the custom swizzle block height.
+ */
+ public void setCustomBlockHeight(int height) {
+ this.customBlockHeight = height;
+ }
+
+ /**
+ * Gets whether custom swizzle uses Morton order.
+ */
+ public boolean getCustomMortonOrder() {
+ return customMortonOrder;
+ }
+
+ /**
+ * Sets whether custom swizzle uses Morton order.
+ */
+ public void setCustomMortonOrder(boolean useMorton) {
+ this.customMortonOrder = useMorton;
+ }
+
}
\ No newline at end of file
diff --git a/src/main/java/tm/tilecodecs/_3BPPLinearTileCodec.java b/src/main/java/tm/tilecodecs/_3BPPLinearTileCodec.java
index bf04f71..58d7bb7 100644
--- a/src/main/java/tm/tilecodecs/_3BPPLinearTileCodec.java
+++ b/src/main/java/tm/tilecodecs/_3BPPLinearTileCodec.java
@@ -17,22 +17,41 @@ public _3BPPLinearTileCodec() {
**/
public int[] decode(byte[] bits, int ofs, int stride) {
+ int[] pixels = new int[tileWidth * tileHeight];
int pos=0;
int b1, b2, b3;
stride *= bytesPerRow;
- for (int i=0; i<8; i++) {
+ for (int i=0; i> 5) & 7;
- pixels[pos++] = (b1 >> 2) & 7;
- pixels[pos++] = ((b1 & 3) << 1) | ((b2 >> 7) & 1);
- pixels[pos++] = (b2 >> 4) & 7;
- pixels[pos++] = (b2 >> 1) & 7;
- pixels[pos++] = ((b2 & 1) << 2) | ((b3 >> 6) & 3);
- pixels[pos++] = (b3 >> 3) & 7;
- pixels[pos++] = b3 & 7;
+
+ // Original 8-pixel decoding with bounds checking
+ if (pos + 7 < pixels.length) {
+ pixels[pos++] = (b1 >> 5) & 7;
+ pixels[pos++] = (b1 >> 2) & 7;
+ pixels[pos++] = ((b1 & 3) << 1) | ((b2 >> 7) & 1);
+ pixels[pos++] = (b2 >> 4) & 7;
+ pixels[pos++] = (b2 >> 1) & 7;
+ pixels[pos++] = ((b2 & 1) << 2) | ((b3 >> 6) & 3);
+ pixels[pos++] = (b3 >> 3) & 7;
+ pixels[pos++] = b3 & 7;
+ } else {
+ // Handle tiles narrower than 8 pixels
+ for (int j = 0; j < 8 && pos < pixels.length; j++) {
+ switch (j) {
+ case 0: pixels[pos++] = (b1 >> 5) & 7; break;
+ case 1: pixels[pos++] = (b1 >> 2) & 7; break;
+ case 2: pixels[pos++] = ((b1 & 3) << 1) | ((b2 >> 7) & 1); break;
+ case 3: pixels[pos++] = (b2 >> 4) & 7; break;
+ case 4: pixels[pos++] = (b2 >> 1) & 7; break;
+ case 5: pixels[pos++] = ((b2 & 1) << 2) | ((b3 >> 6) & 3); break;
+ case 6: pixels[pos++] = (b3 >> 3) & 7; break;
+ case 7: pixels[pos++] = b3 & 7; break;
+ }
+ }
+ }
ofs += stride;
}
return pixels;
@@ -48,18 +67,40 @@ public void encode(int[] pixels, byte[] bits, int ofs, int stride) {
int pos = 0;
int b1, b2, b3;
stride *= bytesPerRow;
- for (int i=0; i<8; i++) {
+ for (int i=0; i> 1;
- b2 = (pixels[pos++] & 1) << 7;
- b2 |= (pixels[pos++] & 7) << 4;
- b2 |= (pixels[pos++] & 7) << 1;
- b2 |= (pixels[pos] & 4) >> 2;
- b3 = (pixels[pos++] & 3) << 6;
- b3 |= (pixels[pos++] & 7) << 3;
- b3 |= (pixels[pos++] & 7);
+ if (pos + 7 < pixels.length) {
+ // Standard 8-pixel encoding
+ b1 = (pixels[pos++] & 7) << 5;
+ b1 |= (pixels[pos++] & 7) << 2;
+ b1 |= (pixels[pos] & 6) >> 1;
+ b2 = (pixels[pos++] & 1) << 7;
+ b2 |= (pixels[pos++] & 7) << 4;
+ b2 |= (pixels[pos++] & 7) << 1;
+ b2 |= (pixels[pos] & 4) >> 2;
+ b3 = (pixels[pos++] & 3) << 6;
+ b3 |= (pixels[pos++] & 7) << 3;
+ b3 |= (pixels[pos++] & 7);
+ } else {
+ // Handle tiles narrower than 8 pixels
+ b1 = b2 = b3 = 0;
+ int[] rowPixels = new int[8];
+ for (int j = 0; j < 8; j++) {
+ rowPixels[j] = (pos < pixels.length) ? pixels[pos++] : 0;
+ }
+
+ b1 = (rowPixels[0] & 7) << 5;
+ b1 |= (rowPixels[1] & 7) << 2;
+ b1 |= (rowPixels[2] & 6) >> 1;
+ b2 = (rowPixels[2] & 1) << 7;
+ b2 |= (rowPixels[3] & 7) << 4;
+ b2 |= (rowPixels[4] & 7) << 1;
+ b2 |= (rowPixels[5] & 4) >> 2;
+ b3 = (rowPixels[5] & 3) << 6;
+ b3 |= (rowPixels[6] & 7) << 3;
+ b3 |= (rowPixels[7] & 7);
+ }
+
bits[ofs++] = (byte)b1; // byte 1: 0001 1122
bits[ofs++] = (byte)b2; // byte 2: 2333 4445
bits[ofs++] = (byte)b3; // byte 3: 5566 6777
diff --git a/src/main/java/tm/tilecodecs/_6BPPLinearTileCodec.java b/src/main/java/tm/tilecodecs/_6BPPLinearTileCodec.java
index 81027a9..c7b5e69 100644
--- a/src/main/java/tm/tilecodecs/_6BPPLinearTileCodec.java
+++ b/src/main/java/tm/tilecodecs/_6BPPLinearTileCodec.java
@@ -17,38 +17,50 @@ public _6BPPLinearTileCodec() {
**/
public int[] decode(byte[] bits, int ofs, int stride) {
+ int[] pixels = new int[tileWidth * tileHeight];
int pos=0;
int b1, b2, b3, b4, b5, b6;
stride *= bytesPerRow;
- for (int i=0; i<8; i++) {
- // do one row
- /* BPP3 Linear:
- * byte 1: 0001 1122
- byte 2: 2333 4445
- byte 3: 5566 6777
- */
- /* BPP6 Linear:
- * byte 1: 0000 0011
- byte 2: 1111 2222
- byte 3: 2233 3333
- byte 4: 4444 4455
- byte 5: 5555 6666
- byte 6: 6677 7777
- */
- b6 = bits[ofs++] & 0xFF; // byte 1: 0000 0011
- b5 = bits[ofs++] & 0xFF; // byte 2: 1111 2222
- b4 = bits[ofs++] & 0xFF; // byte 3: 2233 3333
- b3 = bits[ofs++] & 0xFF; // byte 4: 4444 4455
- b2 = bits[ofs++] & 0xFF; // byte 5: 5555 6666
- b1 = bits[ofs++] & 0xFF; // byte 6: 6677 7777
- pixels[pos++] = (b1 >> 2) & 63;
- pixels[pos++] = ((b1 & 3) << 4) | ((b2 >> 4) & 15);
- pixels[pos++] = ((b2 & 15) << 2) | ((b3 >> 6) & 3);
- pixels[pos++] = b3 & 63;
- pixels[pos++] = (b4 >> 2) & 63;
- pixels[pos++] = ((b4 & 3) << 4) | ((b5 >> 4) & 15);
- pixels[pos++] = ((b5 & 15) << 2) | ((b6 >> 6) & 3);
- pixels[pos++] = b6 & 63;
+ for (int i=0; i> 2) & 63;
+ decodedPixels[1] = ((b1 & 3) << 4) | ((b2 >> 4) & 15);
+ decodedPixels[2] = ((b2 & 15) << 2) | ((b3 >> 6) & 3);
+ decodedPixels[3] = b3 & 63;
+ decodedPixels[4] = (b4 >> 2) & 63;
+ decodedPixels[5] = ((b4 & 3) << 4) | ((b5 >> 4) & 15);
+ decodedPixels[6] = ((b5 & 15) << 2) | ((b6 >> 6) & 3);
+ decodedPixels[7] = b6 & 63;
+
+ // Copy only the pixels we need for this row
+ int pixelsToTake = Math.min(8, tileWidth - pixelsProcessed);
+ for (int p = 0; p < pixelsToTake && pos < pixels.length; p++) {
+ pixels[pos++] = decodedPixels[p];
+ }
+ pixelsProcessed += pixelsToTake;
+ } else {
+ // Not enough data, fill with zeros
+ while (pixelsProcessed < tileWidth && pos < pixels.length) {
+ pixels[pos++] = 0;
+ pixelsProcessed++;
+ }
+ }
+ }
ofs += stride;
}
return pixels;
@@ -64,48 +76,58 @@ public void encode(int[] pixels, byte[] bits, int ofs, int stride) {
int pos = 0;
int b1, b2, b3, b4, b5, b6;
stride *= bytesPerRow;
- for (int i=0; i<8; i++) {
- /* BPP3 Linear:
- * byte 1: 0001 1122
- byte 2: 2333 4445
- byte 3: 5566 6777
- */
- /* BPP6 Linear:
- * byte 1: 0000 0011
- byte 2: 1111 2222
- byte 3: 2233 3333
- byte 4: 4444 4455
- byte 5: 5555 6666
- byte 6: 6677 7777
- */
-
- // do one row
- b1 = (pixels[pos++] & 63) << 2;
- b1 |= (pixels[pos] & 48) >> 4;
-
- b2 = (pixels[pos++] & 15) << 4;
- b2 |= (pixels[pos] & 60) >> 2;
-
- b3 = (pixels[pos++] & 3) << 6;
- b3 |= (pixels[pos++] & 63);
-
-
- b4 = (pixels[pos++] & 63) << 2;
- b4 |= (pixels[pos] & 48) >> 4;
-
- b5 = (pixels[pos++] & 15) << 4;
- b5 |= (pixels[pos] & 60) >> 2;
-
- b6 = (pixels[pos++] & 3) << 6;
- b6 |= (pixels[pos++] & 63);
+ for (int i=0; i> 4;
+
+ b2 = (pixelsToEncode[1] & 15) << 4;
+ b2 |= (pixelsToEncode[2] & 60) >> 2;
+
+ b3 = (pixelsToEncode[2] & 3) << 6;
+ b3 |= (pixelsToEncode[3] & 63);
+
+ b4 = (pixelsToEncode[4] & 63) << 2;
+ b4 |= (pixelsToEncode[5] & 48) >> 4;
+
+ b5 = (pixelsToEncode[5] & 15) << 4;
+ b5 |= (pixelsToEncode[6] & 60) >> 2;
+
+ b6 = (pixelsToEncode[6] & 3) << 6;
+ b6 |= (pixelsToEncode[7] & 63);
+
+ // Write the 6 bytes
+ if (ofs + 5 < bits.length) {
+ bits[ofs++] = (byte)b6; // byte 1: 0000 0011
+ bits[ofs++] = (byte)b5; // byte 2: 1111 2222
+ bits[ofs++] = (byte)b4; // byte 3: 2233 3333
+ bits[ofs++] = (byte)b3; // byte 4: 4444 4455
+ bits[ofs++] = (byte)b2; // byte 5: 5555 6666
+ bits[ofs++] = (byte)b1; // byte 6: 6677 7777
+ } else {
+ // Not enough space, skip
+ break;
+ }
+
+ pixelsProcessed += pixelsToTake;
+ }
ofs += stride;
}
}
diff --git a/src/main/java/tm/treenodes/BookmarkItemNode.java b/src/main/java/tm/treenodes/BookmarkItemNode.java
index 07ac57f..3c27bc9 100644
--- a/src/main/java/tm/treenodes/BookmarkItemNode.java
+++ b/src/main/java/tm/treenodes/BookmarkItemNode.java
@@ -42,6 +42,7 @@ public class BookmarkItemNode extends TMTreeNode {
private String description; // entered by the user to describe/name the bookmark
private String paletteID;
private int palIndex;
+ private String swizzlePattern;
public BookmarkItemNode(int offset,
int cols,
@@ -53,6 +54,7 @@ public BookmarkItemNode(int offset,
int mode,
int palIndex,
TileCodec codec,
+ String swizzlePattern,
String description
) {
super();
@@ -66,6 +68,7 @@ public BookmarkItemNode(int offset,
this.mode = mode;
this.palIndex = palIndex;
this.codec = codec;
+ this.swizzlePattern = swizzlePattern;
this.description = description;
}
@@ -129,6 +132,16 @@ public boolean getRowInterleaved() {
return rowInterleaved;
}
+/**
+*
+* Gets the swizzle pattern.
+*
+**/
+
+ public String getSwizzlePattern() {
+ return swizzlePattern;
+ }
+
/**
*
*
@@ -207,6 +220,7 @@ public String toXML() {
s.append(" blockheight=\"").append(blockHeight).append("\"");
s.append(" rowinterleaved=\"").append(rowInterleaved).append("\"");
s.append(" sizeblocktocanvas=\"").append(sizeBlockToCanvas).append("\"");
+ s.append(" swizzlepattern=\"").append(swizzlePattern).append("\"");
if (mode == TileCodec.MODE_1D) {
s.append(" mode=\"1D\"");
}
diff --git a/src/main/java/tm/ui/CustomSwizzleDialog.java b/src/main/java/tm/ui/CustomSwizzleDialog.java
new file mode 100644
index 0000000..18f3b36
--- /dev/null
+++ b/src/main/java/tm/ui/CustomSwizzleDialog.java
@@ -0,0 +1,160 @@
+/*
+*
+* Copyright (C) 2024 Hans Bonini.
+*
+* This file is part of Tile Molester.
+*
+* Tile Molester is free software; you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation; either version 2 of the License, or
+* (at your option) any later version.
+*
+* Tile Molester is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+*/
+
+package tm.ui;
+
+import tm.tilecodecs.TileCodec;
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.*;
+
+/**
+*
+* Dialog for configuring custom swizzle settings.
+*
+**/
+
+public class CustomSwizzleDialog extends JDialog {
+
+ private boolean isOK = false;
+ private TileCodec codec;
+
+ private JSpinner blockWidthSpinner;
+ private JSpinner blockHeightSpinner;
+ private JCheckBox mortonOrderCheckbox;
+
+ private JButton okButton;
+ private JButton cancelButton;
+
+ /**
+ * Creates a new custom swizzle dialog.
+ */
+ public CustomSwizzleDialog(Frame parent, TileCodec codec) {
+ super(parent, "Custom Swizzle Settings", true);
+ this.codec = codec;
+ initComponents();
+ setupDialog();
+ }
+
+ /**
+ * Initializes the dialog components.
+ */
+ private void initComponents() {
+ setLayout(new BorderLayout());
+
+ // Main panel with form controls
+ JPanel mainPanel = new JPanel(new GridBagLayout());
+ GridBagConstraints gbc = new GridBagConstraints();
+ gbc.insets = new Insets(5, 5, 5, 5);
+
+ // Block Width
+ gbc.gridx = 0; gbc.gridy = 0; gbc.anchor = GridBagConstraints.WEST;
+ mainPanel.add(new JLabel("Block Width:"), gbc);
+
+ gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL;
+ blockWidthSpinner = new JSpinner(new SpinnerNumberModel(
+ codec != null ? codec.getCustomBlockWidth() : 4, 1, 256, 1));
+ mainPanel.add(blockWidthSpinner, gbc);
+
+ // Block Height
+ gbc.gridx = 0; gbc.gridy = 1; gbc.fill = GridBagConstraints.NONE;
+ mainPanel.add(new JLabel("Block Height:"), gbc);
+
+ gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL;
+ blockHeightSpinner = new JSpinner(new SpinnerNumberModel(
+ codec != null ? codec.getCustomBlockHeight() : 4, 1, 256, 1));
+ mainPanel.add(blockHeightSpinner, gbc);
+
+ // Morton Order checkbox
+ gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 2; gbc.fill = GridBagConstraints.NONE;
+ mortonOrderCheckbox = new JCheckBox("Use Morton Order (Z-order curve)",
+ codec != null ? codec.getCustomMortonOrder() : true);
+ mainPanel.add(mortonOrderCheckbox, gbc);
+
+ // Description label
+ gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 2; gbc.fill = GridBagConstraints.HORIZONTAL;
+ JLabel descLabel = new JLabel("Morton order interleaves X,Y bits for cache-friendly access patterns.
" +
+ "Linear order stores pixels sequentially within each block.");
+ descLabel.setFont(descLabel.getFont().deriveFont(Font.PLAIN, 11.0f));
+ mainPanel.add(descLabel, gbc);
+
+ add(mainPanel, BorderLayout.CENTER);
+
+ // Button panel
+ JPanel buttonPanel = new JPanel(new FlowLayout());
+ okButton = new JButton("OK");
+ cancelButton = new JButton("Cancel");
+
+ okButton.addActionListener(e -> {
+ saveSettings();
+ isOK = true;
+ setVisible(false);
+ });
+
+ cancelButton.addActionListener(e -> {
+ isOK = false;
+ setVisible(false);
+ });
+
+ buttonPanel.add(okButton);
+ buttonPanel.add(cancelButton);
+ add(buttonPanel, BorderLayout.SOUTH);
+ }
+
+ /**
+ * Sets up the dialog properties.
+ */
+ private void setupDialog() {
+ setDefaultCloseOperation(DISPOSE_ON_CLOSE);
+ pack();
+ setLocationRelativeTo(getParent());
+ setResizable(false);
+
+ // Set default button
+ getRootPane().setDefaultButton(okButton);
+
+ // ESC key closes dialog
+ KeyStroke escapeKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false);
+ getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(escapeKeyStroke, "ESCAPE");
+ getRootPane().getActionMap().put("ESCAPE", new AbstractAction() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ isOK = false;
+ setVisible(false);
+ }
+ });
+ }
+
+ /**
+ * Saves the settings to the codec.
+ */
+ private void saveSettings() {
+ if (codec != null) {
+ codec.setCustomBlockWidth((Integer) blockWidthSpinner.getValue());
+ codec.setCustomBlockHeight((Integer) blockHeightSpinner.getValue());
+ codec.setCustomMortonOrder(mortonOrderCheckbox.isSelected());
+ }
+ }
+
+ /**
+ * Returns whether the OK button was pressed.
+ */
+ public boolean isOK() {
+ return isOK;
+ }
+}
diff --git a/src/main/java/tm/ui/TMStatusBar.java b/src/main/java/tm/ui/TMStatusBar.java
index d9f8894..082115f 100644
--- a/src/main/java/tm/ui/TMStatusBar.java
+++ b/src/main/java/tm/ui/TMStatusBar.java
@@ -41,6 +41,19 @@ public class TMStatusBar extends JPanel {
private JLabel modeLabel = new JLabel(" ");
private JLabel tilesLabel = new JLabel(" ");
private JLabel messageLabel = new JLabel(" ");
+ private JLabel swizzleLabel = new JLabel(" "); // Swizzle pattern information
+
+ // Tile size controls
+ private JLabel tileSizeLabel = new JLabel("Tile Size:");
+ private JSpinner tileWidthSpinner;
+ private JSpinner tileHeightSpinner;
+
+ // Block size controls
+ private JLabel blockSizeLabel = new JLabel("Block Size:");
+ private JSpinner blockWidthSpinner;
+ private JSpinner blockHeightSpinner;
+
+ private TMUI parentUI;
/**
*
@@ -48,8 +61,10 @@ public class TMStatusBar extends JPanel {
*
**/
- public TMStatusBar() {
+ public TMStatusBar(TMUI parentUI) {
super();
+ this.parentUI = parentUI;
+
JPanel p1 = new JPanel();
p1.setLayout(new GridLayout(1, 3));
p1.add(messageLabel);
@@ -65,11 +80,67 @@ public TMStatusBar() {
p3.setLayout(new GridLayout(1, 2));
p3.add(modeLabel);
p3.add(tilesLabel);
-
- setLayout(new GridLayout(1, 4));
- add(p1);
- add(p2);
- add(p3);
+
+ // Only add tile size controls if parentUI is provided
+ if (parentUI != null) {
+ // Initialize tile size spinners
+ tileWidthSpinner = new JSpinner(new SpinnerNumberModel(8, 1, 256, 1));
+ tileHeightSpinner = new JSpinner(new SpinnerNumberModel(8, 1, 256, 1));
+
+ // Initialize block size spinners
+ blockWidthSpinner = new JSpinner(new SpinnerNumberModel(16, 1, 256, 1));
+ blockHeightSpinner = new JSpinner(new SpinnerNumberModel(16, 1, 256, 1));
+
+ // Set preferred size for spinners
+ Dimension spinnerSize = new Dimension(50, 20);
+ tileWidthSpinner.setPreferredSize(spinnerSize);
+ tileHeightSpinner.setPreferredSize(spinnerSize);
+ blockWidthSpinner.setPreferredSize(spinnerSize);
+ blockHeightSpinner.setPreferredSize(spinnerSize);
+
+ // Add change listeners to spinners
+ tileWidthSpinner.addChangeListener(e -> updateTileSize());
+ tileHeightSpinner.addChangeListener(e -> updateTileSize());
+ blockWidthSpinner.addChangeListener(e -> updateBlockSize());
+ blockHeightSpinner.addChangeListener(e -> updateBlockSize());
+
+ // Create tile size panel
+ JPanel p4 = new JPanel();
+ p4.setLayout(new FlowLayout(FlowLayout.LEFT, 2, 0));
+ p4.add(tileSizeLabel);
+ p4.add(new JLabel("W:"));
+ p4.add(tileWidthSpinner);
+ p4.add(new JLabel("H:"));
+ p4.add(tileHeightSpinner);
+
+ // Create block size panel
+ JPanel p5 = new JPanel();
+ p5.setLayout(new FlowLayout(FlowLayout.LEFT, 2, 0));
+ p5.add(blockSizeLabel);
+ p5.add(new JLabel("W:"));
+ p5.add(blockWidthSpinner);
+ p5.add(new JLabel("H:"));
+ p5.add(blockHeightSpinner);
+
+ // Create swizzle panel
+ JPanel p6 = new JPanel();
+ p6.setLayout(new FlowLayout(FlowLayout.LEFT, 2, 0));
+ p6.add(new JLabel("Swizzle:"));
+ p6.add(swizzleLabel);
+
+ setLayout(new GridLayout(1, 7));
+ add(p1);
+ add(p2);
+ add(p3);
+ add(p4);
+ add(p5);
+ add(p6);
+ } else {
+ setLayout(new GridLayout(1, 4));
+ add(p1);
+ add(p2);
+ add(p3);
+ }
// pane.add(new JLabel(" ")); // just some whitespace
//offsetLabel.setBorder(new BevelBorder(BevelBorder.LOWERED));
@@ -80,6 +151,103 @@ public TMStatusBar() {
//tilesLabel.setBorder(new BevelBorder(BevelBorder.LOWERED));
}
+/**
+*
+* Creates the status bar (legacy constructor for backward compatibility).
+*
+**/
+
+ public TMStatusBar() {
+ this(null);
+ }
+
+/**
+*
+* Updates tile size when spinner values change.
+*
+**/
+
+ private void updateTileSize() {
+ if (parentUI != null) {
+ int width = (Integer) tileWidthSpinner.getValue();
+ int height = (Integer) tileHeightSpinner.getValue();
+ parentUI.setTileSize(width, height);
+ }
+ }
+
+/**
+*
+* Updates block size when spinner values change.
+*
+**/
+
+ private void updateBlockSize() {
+ if (parentUI != null) {
+ // Block dimensions are always independent of canvas size
+ int width = (Integer) blockWidthSpinner.getValue();
+ int height = (Integer) blockHeightSpinner.getValue();
+ parentUI.setBlockSize(width, height);
+ }
+ }
+
+/**
+*
+* Updates the tile size spinners to reflect current tile dimensions.
+*
+**/
+
+ public void setTileSize(int width, int height) {
+ if (tileWidthSpinner != null && tileHeightSpinner != null) {
+ tileWidthSpinner.setValue(width);
+ tileHeightSpinner.setValue(height);
+ }
+ }
+
+/**
+*
+* Updates the block size spinners to reflect current block dimensions.
+*
+**/
+
+ public void setBlockSize(int width, int height) {
+ if (blockWidthSpinner != null && blockHeightSpinner != null) {
+ // Temporarily remove listeners to avoid triggering updates
+ ChangeListener[] widthListeners = blockWidthSpinner.getChangeListeners();
+ ChangeListener[] heightListeners = blockHeightSpinner.getChangeListeners();
+
+ for (ChangeListener listener : widthListeners) {
+ blockWidthSpinner.removeChangeListener(listener);
+ }
+ for (ChangeListener listener : heightListeners) {
+ blockHeightSpinner.removeChangeListener(listener);
+ }
+
+ // Update values
+ blockWidthSpinner.setValue(width);
+ blockHeightSpinner.setValue(height);
+
+ // Restore listeners
+ for (ChangeListener listener : widthListeners) {
+ blockWidthSpinner.addChangeListener(listener);
+ }
+ for (ChangeListener listener : heightListeners) {
+ blockHeightSpinner.addChangeListener(listener);
+ }
+ }
+ }
+
+ /**
+ * Updates the block size label to show that block dimensions are always independent.
+ */
+ public void updateBlockSizeLabel(boolean isFullCanvas) {
+ // Block dimensions are always independent of canvas size
+ blockSizeLabel.setText("Block Size:");
+ blockSizeLabel.setToolTipText("Block dimensions are independent of canvas size");
+ // Always enable spinners since block size is always independent
+ if (blockWidthSpinner != null) blockWidthSpinner.setEnabled(true);
+ if (blockHeightSpinner != null) blockHeightSpinner.setEnabled(true);
+ }
+
/**
*
* Sets the text for general message.
@@ -197,6 +365,8 @@ else if (ec.isDrawingLine()) {
}
if (view.getTileCodec() != null) {
setCodec(view.getTileCodec().getDescription());
+ // Update tile size spinners
+ setTileSize(view.getTileCodec().getTileWidth(), view.getTileCodec().getTileHeight());
}
else {
setCodec("");
@@ -204,6 +374,13 @@ else if (ec.isDrawingLine()) {
setPalOffset(view.getPalette().getOffset());
setMode(view.getMode());
setTiles(view.getCols(), view.getRows());
+
+ // Update block size spinners and label
+ setBlockSize(view.getBlockWidth(), view.getBlockHeight());
+ updateBlockSizeLabel(view.getSizeBlockToCanvas());
+
+ // Update swizzle information
+ setSwizzle(view.getSwizzlePattern(), view.getTileCodec());
}
/**
@@ -230,4 +407,36 @@ protected static void buildConstraints(GridBagConstraints gbc, int gx, int gy, i
public void setCoords(String string) {
coordsLabel.setText(string);
}
+
+ /**
+ * Sets the swizzle information text.
+ */
+ public void setSwizzle(String swizzlePattern, tm.tilecodecs.TileCodec codec) {
+ if (swizzlePattern == null || tm.tilecodecs.TileCodec.SWIZZLE_NONE.equals(swizzlePattern)) {
+ swizzleLabel.setText("None");
+ } else if (tm.tilecodecs.TileCodec.SWIZZLE_CUSTOM.equals(swizzlePattern) && codec != null) {
+ String mortonText = codec.getCustomMortonOrder() ? "Morton" : "Linear";
+ swizzleLabel.setText(String.format("Custom... %dx%d %s",
+ codec.getCustomBlockWidth(), codec.getCustomBlockHeight(), mortonText));
+ } else {
+ // For predefined patterns, just show the pattern name
+ swizzleLabel.setText(getSwizzleDisplayName(swizzlePattern));
+ }
+ }
+
+ /**
+ * Gets a user-friendly display name for a swizzle pattern.
+ * Names match those used in the menu system for consistency.
+ */
+ private String getSwizzleDisplayName(String pattern) {
+ switch (pattern) {
+ case tm.tilecodecs.TileCodec.SWIZZLE_BC: return "BC Texture";
+ case tm.tilecodecs.TileCodec.SWIZZLE_PSP: return "PlayStation Portable";
+ case tm.tilecodecs.TileCodec.SWIZZLE_NDS: return "Nintendo DS";
+ case tm.tilecodecs.TileCodec.SWIZZLE_3DS: return "Nintendo 3DS";
+ case tm.tilecodecs.TileCodec.SWIZZLE_WII: return "Nintendo Wii";
+ case tm.tilecodecs.TileCodec.SWIZZLE_SWITCH: return "Nintendo Switch";
+ default: return pattern != null ? pattern : "None";
+ }
+ }
}
\ No newline at end of file
diff --git a/src/main/java/tm/ui/TMUI.java b/src/main/java/tm/ui/TMUI.java
index f49998f..fb0d200 100644
--- a/src/main/java/tm/ui/TMUI.java
+++ b/src/main/java/tm/ui/TMUI.java
@@ -76,7 +76,7 @@ public class TMUI extends JFrame {
// UI components
private mxScrollableDesktop desktop = new mxScrollableDesktop();
- private TMStatusBar statusBar = new TMStatusBar();
+ private TMStatusBar statusBar;
private JToolBar toolBar = new JToolBar(JToolBar.HORIZONTAL);
private JToolBar toolBarMDI = new JToolBar(JToolBar.HORIZONTAL);
private JToolBar toolPalette = new JToolBar(JToolBar.VERTICAL);
@@ -105,6 +105,7 @@ public class TMUI extends JFrame {
private TMStretchDialog stretchDialog;
private TMCanvasSizeDialog canvasSizeDialog;
private TMBlockSizeDialog blockSizeDialog;
+ private TMTileSizeDialog tileSizeDialog;
private TMAddToTreeDialog addBookmarkDialog;
private TMAddToTreeDialog addPaletteDialog;
private TMOrganizeTreeDialog organizeBookmarksDialog;
@@ -239,8 +240,23 @@ public class TMUI extends JFrame {
private JMenuItem _3200MenuItem = new JMenuItem("3200%");
private JMenu blockSizeMenu = new JMenu("Block Size");
private JCheckBoxMenuItem sizeBlockToCanvasMenuItem = new JCheckBoxMenuItem("Full Canvas");
+ private JCheckBoxMenuItem sizeBlockToCanvasMenuItem8x8 = new JCheckBoxMenuItem("8x8 pixels");
+ private JCheckBoxMenuItem sizeBlockToCanvasMenuItem8x16 = new JCheckBoxMenuItem("8x16 pixels");
+ private JCheckBoxMenuItem sizeBlockToCanvasMenuItem16x16 = new JCheckBoxMenuItem("16x16 pixels");
+ private JCheckBoxMenuItem sizeBlockToCanvasMenuItem24x24 = new JCheckBoxMenuItem("24x24 pixels");
+ private JCheckBoxMenuItem sizeBlockToCanvasMenuItem32x32 = new JCheckBoxMenuItem("32x32 pixels");
private JMenuItem customBlockSizeMenuItem = new JMenuItem("Custom...");
+ private JMenuItem customTileSizeMenuItem = new JMenuItem("Custom Tile Size...");
private JRadioButtonMenuItem rowInterleaveBlocksMenuItem = new JRadioButtonMenuItem("Row-interleave Blocks");
+ private JMenu swizzleMenu = new JMenu("Swizzle");
+ private JRadioButtonMenuItem swizzleNoneMenuItem = new JRadioButtonMenuItem("None");
+ private JRadioButtonMenuItem swizzleBCMenuItem = new JRadioButtonMenuItem("BC");
+ private JRadioButtonMenuItem swizzlePSPMenuItem = new JRadioButtonMenuItem("PSP");
+ private JRadioButtonMenuItem swizzleNDSMenuItem = new JRadioButtonMenuItem("NDS");
+ private JRadioButtonMenuItem swizzle3DSMenuItem = new JRadioButtonMenuItem("3DS");
+ private JRadioButtonMenuItem swizzleWiiMenuItem = new JRadioButtonMenuItem("WII");
+ private JRadioButtonMenuItem swizzleSWITCHMenuItem = new JRadioButtonMenuItem("SWITCH");
+ private JRadioButtonMenuItem swizzleCustomMenuItem = new JRadioButtonMenuItem("Custom");
private JMenu modeMenu = new JMenu("Mode");
private JRadioButtonMenuItem _1DimensionalMenuItem = new JRadioButtonMenuItem("1-Dimensional");
private JRadioButtonMenuItem _2DimensionalMenuItem = new JRadioButtonMenuItem("2-Dimensional");
@@ -292,6 +308,7 @@ public class TMUI extends JFrame {
private ButtonGroup paletteButtonGroup = new ButtonGroup();
private ButtonGroup modeButtonGroup = new ButtonGroup();
private ButtonGroup paletteEndiannessButtonGroup = new ButtonGroup();
+ private ButtonGroup swizzleButtonGroup = new ButtonGroup();
private Hashtable tileCodecButtonHashtable = new Hashtable();
private Hashtable colorCodecButtonHashtable = new Hashtable();
@@ -401,6 +418,15 @@ public TMUI() {
sizeBlockToCanvasMenuItem.setText(xlate("Full_Canvas"));
customBlockSizeMenuItem.setText(xlate("Custom_Block_Size"));
rowInterleaveBlocksMenuItem.setText(xlate("Row_Interleave_Blocks"));
+ swizzleMenu.setText(xlate("Swizzle"));
+ swizzleNoneMenuItem.setText(xlate("Swizzle_None"));
+ swizzleBCMenuItem.setText(xlate("Swizzle_BC"));
+ swizzlePSPMenuItem.setText(xlate("Swizzle_PSP"));
+ swizzleNDSMenuItem.setText(xlate("Swizzle_NDS"));
+ swizzle3DSMenuItem.setText(xlate("Swizzle_3DS"));
+ swizzleWiiMenuItem.setText(xlate("Swizzle_WII"));
+ swizzleSWITCHMenuItem.setText(xlate("Swizzle_SWITCH"));
+ swizzleCustomMenuItem.setText(xlate("Swizzle_Custom"));
blockGridMenuItem.setText(xlate("Block_Grid"));
tileGridMenuItem.setText(xlate("Tile_Grid"));
pixelGridMenuItem.setText(xlate("Pixel_Grid"));
@@ -495,6 +521,7 @@ public TMUI() {
stretchDialog = new TMStretchDialog(this, xl);
canvasSizeDialog = new TMCanvasSizeDialog(this, xl);
blockSizeDialog = new TMBlockSizeDialog(this, xl);
+ tileSizeDialog = new TMTileSizeDialog(this, xl);
addBookmarkDialog = new TMAddToTreeDialog(this, "Add_To_Bookmarks_Dialog_Title", xl);
addPaletteDialog = new TMAddToTreeDialog(this, "Add_To_Palettes_Dialog_Title", xl);
organizeBookmarksDialog = new TMOrganizeTreeDialog(this, "Organize_Bookmarks_Dialog_Title", xl);
@@ -506,6 +533,9 @@ public TMUI() {
newPaletteDialog.setCodecs(colorcodecs);
importInternalPaletteDialog.setCodecs(colorcodecs);
+ // Initialize status bar with reference to this TMUI instance
+ statusBar = new TMStatusBar(this);
+
// Set up the GUI.
// main contentpane
JPanel pane = new JPanel();
@@ -1424,9 +1454,47 @@ public void actionPerformed(ActionEvent e) {
doSizeBlockToCanvasCommand();
}
});
+ sizeBlockToCanvasMenuItem8x8.addActionListener(
+ new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ doSizeBlock8x8ToCanvasCommand();
+ }
+ });
+ sizeBlockToCanvasMenuItem8x16.addActionListener(
+ new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ doSizeBlock8x16ToCanvasCommand();
+ }
+ });
+ sizeBlockToCanvasMenuItem16x16.addActionListener(
+ new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ doSizeBlock16x16ToCanvasCommand();
+ }
+ });
+ sizeBlockToCanvasMenuItem24x24.addActionListener(
+ new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ doSizeBlock24x24ToCanvasCommand();
+ }
+ });
+ sizeBlockToCanvasMenuItem32x32.addActionListener(
+ new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ doSizeBlock32x32ToCanvasCommand();
+ }
+ });
blockSizeMenu.add(sizeBlockToCanvasMenuItem);
//
blockSizeMenu.addSeparator();
+ // Pre Defined Block Size
+ blockSizeMenu.add(sizeBlockToCanvasMenuItem8x8);
+ blockSizeMenu.add(sizeBlockToCanvasMenuItem8x16);
+ blockSizeMenu.add(sizeBlockToCanvasMenuItem16x16);
+ blockSizeMenu.add(sizeBlockToCanvasMenuItem24x24);
+ blockSizeMenu.add(sizeBlockToCanvasMenuItem32x32);
+ //
+ blockSizeMenu.addSeparator();
// Custom Block Size
customBlockSizeMenuItem.setMnemonic(KeyEvent.VK_C);
customBlockSizeMenuItem.addActionListener(
@@ -1437,6 +1505,15 @@ public void actionPerformed(ActionEvent e) {
});
blockSizeMenu.add(customBlockSizeMenuItem);
viewMenu.add(blockSizeMenu);
+ // Custom Tile Size
+ customTileSizeMenuItem.setText(xlate("Custom_Tile_Size"));
+ customTileSizeMenuItem.addActionListener(
+ new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ doCustomTileSizeCommand();
+ }
+ });
+ viewMenu.add(customTileSizeMenuItem);
// Row-interleave Blocks
rowInterleaveBlocksMenuItem.setMnemonic(KeyEvent.VK_R);
rowInterleaveBlocksMenuItem.addActionListener(
@@ -1446,6 +1523,50 @@ public void actionPerformed(ActionEvent e) {
}
});
viewMenu.add(rowInterleaveBlocksMenuItem);
+ // Swizzle submenu
+ swizzleNoneMenuItem.setMnemonic(KeyEvent.VK_N);
+ swizzleNoneMenuItem.addActionListener(e -> doSwizzleCommand(TileCodec.SWIZZLE_NONE));
+ swizzleButtonGroup.add(swizzleNoneMenuItem);
+ swizzleMenu.add(swizzleNoneMenuItem);
+
+ swizzleBCMenuItem.setMnemonic(KeyEvent.VK_B);
+ swizzleBCMenuItem.addActionListener(e -> doSwizzleCommand(TileCodec.SWIZZLE_BC));
+ swizzleButtonGroup.add(swizzleBCMenuItem);
+ swizzleMenu.add(swizzleBCMenuItem);
+
+ swizzlePSPMenuItem.setMnemonic(KeyEvent.VK_P);
+ swizzlePSPMenuItem.addActionListener(e -> doSwizzleCommand(TileCodec.SWIZZLE_PSP));
+ swizzleButtonGroup.add(swizzlePSPMenuItem);
+ swizzleMenu.add(swizzlePSPMenuItem);
+
+ swizzleNDSMenuItem.setMnemonic(KeyEvent.VK_D);
+ swizzleNDSMenuItem.addActionListener(e -> doSwizzleCommand(TileCodec.SWIZZLE_NDS));
+ swizzleButtonGroup.add(swizzleNDSMenuItem);
+ swizzleMenu.add(swizzleNDSMenuItem);
+
+ swizzle3DSMenuItem.setMnemonic(KeyEvent.VK_3);
+ swizzle3DSMenuItem.addActionListener(e -> doSwizzleCommand(TileCodec.SWIZZLE_3DS));
+ swizzleButtonGroup.add(swizzle3DSMenuItem);
+ swizzleMenu.add(swizzle3DSMenuItem);
+
+ swizzleWiiMenuItem.setMnemonic(KeyEvent.VK_W);
+ swizzleWiiMenuItem.addActionListener(e -> doSwizzleCommand(TileCodec.SWIZZLE_WII));
+ swizzleButtonGroup.add(swizzleWiiMenuItem);
+ swizzleMenu.add(swizzleWiiMenuItem);
+
+ swizzleSWITCHMenuItem.setMnemonic(KeyEvent.VK_H);
+ swizzleSWITCHMenuItem.addActionListener(e -> doSwizzleCommand(TileCodec.SWIZZLE_SWITCH));
+ swizzleButtonGroup.add(swizzleSWITCHMenuItem);
+ swizzleMenu.add(swizzleSWITCHMenuItem);
+
+ swizzleCustomMenuItem.setMnemonic(KeyEvent.VK_C);
+ swizzleCustomMenuItem.addActionListener(e -> doCustomSwizzleCommand());
+ swizzleButtonGroup.add(swizzleCustomMenuItem);
+ swizzleMenu.add(swizzleCustomMenuItem);
+
+ // Default to None
+ swizzleNoneMenuItem.setSelected(true);
+ viewMenu.add(swizzleMenu);
//
viewMenu.addSeparator();
// Block Grid
@@ -2827,6 +2948,123 @@ public void doSizeBlockToCanvasCommand() {
if (view != null) {
view.setSizeBlockToCanvas(!view.getSizeBlockToCanvas());
sizeBlockToCanvasMenuItem.setSelected(view.getSizeBlockToCanvas());
+ sizeBlockToCanvasMenuItem8x8.setSelected(false);
+ sizeBlockToCanvasMenuItem8x16.setSelected(false);
+ sizeBlockToCanvasMenuItem16x16.setSelected(false);
+ sizeBlockToCanvasMenuItem24x24.setSelected(false);
+ sizeBlockToCanvasMenuItem32x32.setSelected(false);
+
+ // Update status bar to reflect the change
+ if (statusBar != null) {
+ statusBar.updateBlockSizeLabel(view.getSizeBlockToCanvas());
+ statusBar.setBlockSize(view.getBlockWidth(), view.getBlockHeight());
+ }
+ }
+ }
+
+ /**
+ *
+ *
+ *
+ **/
+
+ public void doSizeBlock8x8ToCanvasCommand() {
+ TMView view = getSelectedView();
+ if (view != null) {
+ // TMBlockSizeDialog blockSizeDialog = new TMBlockSizeDialog(this, xl);
+ view.setSizeBlockToCanvas(false);
+ sizeBlockToCanvasMenuItem.setSelected(false);
+ sizeBlockToCanvasMenuItem8x8.setSelected(true);
+ sizeBlockToCanvasMenuItem8x16.setSelected(false);
+ sizeBlockToCanvasMenuItem16x16.setSelected(false);
+ sizeBlockToCanvasMenuItem24x24.setSelected(false);
+ sizeBlockToCanvasMenuItem32x32.setSelected(false);
+ view.setBlockDimensions(1, 1);
+ }
+ }
+
+ /**
+ *
+ *
+ *
+ **/
+
+ public void doSizeBlock8x16ToCanvasCommand() {
+ TMView view = getSelectedView();
+ if (view != null) {
+ // TMBlockSizeDialog blockSizeDialog = new TMBlockSizeDialog(this, xl);
+ view.setSizeBlockToCanvas(false);
+ sizeBlockToCanvasMenuItem.setSelected(false);
+ sizeBlockToCanvasMenuItem8x8.setSelected(false);
+ sizeBlockToCanvasMenuItem8x16.setSelected(true);
+ sizeBlockToCanvasMenuItem16x16.setSelected(false);
+ sizeBlockToCanvasMenuItem24x24.setSelected(false);
+ sizeBlockToCanvasMenuItem32x32.setSelected(false);
+ view.setBlockDimensions(1, 2);
+ }
+ }
+
+ /**
+ *
+ *
+ *
+ **/
+
+ public void doSizeBlock16x16ToCanvasCommand() {
+ TMView view = getSelectedView();
+ if (view != null) {
+ // TMBlockSizeDialog blockSizeDialog = new TMBlockSizeDialog(this, xl);
+ view.setSizeBlockToCanvas(false);
+ sizeBlockToCanvasMenuItem.setSelected(false);
+ sizeBlockToCanvasMenuItem8x8.setSelected(false);
+ sizeBlockToCanvasMenuItem8x16.setSelected(false);
+ sizeBlockToCanvasMenuItem16x16.setSelected(true);
+ sizeBlockToCanvasMenuItem24x24.setSelected(false);
+ sizeBlockToCanvasMenuItem32x32.setSelected(false);
+ view.setBlockDimensions(2, 2);
+ }
+ }
+
+
+ /**
+ *
+ *
+ *
+ **/
+
+ public void doSizeBlock24x24ToCanvasCommand() {
+ TMView view = getSelectedView();
+ if (view != null) {
+ // TMBlockSizeDialog blockSizeDialog = new TMBlockSizeDialog(this, xl);
+ view.setSizeBlockToCanvas(false);
+ sizeBlockToCanvasMenuItem.setSelected(false);
+ sizeBlockToCanvasMenuItem8x8.setSelected(false);
+ sizeBlockToCanvasMenuItem8x16.setSelected(false);
+ sizeBlockToCanvasMenuItem16x16.setSelected(false);
+ sizeBlockToCanvasMenuItem24x24.setSelected(true);
+ sizeBlockToCanvasMenuItem32x32.setSelected(false);
+ view.setBlockDimensions(3, 3);
+ }
+ }
+
+ /**
+ *
+ *
+ *
+ **/
+
+ public void doSizeBlock32x32ToCanvasCommand() {
+ TMView view = getSelectedView();
+ if (view != null) {
+ // TMBlockSizeDialog blockSizeDialog = new TMBlockSizeDialog(this, xl);
+ view.setSizeBlockToCanvas(false);
+ sizeBlockToCanvasMenuItem.setSelected(false);
+ sizeBlockToCanvasMenuItem8x8.setSelected(false);
+ sizeBlockToCanvasMenuItem8x16.setSelected(false);
+ sizeBlockToCanvasMenuItem16x16.setSelected(false);
+ sizeBlockToCanvasMenuItem24x24.setSelected(false);
+ sizeBlockToCanvasMenuItem32x32.setSelected(true);
+ view.setBlockDimensions(4, 4);
}
}
@@ -2849,6 +3087,74 @@ public void doCustomBlockSizeCommand() {
}
}
+ /**
+ *
+ * Handles menu command "Custom Tile Size".
+ *
+ **/
+ public void doCustomTileSizeCommand() {
+ TMView view = getSelectedView();
+ if (view != null) {
+ TileCodec codec = view.getTileCodec();
+ if (codec != null) {
+ int currentWidth = codec.getTileWidth();
+ int currentHeight = codec.getTileHeight();
+ int retVal = tileSizeDialog.showDialog(currentWidth, currentHeight);
+ if (retVal == JOptionPane.OK_OPTION) {
+ int newWidth = tileSizeDialog.getTileWidth();
+ int newHeight = tileSizeDialog.getTileHeight();
+ codec.setTileDimensions(newWidth, newHeight);
+ // Redimensionar o canvas para acomodar os novos tamanhos de tile
+ TMTileCanvas canvas = view.getEditorCanvas();
+ canvas.setGridSize(canvas.getCols(), canvas.getRows());
+ // Limpar e redecodificar os pixels com as novas dimensões
+ canvas.unpackPixels();
+ view.repaint();
+ // Update status bar display
+ statusBar.setTileSize(newWidth, newHeight);
+ }
+ }
+ }
+ }
+
+ /**
+ *
+ * Sets tile size directly (called from status bar spinners).
+ *
+ **/
+
+ public void setTileSize(int width, int height) {
+ TMView view = getSelectedView();
+ if (view != null) {
+ TileCodec codec = view.getTileCodec();
+ if (codec != null) {
+ codec.setTileDimensions(width, height);
+ // Redimensionar o canvas para acomodar os novos tamanhos de tile
+ TMTileCanvas canvas = view.getEditorCanvas();
+ canvas.setGridSize(canvas.getCols(), canvas.getRows());
+ // Limpar e redecodificar os pixels com as novas dimensões
+ canvas.unpackPixels();
+ view.repaint();
+ }
+ }
+ }
+
+ /**
+ *
+ * Sets block size directly (called from status bar spinners).
+ * Block dimensions are always independent of canvas size.
+ *
+ **/
+
+ public void setBlockSize(int width, int height) {
+ TMView view = getSelectedView();
+ if (view != null) {
+ view.setBlockDimensions(width, height);
+ // Update status bar display
+ statusBar.setBlockSize(width, height);
+ }
+ }
+
/**
*
*
@@ -2863,6 +3169,66 @@ public void doRowInterleaveBlocksCommand() {
}
}
+ /**
+ *
+ * Sets the swizzle pattern for the current view.
+ *
+ **/
+
+ public void doSwizzleCommand(String swizzlePattern) {
+ TMView view = getSelectedView();
+ if (view != null) {
+ // Set optimal tile dimensions for the selected swizzle pattern
+ tm.tilecodecs.SwizzleUtil.setOptimalTileDimensions(swizzlePattern, view.getTileCodec());
+
+ view.setSwizzlePattern(swizzlePattern);
+ updateSwizzleMenuSelection(swizzlePattern);
+ refreshStatusBar(); // Update status bar to show swizzle info
+
+ // Refresh the view to apply new tile dimensions
+ view.getEditorCanvas().repaint();
+ }
+ }
+
+ /**
+ *
+ * Opens the custom swizzle configuration dialog.
+ *
+ **/
+
+ public void doCustomSwizzleCommand() {
+ TMView view = getSelectedView();
+ if (view != null) {
+ CustomSwizzleDialog dialog = new CustomSwizzleDialog(this, view.getTileCodec());
+ dialog.setVisible(true);
+ if (dialog.isOK()) {
+ view.setSwizzlePattern(TileCodec.SWIZZLE_CUSTOM);
+ updateSwizzleMenuSelection(TileCodec.SWIZZLE_CUSTOM);
+ refreshStatusBar(); // Update status bar to show swizzle info
+ } else {
+ // If cancelled, revert to previous selection
+ updateSwizzleMenuSelection(view.getSwizzlePattern());
+ }
+ }
+ }
+
+ /**
+ *
+ * Updates the swizzle menu selection based on the current pattern.
+ *
+ **/
+
+ private void updateSwizzleMenuSelection(String swizzlePattern) {
+ swizzleNoneMenuItem.setSelected(TileCodec.SWIZZLE_NONE.equals(swizzlePattern));
+ swizzleBCMenuItem.setSelected(TileCodec.SWIZZLE_BC.equals(swizzlePattern));
+ swizzlePSPMenuItem.setSelected(TileCodec.SWIZZLE_PSP.equals(swizzlePattern));
+ swizzleNDSMenuItem.setSelected(TileCodec.SWIZZLE_NDS.equals(swizzlePattern));
+ swizzle3DSMenuItem.setSelected(TileCodec.SWIZZLE_3DS.equals(swizzlePattern));
+ swizzleWiiMenuItem.setSelected(TileCodec.SWIZZLE_WII.equals(swizzlePattern));
+ swizzleSWITCHMenuItem.setSelected(TileCodec.SWIZZLE_SWITCH.equals(swizzlePattern));
+ swizzleCustomMenuItem.setSelected(TileCodec.SWIZZLE_CUSTOM.equals(swizzlePattern));
+ }
+
/**
*
*
@@ -4205,6 +4571,7 @@ public void viewSelected(TMView view) {
tileGridMenuItem.setSelected(ec.isTileGridVisible());
pixelGridMenuItem.setSelected(ec.isPixelGridVisible());
rowInterleaveBlocksMenuItem.setSelected(ec.getRowInterleaveBlocks());
+ updateSwizzleMenuSelection(ec.getSwizzlePattern());
refreshModeSelection(view);
refreshTileCodecSelection(view);
@@ -4303,6 +4670,11 @@ public void refreshStatusBar() {
TMView view = getSelectedView();
if (view != null) {
statusBar.viewSelected(view);
+ // Update tile size spinners
+ TileCodec codec = view.getTileCodec();
+ if (codec != null) {
+ statusBar.setTileSize(codec.getTileWidth(), codec.getTileHeight());
+ }
}
}
diff --git a/src/main/java/tm/ui/TMView.java b/src/main/java/tm/ui/TMView.java
index 59ca5c9..70755f9 100644
--- a/src/main/java/tm/ui/TMView.java
+++ b/src/main/java/tm/ui/TMView.java
@@ -110,7 +110,9 @@ public void internalFrameClosing(InternalFrameEvent e) {
addComponentListener(new ComponentAdapter() {
public void componentResized(ComponentEvent e) {
- slider.setSize(slider.getWidth(), editorCanvas.getHeight());
+ if (editorCanvas != null) {
+ slider.setSize(slider.getWidth(), editorCanvas.getHeight());
+ }
// slider.setSize(slider.getWidth(),
// getHeight()-((BasicInternalFrameUI)getUI()).getNorthPane().getHeight());
}
@@ -389,6 +391,8 @@ private void updateSlider() {
/**
*
* Sets the size of the tile grid.
+ *
+ * Block dimensions are always independent of canvas size.
*
**/
@@ -402,9 +406,7 @@ else if (cols > 1024)
else if (rows > 1024)
rows = 1024;
editorCanvas.setGridSize(cols, rows);
- if (sizeBlockToCanvas) {
- editorCanvas.setBlockDimensions(cols, rows);
- }
+ // Block dimensions are always independent of canvas size
updateSlider();
editorCanvas.unpackPixels();
setScale(getScale());
@@ -808,6 +810,7 @@ public void gotoBookmark(BookmarkItemNode bookmark) {
editorCanvas.setBlockDimensions(bookmark.getBlockWidth(), bookmark.getBlockHeight());
sizeBlockToCanvas = bookmark.getSizeBlockToCanvas();
editorCanvas.setRowInterleaveBlocks(bookmark.getRowInterleaved());
+ editorCanvas.setSwizzlePattern(bookmark.getSwizzlePattern());
editorCanvas.setMode(bookmark.getMode());
setAbsoluteOffset(bookmark.getOffset());
// editorCanvas.setPalette(
@@ -837,6 +840,7 @@ public BookmarkItemNode createBookmark(String description) {
getMode(),
getPalIndex(),
getTileCodec(),
+ getSwizzlePattern(),
description);
}
@@ -953,20 +957,20 @@ public int getBlockHeight() {
}
/**
- *
- * Gets whether blocks should be row-interleaved.
- *
- **/
+ *
+ * Gets whether blocks should be row-interleaved.
+ *
+ **/
public boolean getRowInterleaveBlocks() {
return editorCanvas.getRowInterleaveBlocks();
}
/**
- *
- * Sets whether blocks should be row-interleaved.
- *
- **/
+ *
+ * Sets whether blocks should be row-interleaved.
+ *
+ **/
public void setRowInterleaveBlocks(boolean rowInterleaved) {
editorCanvas.setRowInterleaveBlocks(rowInterleaved);
@@ -975,6 +979,26 @@ public void setRowInterleaveBlocks(boolean rowInterleaved) {
}
/**
+ *
+ * Gets the current swizzle pattern.
+ *
+ **/
+
+ public String getSwizzlePattern() {
+ return editorCanvas.getSwizzlePattern();
+ }
+
+ /**
+ *
+ * Sets the swizzle pattern for tile rendering.
+ *
+ **/
+
+ public void setSwizzlePattern(String swizzlePattern) {
+ editorCanvas.setSwizzlePattern(swizzlePattern);
+ editorCanvas.unpackPixels();
+ editorCanvas.repaint();
+ } /**
*
* Sets whether the block size should follow the canvas size.
*
diff --git a/src/main/resources/languages/language.properties b/src/main/resources/languages/language.properties
index c26e21c..01307e8 100644
--- a/src/main/resources/languages/language.properties
+++ b/src/main/resources/languages/language.properties
@@ -33,6 +33,16 @@ Tile_Grid = Tile Grid
Pixel_Grid = Pixel Grid
Dark_Mode = Dark Mode
+# Swizzle settings
+Swizzle = Swizzle
+Swizzle_None = None
+Swizzle_BC = BC Texture
+Swizzle_PSP = PlayStation Portable
+Swizzle_NDS = Nintendo DS
+Swizzle_3DS = Nintendo 3DS
+Swizzle_WII = Nintendo Wii
+Swizzle_SWITCH = Nintendo Switch
+
# Edit Menu
Edit = Edit
@@ -203,6 +213,10 @@ Offset_Prompt = Offset:
Size_Prompt = Size:
Columns_Prompt = Columns:
Rows_Prompt = Rows:
+Tile_Width_Prompt = Tile Width (pixels):
+Tile_Height_Prompt = Tile Height (pixels):
+Tile_Size_Dialog_Title = Tile Size
+Custom_Tile_Size = Custom Tile Size...
# Other
diff --git a/src/main/resources/languages/language_en_US.properties b/src/main/resources/languages/language_en_US.properties
index c26e21c..a467365 100644
--- a/src/main/resources/languages/language_en_US.properties
+++ b/src/main/resources/languages/language_en_US.properties
@@ -33,7 +33,16 @@ Tile_Grid = Tile Grid
Pixel_Grid = Pixel Grid
Dark_Mode = Dark Mode
-# Edit Menu
+# Swizzle settings
+Swizzle = Swizzle
+Swizzle_None = None
+Swizzle_BC = BC Texture
+Swizzle_PSP = PlayStation Portable
+Swizzle_NDS = Nintendo DS
+Swizzle_3DS = Nintendo 3DS
+Swizzle_WII = Nintendo Wii
+Swizzle_SWITCH = Nintendo Switch
+Swizzle_Custom = Custom...
Edit = Edit
Undo = Undo
@@ -203,6 +212,10 @@ Offset_Prompt = Offset:
Size_Prompt = Size:
Columns_Prompt = Columns:
Rows_Prompt = Rows:
+Tile_Width_Prompt = Tile Width (pixels):
+Tile_Height_Prompt = Tile Height (pixels):
+Tile_Size_Dialog_Title = Tile Size
+Custom_Tile_Size = Custom Tile Size...
# Other
diff --git a/tmspec.xml b/tmspec.xml
index 91d70c5..c7b2de8 100644
--- a/tmspec.xml
+++ b/tmspec.xml
@@ -30,6 +30,10 @@
000000990000009900CC6600000099990099009999CCCCCC666666FF666666FF66FFFF666666FFFF66FF66FFFFFFFFFF
Enhanced Graphics Adapter (EGA)
+
+ 000000111111222222333333444444555555666666777777888888999999AAAAAABBBBBBCCCCCCDDDDDDEEEEEEFFFFFF
+ Grayscale
+
@@ -42,12 +46,18 @@
2bpp planar
+
+ 2bpp planar reverse
+
3bpp planar
4bpp planar
+
+ 4bpp planar reverse
+
5bpp planar
@@ -65,12 +75,24 @@
2bpp planar, composite
+
+ 3bpp planar, composite (3x1bpp)
+
3bpp planar, composite (2bpp+1bpp)
+
+ 3bpp planar reverse, composite (2bpp+1bpp)
+
+
+ 4bpp planar, composite (4x1bpp)
+
4bpp planar, composite (2x2bpp)
+
+ 4bpp planar reverse, composite (2x2bpp)
+
8bpp planar, composite (4x2bpp)
@@ -206,31 +228,31 @@
FCEUltra Save States (*.fc?)
-
Genecyst/Kega/Gens Save States (*.gs?)
-
+
+ Sega Genesis CRAM Dump (*.bin)
+
NESticle Save States (*.st?)
-
ZSNES Save States (*.zs?)
-
+
+ Mesen SNES CGRAM Dump (*.dmp)
+
Tile Layer Pro palette (*.tpl)
-
Windows Palette (*.pal)
-
Raw palette file (*.col,*.pal)
-
+
@@ -239,7 +261,7 @@
-
+