mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Fix padded Xbox texture rows and BC2/BC3 colour decoding
This commit is contained in:
1 parent
7d44d84f19
commit
ad992f2001
4 files changed
+94
-4
No files matched your search
@@ -187,3 +187,42 @@ for the user. The normal launcher clears SKATE_DEBUG_FOLIAGE.
|
||||
For integration with staged map switching, prepare this supplement alongside the
|
||||
district, before applying sky lighting. Track its entities and assets in the
|
||||
same map lifetime. The current adapter is in `retail_backdrop.rs`.
|
||||
|
||||
### Black texture/lightmap bands (2026-09-08)
|
||||
|
||||
The University bark rings and mega-park shark logo expose an exporter defect in
|
||||
the shared Xbox texture decoder. `XGAddress2DTiledX/Y` returns coordinates in the
|
||||
padded physical tile. `_untile360` flattened `y * logical_width + x` before
|
||||
rejecting columns outside the logical width. Padding columns consequently aliased
|
||||
onto later valid rows, overwriting colour blocks with padding or other tile data.
|
||||
The NumPy implementation reproduced the scalar bug, including last-write wins.
|
||||
Both implementations now reject `x >= logical_width` before writing.
|
||||
|
||||
The logo's diffuse is `0x2c70170a001d01bb`; its 64x64 DXT1 lightmap is
|
||||
`0x8b068e5c4f70dd7a`. Re-decoding that lightmap removes the black stripes while
|
||||
the logo diffuse remains unchanged. Nine lightmaps bound to the spring/fall-tree
|
||||
bark diffuse textures `0x00007e2903e3870a` and `0x00007dc403e3870a` have the same
|
||||
corruption. The shared decoder affects colour textures as well: the 64x128 palm
|
||||
fringe texture `0x00007e3103e3870a` also loses artificial horizontal bands.
|
||||
This establishes an asset-decoding cause independently of shader lighting.
|
||||
|
||||
A separate colour-palette error was corrected in both decoders: DXT3/DXT5 colour
|
||||
blocks must retain four interpolated colours even when their endpoints are in
|
||||
ascending order. The old code reused DXT1's three-colour/black branch and merely
|
||||
made that black opaque. DXT1's transparent palette remains intact. See Microsoft's
|
||||
[block compression reference](https://learn.microsoft.com/en-us/windows/win32/direct3d10/d3d10-graphics-programming-guide-resources-block-compression)
|
||||
for the format layouts and separate BC2/BC3 alpha blocks.
|
||||
|
||||
The private University repair re-decodes the original RX2 sources and changes
|
||||
723 of 2,046 texture payloads: 720 DXT1 (logical widths 16/32/64), two DXT5 and
|
||||
one A8R8G8B8. It preserves the exporter's dedicated B5G6R5 decoder output. Every
|
||||
original payload was checked against its source cache before replacement;
|
||||
material definitions and the entire geometry/collision/metadata tail remain
|
||||
byte-identical. The repaired package passes the offline SKATE reader.
|
||||
|
||||
Regression tests cover narrow/wide tile layouts at 2/4/8/16 bytes per unit,
|
||||
DXT1 transparency, and DXT3/DXT5 reversed colour endpoints with independent alpha,
|
||||
using both scalar and NumPy paths. All 16 asset-pipeline tests pass. The game was
|
||||
not launched; visual confirmation remains user-run. Existing exported maps need
|
||||
their textures regenerated from RX2, since changing the decoder cannot repair
|
||||
already-baked PNG/RGBA cache files. Fresh conversions use the fix across maps.
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Regression fixtures for padded Xbox tiles and BC colour palettes."""
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
UTT = Path(__file__).resolve().parents[1] / 'vendor/utt'
|
||||
sys.path.insert(0, str(UTT))
|
||||
import rx2_fast
|
||||
|
||||
# Exercise the standalone fallback as well as the NumPy implementation.
|
||||
spec = importlib.util.spec_from_file_location('rx2_scalar_test', UTT / 'rx2_parser.py')
|
||||
scalar = importlib.util.module_from_spec(spec)
|
||||
with patch.dict(sys.modules, {'rx2_fast': None}):
|
||||
spec.loader.exec_module(scalar)
|
||||
|
||||
|
||||
class TextureDecodeTests(unittest.TestCase):
|
||||
def test_padding_cannot_overwrite_visible_rows(self):
|
||||
for width, pitch in ((w, p) for w in (4, 8, 16, 32) for p in (2, 4, 8, 16)):
|
||||
with self.subTest(width=width, pitch=pitch):
|
||||
units = max(32*32, 4096//pitch)
|
||||
raw = bytearray(units*pitch)
|
||||
for offset in range(units):
|
||||
x = scalar._x360_tiled_x(offset, width, pitch)
|
||||
y = scalar._x360_tiled_y(offset, width, pitch)
|
||||
if x < width and y < 32:
|
||||
raw[offset*pitch:offset*pitch+pitch] = bytes([1+y])*pitch
|
||||
expected = b''.join(bytes([1+y])*(width*pitch) for y in range(32))
|
||||
for decoder in (scalar, rx2_fast):
|
||||
self.assertEqual(decoder._untile360(raw, width, pitch)[:len(expected)], expected)
|
||||
|
||||
def test_bc1_keeps_transparent_palette(self):
|
||||
colour = struct.pack('>HH', 0x001f, 0xf800) + b'\xff'*4
|
||||
self.assertEqual(scalar._dxt1_block(colour, True), bytes(64))
|
||||
self.assertEqual(rx2_fast._decode(colour, 4, 4, 1, False), bytes(64))
|
||||
|
||||
def test_bc2_bc3_use_four_colours_with_reversed_endpoints(self):
|
||||
colour = struct.pack('>HH', 0x001f, 0xf800) + b'\xff'*4
|
||||
for kind, alpha, block_decoder in ((3, b'\xaa'*8, scalar._dxt3_block),
|
||||
(5, b'\x00\xaa'+bytes(6), scalar._dxt5_block)):
|
||||
expected = bytes((170, 0, 85, 170))*16
|
||||
self.assertEqual(block_decoder(alpha+colour), expected)
|
||||
self.assertEqual(rx2_fast._decode(alpha+colour, 4, 4, kind, False), expected)
|
||||
Vendored
+6
-2
@@ -17,7 +17,9 @@ def _layout(units,width,pitch):
|
||||
y=(((m//(aligned>>5))<<2)+((t>>(6+log))&1)+((b&2048)>>10))<<3
|
||||
y+=(((t&(((pitch<<6)-1)&~31))+((t&15)<<1))>>(3+log))&~1
|
||||
y+=(t&16)>>4
|
||||
dest=y*width+x;valid=dest<units
|
||||
# Padded tile columns are outside the logical image. Flattening them
|
||||
# first aliases them onto later rows and overwrites valid blocks.
|
||||
dest=y*width+x;valid=(x<width)&(dest<units)
|
||||
# Match scalar last-write ordering even for narrow padded surfaces.
|
||||
source=np.full(units,-1,dtype=np.int64)
|
||||
np.maximum.at(source,dest[valid],offset[valid])
|
||||
@@ -39,7 +41,9 @@ def _blocks(data,w,h,size):
|
||||
def _colour(b,transparent):
|
||||
c=b[:,:4].astype(np.uint32);c0=c[:,0]*256+c[:,1];c1=c[:,2]*256+c[:,3]
|
||||
def rgb(v):return np.stack((((v>>11)&31)*255//31,((v>>5)&63)*255//63,(v&31)*255//31),axis=1)
|
||||
a=rgb(c0);z=rgb(c1);four=c0>c1
|
||||
# BC2/BC3 always use four colours. Only BC1 uses endpoint ordering
|
||||
# to select the three-colour + transparent-black palette.
|
||||
a=rgb(c0);z=rgb(c1);four=(c0>c1) | (not transparent)
|
||||
pal=np.zeros((len(b),4,4),dtype=np.uint8);pal[:,:,3]=255
|
||||
pal[:,0,:3]=a;pal[:,1,:3]=z
|
||||
pal[:,2,:3]=np.where(four[:,None],(2*a+z+1)//3,(a+z)//2)
|
||||
|
||||
Vendored
+3
-2
@@ -426,7 +426,7 @@ def _untile360(src, width_units, texel_pitch):
|
||||
y = _x360_tiled_y(j * width_units + i, width_units, texel_pitch)
|
||||
dst_idx = (y * width_units + x) * texel_pitch
|
||||
src_idx = (j * width_units + i) * texel_pitch
|
||||
if dst_idx + texel_pitch <= len(dst) and src_idx + texel_pitch <= len(src):
|
||||
if x < width_units and dst_idx + texel_pitch <= len(dst) and src_idx + texel_pitch <= len(src):
|
||||
dst[dst_idx:dst_idx + texel_pitch] = src[src_idx:src_idx + texel_pitch]
|
||||
return bytes(dst)
|
||||
|
||||
@@ -473,7 +473,8 @@ def _dxt1_block(blk, transparent):
|
||||
c0, c1 = struct.unpack_from(">HH", blk, 0)
|
||||
r0, g0, b0 = _rgb565(c0)
|
||||
r1, g1, b1 = _rgb565(c1)
|
||||
if c0 > c1:
|
||||
# DXT3/DXT5 have independent alpha and always use four colour entries.
|
||||
if c0 > c1 or not transparent:
|
||||
c2, c3 = _rgb565_interp((c0 >> 11) & 0x1F, (c1 >> 11) & 0x1F,
|
||||
(c0 >> 5) & 0x3F, (c1 >> 5) & 0x3F,
|
||||
c0 & 0x1F, c1 & 0x1F)
|
||||
|
||||
Reference in new issue
Block a user