From 2c77a6fd3bebdc3eab74ee7052a7d38abe37596e Mon Sep 17 00:00:00 2001 From: Matthew Schwartz <277844502+mrkinglollipop@users.noreply.github.com> Date: Sat, 11 Jul 2026 23:15:02 -0400 Subject: [PATCH] HD terrain: bump tile resolution 64->256, restructure mip chain, add legacy-TGA loader fallback TILE_PIXEL_EXTENT 64->256 and atlas TEXTURE_WIDTH 2048->8192 (TileData.h, mirrored in TileData.cpp's shadow macro copy). Mip chain restructured from 6 fixed levels (32/16/8/4/2/1) to 8 chained levels (128/64/32/16/8/4/2/1) so updateMips/doMip keep halving exactly once per level -- a naive constant bump alone would have made the first doMip() write a 128x128 result into the old 32x32 buffer. WorldHeightMap.cpp countTiles/readTiles gain a legacy-grid detection+scatter path (ChooseTileGrid + LEGACY_TILE_PIXEL_EXTENT=64) so unmodified base-game and custom-map TGAs, still packed as NxN grids of 64px cells, keep loading: each source pixel is nearest-neighbor scattered into the corresponding 4x4 block of the native 256px tile buffer during the existing single-pass scanline read. The one genuinely ambiguous width (256px, valid as both a native 1x1 HD grid and a legacy 4x4 grid of 16 tiles) is disambiguated using the texture class's INI-declared tile count, threaded through from countTiles to readTiles. Grep-audited TileData.*, WorldHeightMap.*, TerrainTex.* for remaining hardcoded 64/32/2048 literals -- none remain outside comments/unrelated constants (VERTEX_BUFFER_TILE_LENGTH is a vertex-grid count, m_numBitmapTiles<2048 is a tile-count sanity assert, both confirmed unaffected by the pixel-size change). Builds clean via scripts/build/macos/build-macos-zh.sh (attempt 1/3). --- .../Include/W3DDevice/GameClient/TileData.h | 40 +++-- .../W3DDevice/GameClient/WorldHeightMap.h | 8 +- .../Source/W3DDevice/GameClient/TileData.cpp | 52 ++++-- .../W3DDevice/GameClient/WorldHeightMap.cpp | 166 +++++++++++++----- 4 files changed, 191 insertions(+), 75 deletions(-) diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/TileData.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/TileData.h index 0acb667b6c8..3cea590e652 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/TileData.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/TileData.h @@ -45,17 +45,25 @@ typedef struct { #define INVERTED_MASK 0x1 //AND this with TBlendTileInfo.inverted to get actual inverted state #define FLIPPED_MASK 0x2 //AND this with TBlendTileInfo.inverted to get forced flip state (for horizontal/vertical flips). -#define TILE_PIXEL_EXTENT 64 +// GeneralsX @feature mrkinglollipop 11/07/2026 Bumps base tile resolution 64->256 (4x per axis, 16x pixels). +// The mip chain grows from 6 fixed levels (32/16/8/4/2/1) to 8 chained levels +// (128/64/32/16/8/4/2/1) so doMip() still halves exactly once per level down to 1x1. +// LEGACY_TILE_PIXEL_EXTENT (see WorldHeightMap.cpp) is the pre-patch 64px grid cell +// size that unmodified base/custom-map TGAs are still packed in; the loader detects +// and nearest-neighbor upscales those on read so they keep working unmodified. +#define TILE_PIXEL_EXTENT 256 #define TILE_BYTES_PER_PIXEL 4 #define DATA_LEN_BYTES TILE_PIXEL_EXTENT*TILE_PIXEL_EXTENT*TILE_BYTES_PER_PIXEL #define DATA_LEN_PIXELS TILE_PIXEL_EXTENT*TILE_PIXEL_EXTENT -#define TILE_PIXEL_EXTENT_MIP1 32 -#define TILE_PIXEL_EXTENT_MIP2 16 -#define TILE_PIXEL_EXTENT_MIP3 8 -#define TILE_PIXEL_EXTENT_MIP4 4 -#define TILE_PIXEL_EXTENT_MIP5 2 -#define TILE_PIXEL_EXTENT_MIP6 1 -#define TEXTURE_WIDTH 2048 // was 1024 jba +#define TILE_PIXEL_EXTENT_MIP1 128 +#define TILE_PIXEL_EXTENT_MIP2 64 +#define TILE_PIXEL_EXTENT_MIP3 32 +#define TILE_PIXEL_EXTENT_MIP4 16 +#define TILE_PIXEL_EXTENT_MIP5 8 +#define TILE_PIXEL_EXTENT_MIP6 4 +#define TILE_PIXEL_EXTENT_MIP7 2 +#define TILE_PIXEL_EXTENT_MIP8 1 +#define TEXTURE_WIDTH 8192 // GeneralsX @feature mrkinglollipop 11/07/2026 Bumps atlas TEXTURE_WIDTH 2048->8192 (was 2048). MoltenVK/Metal cap is 16384. /** This class holds the bitmap data from the .tga texture files. It is used to create the D3D texture in the game and 3d windows, and to create DIB data for the @@ -68,13 +76,15 @@ class TileData : public RefCountClass // Also, first byte is lower left pixel, not upper left pixel. // so 0,0 is lower left, not upper left. UnsignedByte m_tileData[DATA_LEN_BYTES]; - /// Mipped down copies of the tile data. - UnsignedByte m_tileDataMip32[TILE_PIXEL_EXTENT_MIP1*TILE_PIXEL_EXTENT_MIP1*TILE_BYTES_PER_PIXEL]; - UnsignedByte m_tileDataMip16[TILE_PIXEL_EXTENT_MIP2*TILE_PIXEL_EXTENT_MIP2*TILE_BYTES_PER_PIXEL]; - UnsignedByte m_tileDataMip8[TILE_PIXEL_EXTENT_MIP3*TILE_PIXEL_EXTENT_MIP3*TILE_BYTES_PER_PIXEL]; - UnsignedByte m_tileDataMip4[TILE_PIXEL_EXTENT_MIP4*TILE_PIXEL_EXTENT_MIP4*TILE_BYTES_PER_PIXEL]; - UnsignedByte m_tileDataMip2[TILE_PIXEL_EXTENT_MIP5*TILE_PIXEL_EXTENT_MIP5*TILE_BYTES_PER_PIXEL]; - UnsignedByte m_tileDataMip1[TILE_PIXEL_EXTENT_MIP6*TILE_PIXEL_EXTENT_MIP6*TILE_BYTES_PER_PIXEL]; + /// Mipped down copies of the tile data. 8-level chain: 128/64/32/16/8/4/2/1. + UnsignedByte m_tileDataMip128[TILE_PIXEL_EXTENT_MIP1*TILE_PIXEL_EXTENT_MIP1*TILE_BYTES_PER_PIXEL]; + UnsignedByte m_tileDataMip64[TILE_PIXEL_EXTENT_MIP2*TILE_PIXEL_EXTENT_MIP2*TILE_BYTES_PER_PIXEL]; + UnsignedByte m_tileDataMip32[TILE_PIXEL_EXTENT_MIP3*TILE_PIXEL_EXTENT_MIP3*TILE_BYTES_PER_PIXEL]; + UnsignedByte m_tileDataMip16[TILE_PIXEL_EXTENT_MIP4*TILE_PIXEL_EXTENT_MIP4*TILE_BYTES_PER_PIXEL]; + UnsignedByte m_tileDataMip8[TILE_PIXEL_EXTENT_MIP5*TILE_PIXEL_EXTENT_MIP5*TILE_BYTES_PER_PIXEL]; + UnsignedByte m_tileDataMip4[TILE_PIXEL_EXTENT_MIP6*TILE_PIXEL_EXTENT_MIP6*TILE_BYTES_PER_PIXEL]; + UnsignedByte m_tileDataMip2[TILE_PIXEL_EXTENT_MIP7*TILE_PIXEL_EXTENT_MIP7*TILE_BYTES_PER_PIXEL]; + UnsignedByte m_tileDataMip1[TILE_PIXEL_EXTENT_MIP8*TILE_PIXEL_EXTENT_MIP8*TILE_BYTES_PER_PIXEL]; public: ICoord2D m_tileLocationInTexture; diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/WorldHeightMap.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/WorldHeightMap.h index 5cfdcf6cc22..66c03dd25ea 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/WorldHeightMap.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/WorldHeightMap.h @@ -332,8 +332,12 @@ class WorldHeightMap : public RefCountClass, if ((ndx>=0) && (ndxnumTiles, &isLegacyGrid); theFile->seek(0, File::START); if (numTiles >= texClass->numTiles) { numTiles = texClass->numTiles; @@ -1022,7 +1023,7 @@ void WorldHeightMap::readTexClass(TXTextureClass *texClass, TileData **tileData) break; } } - WorldHeightMap::readTiles(pStr, tileData+texClass->firstTile, width); + WorldHeightMap::readTiles(pStr, tileData+texClass->firstTile, width, isLegacyGrid); } theFile->close(); } @@ -1301,17 +1302,69 @@ typedef struct { +// GeneralsX @feature mrkinglollipop 11/07/2026 Adds legacy-grid detection so TGAs packed at the pre-patch +// 64px cell size keep loading now that TILE_PIXEL_EXTENT is 256 (native HD grid cell). +// Unmodified base-game and custom-map TGAs are still packed as NxN grids of +// LEGACY_TILE_PIXEL_EXTENT (64px) cells -- the classic pre-patch tile size. Without a +// fallback, a legacy file's imageWidth/TILE_PIXEL_EXTENT floors to 0 and the file is +// silently rejected (0 tiles), breaking every unmodified map/mod. +// +// Detection rule (documented per plan): try the *native* 256px-cell grid first, but +// only trust it when it alone can satisfy the texture class's INI-declared tile count +// (expectedTileCount). This resolves the one genuinely ambiguous width -- 256px, which +// is simultaneously a valid native 1x1 HD grid AND a valid legacy 4x4 grid of 64px +// cells (16 tiles) -- because the base game genuinely ships 256px-wide legacy sheets +// (width census: 32, 64, 128, 256, 640). Any other width where both interpretations +// divide evenly is treated as legacy too (matches actual on-disk content) -- this is +// the "treat legacy files as 64-grids by default" simplification. The old "halfTile" +// sub-tile case (a lone 32x32 image = half of a legacy 64px cell) keeps keying off the +// legacy half-extent (32), not a new native half-extent, so it matches the same files +// it always did. +#define LEGACY_TILE_PIXEL_EXTENT 64 + +static Bool ChooseTileGrid(Short imageWidth, Short imageHeight, Int expectedTileCount, + Int &outTileWidth, Int &outTileHeight, Bool &outIsLegacyGrid) +{ + Int nativeW = imageWidth / TILE_PIXEL_EXTENT; + Int nativeH = imageHeight / TILE_PIXEL_EXTENT; + Bool nativeValid = (nativeW>=1 && nativeW<=10 && nativeH>=1 && nativeH<=10); + + Int legacyW = imageWidth / LEGACY_TILE_PIXEL_EXTENT; + Int legacyH = imageHeight / LEGACY_TILE_PIXEL_EXTENT; + Bool legacyValid = (legacyW>=1 && legacyW<=10 && legacyH>=1 && legacyH<=10); + + Bool useLegacy; + if (nativeValid && legacyValid) { + // Ambiguous width: only trust native when the class can't possibly want more + // tiles than the (smaller) native grid provides -- i.e. this really is a fresh + // HD single/few-tile replacement, not an unmodified legacy sheet. + useLegacy = !(expectedTileCount > 0 && expectedTileCount <= nativeW*nativeH); + } else if (legacyValid) { + useLegacy = true; + } else if (nativeValid) { + useLegacy = false; + } else { + return false; + } + + outIsLegacyGrid = useLegacy; + outTileWidth = useLegacy ? legacyW : nativeW; + outTileHeight = useLegacy ? legacyH : nativeH; + return true; +} + /// Count how many tiles come in from a targa file. -Int WorldHeightMap::countTiles(InputStream *pStr, Bool *halfTile) +Int WorldHeightMap::countTiles(InputStream *pStr, Bool *halfTile, Int expectedTileCount, Bool *pIsLegacyGrid) { TTargaHeader hdr; if (halfTile) { *halfTile = false; } + if (pIsLegacyGrid) { + *pIsLegacyGrid = false; + } Int len = pStr->read(&hdr,sizeof(hdr)); if (len!=sizeof(hdr)) return(0); - Int tileWidth = hdr.imageWidth/TILE_PIXEL_EXTENT; - Int tileHeight = hdr.imageHeight/TILE_PIXEL_EXTENT; if (hdr.colorMapType != 0) { return(0); // we don't do indexed at this time. jba. @@ -1322,41 +1375,65 @@ Int WorldHeightMap::countTiles(InputStream *pStr, Bool *halfTile) if (hdr.pixelDepth < 24) return(false); if (hdr.pixelDepth > 32) return(false); - // 3x3 gives 9, - // 2x2 gives 4, - // 1x1 gives 1, - // else 0; - if (tileWidth>10 || tileHeight>10) return(0); // don't do huge images, or bad files. - if (tileWidth>=10 && tileHeight >=10) return(100); - if (tileWidth>=9 && tileHeight >=9) return(81); - if (tileWidth>=8 && tileHeight >=8) return(64); - if (tileWidth>=7 && tileHeight >=7) return(49); - if (tileWidth>=6 && tileHeight >=6) return(36); - if (tileWidth>=5 && tileHeight >=5) return(25); - if (tileWidth>=4 && tileHeight >=4) return(16); - if (tileWidth>=3 && tileHeight >=3) return(9); - if (tileWidth>=2 && tileHeight >=2) return(4); - if (tileWidth>=1 && tileHeight >=1) return(1); - if (halfTile && hdr.imageHeight==TILE_PIXEL_EXTENT/2 && hdr.imageWidth==TILE_PIXEL_EXTENT/2) { + + Int tileWidth, tileHeight; + Bool isLegacyGrid = false; + if (ChooseTileGrid(hdr.imageWidth, hdr.imageHeight, expectedTileCount, tileWidth, tileHeight, isLegacyGrid)) { + if (pIsLegacyGrid) *pIsLegacyGrid = isLegacyGrid; + // 3x3 gives 9, + // 2x2 gives 4, + // 1x1 gives 1, + // else 0; + if (tileWidth>=10 && tileHeight >=10) return(100); + if (tileWidth>=9 && tileHeight >=9) return(81); + if (tileWidth>=8 && tileHeight >=8) return(64); + if (tileWidth>=7 && tileHeight >=7) return(49); + if (tileWidth>=6 && tileHeight >=6) return(36); + if (tileWidth>=5 && tileHeight >=5) return(25); + if (tileWidth>=4 && tileHeight >=4) return(16); + if (tileWidth>=3 && tileHeight >=3) return(9); + if (tileWidth>=2 && tileHeight >=2) return(4); + if (tileWidth>=1 && tileHeight >=1) return(1); + } + // Neither native nor legacy full-cell grid fit -- last resort: a lone image half + // as wide/tall as one LEGACY cell (32x32), always legacy-scattered. + if (halfTile && hdr.imageHeight==LEGACY_TILE_PIXEL_EXTENT/2 && hdr.imageWidth==LEGACY_TILE_PIXEL_EXTENT/2) { *halfTile = true; + if (pIsLegacyGrid) *pIsLegacyGrid = true; return 1; } return(0); } -/*Break down a .tga file into a collection of tiles. numRows * numRows total tiles.*/ -Bool WorldHeightMap::readTiles(InputStream *pStr, TileData **tiles, Int numRows) +/*Break down a .tga file into a collection of tiles. numRows * numRows total tiles. + isLegacyGrid must be the interpretation countTiles() already chose for this same + file (via ChooseTileGrid) -- readTiles does not re-derive it, so the two stay + consistent even in the ambiguous-width case. */ +Bool WorldHeightMap::readTiles(InputStream *pStr, TileData **tiles, Int numRows, Bool isLegacyGrid) { TTargaHeader hdr; pStr->read(&hdr, sizeof(hdr)); - Int tileWidth = hdr.imageWidth/TILE_PIXEL_EXTENT; - Int tileHeight = hdr.imageHeight/TILE_PIXEL_EXTENT; - if (hdr.imageHeight==TILE_PIXEL_EXTENT/2) { + // Effective source-cell pixel size for this file's grid. scale is how many times + // each decoded source pixel is nearest-neighbor replicated per axis into the + // native TILE_PIXEL_EXTENT destination cell; scale==1 (no scatter, byte-identical + // to the pre-patch loop) for native HD content. + Int srcCellExtent = isLegacyGrid ? LEGACY_TILE_PIXEL_EXTENT : TILE_PIXEL_EXTENT; + + Int tileWidth = hdr.imageWidth/srcCellExtent; + Int tileHeight = hdr.imageHeight/srcCellExtent; + + // Legacy "halfTile" sub-tile case: a lone image half as wide/tall as one legacy + // cell (32x32) represents a single tile; scatter it across the full native tile. + if (isLegacyGrid && hdr.imageHeight==srcCellExtent/2) { tileHeight = 1; } - if (hdr.imageWidth==TILE_PIXEL_EXTENT/2) { + if (isLegacyGrid && hdr.imageWidth==srcCellExtent/2) { tileWidth = 1; } + if (isLegacyGrid && (hdr.imageWidth==LEGACY_TILE_PIXEL_EXTENT/2 || hdr.imageHeight==LEGACY_TILE_PIXEL_EXTENT/2)) { + srcCellExtent = LEGACY_TILE_PIXEL_EXTENT/2; + } + Int scale = TILE_PIXEL_EXTENT / srcCellExtent; if (tileWidth= (numRows*TILE_PIXEL_EXTENT)) continue; - int tileNdx = (column/TILE_PIXEL_EXTENT) + numRows*(row/TILE_PIXEL_EXTENT); - int pixelNdx = (column%TILE_PIXEL_EXTENT) + TILE_PIXEL_EXTENT*(row%TILE_PIXEL_EXTENT); - - UnsignedByte *pixel = tiles[tileNdx]->getDataPtr(); - - pixel += pixelNdx*TILE_BYTES_PER_PIXEL; - *pixel++ = b; - *pixel++ = g; - *pixel++ = r; - *pixel = a; - + if (column >= (numRows*srcCellExtent)) continue; + int tileNdx = (column/srcCellExtent) + numRows*(row/srcCellExtent); + int cellCol = column % srcCellExtent; + int cellRow = row % srcCellExtent; + + UnsignedByte *tileBase = tiles[tileNdx]->getDataPtr(); + // Nearest-neighbor scatter: replicate this one decoded source pixel into + // its scale x scale destination block. No row lookahead needed -- the + // whole block is written immediately from the single decoded pixel. + for (int dy=0; dy