mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-27 19:54:40 +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
|
For integration with staged map switching, prepare this supplement alongside the
|
||||||
district, before applying sky lighting. Track its entities and assets in the
|
district, before applying sky lighting. Track its entities and assets in the
|
||||||
same map lifetime. The current adapter is in `retail_backdrop.rs`.
|
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=(((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&(((pitch<<6)-1)&~31))+((t&15)<<1))>>(3+log))&~1
|
||||||
y+=(t&16)>>4
|
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.
|
# Match scalar last-write ordering even for narrow padded surfaces.
|
||||||
source=np.full(units,-1,dtype=np.int64)
|
source=np.full(units,-1,dtype=np.int64)
|
||||||
np.maximum.at(source,dest[valid],offset[valid])
|
np.maximum.at(source,dest[valid],offset[valid])
|
||||||
@@ -39,7 +41,9 @@ def _blocks(data,w,h,size):
|
|||||||
def _colour(b,transparent):
|
def _colour(b,transparent):
|
||||||
c=b[:,:4].astype(np.uint32);c0=c[:,0]*256+c[:,1];c1=c[:,2]*256+c[:,3]
|
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)
|
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=np.zeros((len(b),4,4),dtype=np.uint8);pal[:,:,3]=255
|
||||||
pal[:,0,:3]=a;pal[:,1,:3]=z
|
pal[:,0,:3]=a;pal[:,1,:3]=z
|
||||||
pal[:,2,:3]=np.where(four[:,None],(2*a+z+1)//3,(a+z)//2)
|
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)
|
y = _x360_tiled_y(j * width_units + i, width_units, texel_pitch)
|
||||||
dst_idx = (y * width_units + x) * texel_pitch
|
dst_idx = (y * width_units + x) * texel_pitch
|
||||||
src_idx = (j * width_units + i) * 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]
|
dst[dst_idx:dst_idx + texel_pitch] = src[src_idx:src_idx + texel_pitch]
|
||||||
return bytes(dst)
|
return bytes(dst)
|
||||||
|
|
||||||
@@ -473,7 +473,8 @@ def _dxt1_block(blk, transparent):
|
|||||||
c0, c1 = struct.unpack_from(">HH", blk, 0)
|
c0, c1 = struct.unpack_from(">HH", blk, 0)
|
||||||
r0, g0, b0 = _rgb565(c0)
|
r0, g0, b0 = _rgb565(c0)
|
||||||
r1, g1, b1 = _rgb565(c1)
|
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,
|
c2, c3 = _rgb565_interp((c0 >> 11) & 0x1F, (c1 >> 11) & 0x1F,
|
||||||
(c0 >> 5) & 0x3F, (c1 >> 5) & 0x3F,
|
(c0 >> 5) & 0x3F, (c1 >> 5) & 0x3F,
|
||||||
c0 & 0x1F, c1 & 0x1F)
|
c0 & 0x1F, c1 & 0x1F)
|
||||||
|
|||||||
Reference in new issue
Block a user