From 5b9ecdcff8aa57788ec6a6b53eeef0252ec4052f Mon Sep 17 00:00:00 2001 From: Taldariner <71024372+Taldariner@users.noreply.github.com> Date: Sun, 20 Sep 2026 12:01:11 +0300 Subject: [PATCH] fix(f3d): make F3DEX3 binary export write valid triangles and light counts Exporting a display list as binary or insertable binary with F3DEX3 selected fails before writing anything: ValueError: invalid literal for int() with base 10: 'NUMLIGHTS_1NUMLIGHTS_1...' and, past that, `PluginError: SP2Triangles not available in Fast3D.` Two separate causes, both specific to F3DEX3: `F3D.NUML` multiplied the macro name by 0x10 instead of the light count it looks up, because the F3DEX3 branch runs before the lookup. Three binary encoders test `F3DEX_GBI or F3DLP_GBI`, that is "is this F3DEX1", where what they mean is "is this any F3DEX". F3DEX3 deliberately leaves `F3DEX_GBI` false (see the comment in `F3D.__init__`: F3DEX3 is F3DEX2, but F3DEX3 is not F3DEX1), so it fell through to the original F3D encodings: * `_gsSP1Triangle_w1f` scaled vertex indices by 10 instead of 2 and wrote flag as a separate byte. In F3DEX2 and later that byte lands on the command byte, so a triangle with flag 2 turned G_TRI1 (0x05) into 0x07. * `SP2Triangles` raised outright, which is why no F3DEX3 binary export completed. * `SPCullDisplayList` used the F3D vertex scaling. Verification, Blender 4.2.16 LTS: * encoding SP1Triangle and SP2Triangles over 34 index/flag combinations for F3D, F3DEX/LX, F3DLX.Rej, F3DEX2/LX2 and F3DEX3, then decoding the words back: every case now round-trips its indices, its flag and its command byte. Of 62 encodings, the 14 that change are all F3DEX3; F3D, F3DEX1 and F3DEX2 are byte identical. * insertable binary export of a test mesh: F3DEX3 went from raising to producing a display list whose 8 two-triangle commands index inside the 56 vertex buffer. F3D and F3DEX2 exports are byte identical before and after. * C export of a 22 mesh corpus for F3D, F3DEX2 and F3DEX3 is unchanged. --- fast64_internal/f3d/f3d_gbi.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fast64_internal/f3d/f3d_gbi.py b/fast64_internal/f3d/f3d_gbi.py index 86201a54d..5275ca486 100644 --- a/fast64_internal/f3d/f3d_gbi.py +++ b/fast64_internal/f3d/f3d_gbi.py @@ -1757,9 +1757,9 @@ def CALC_DXT_4b(self, width): return int(((1 << self.G_TX_DXT_FRAC) + self.TXL2WORDS_4b(width) - 1) / self.TXL2WORDS_4b(width)) def NUML(self, n): - if self.F3DEX_GBI_3: - return n * 0x10 nVal = self.numLights[n] + if self.F3DEX_GBI_3: + return nVal * 0x10 return ((nVal) * 24) if self.F3DEX_GBI_2 else (((nVal) + 1) * 32 + 0x80000000) def getLightMWO_a(self, n: str): @@ -3629,7 +3629,7 @@ def _gsSP1Triangle_w1(v0, v1, v2): def _gsSP1Triangle_w1f(v0, v1, v2, flag, f3d): - if f3d.F3DLP_GBI or f3d.F3DEX_GBI: + if not f3d.F3D_OLD_GBI: if flag == 0: return _gsSP1Triangle_w1(v0, v1, v2) elif flag == 1: @@ -3740,7 +3740,7 @@ class SP2Triangles(GbiMacro): flag1: int def to_binary(self, f3d, segments): - if f3d.F3DLP_GBI or f3d.F3DEX_GBI: + if not f3d.F3D_OLD_GBI: words = ( _SHIFTL(f3d.G_TRI2, 24, 8) | _gsSP1Triangle_w1f(self.v00, self.v01, self.v02, self.flag0, f3d) ), _gsSP1Triangle_w1f(self.v10, self.v11, self.v12, self.flag1, f3d) @@ -3760,7 +3760,7 @@ class SPCullDisplayList(GbiMacro): vend: int def to_binary(self, f3d, segments): - if f3d.F3DLP_GBI or f3d.F3DEX_GBI: + if not f3d.F3D_OLD_GBI: words = _SHIFTL(f3d.G_CULLDL, 24, 8) | _SHIFTL((self.vstart) * 2, 0, 16), _SHIFTL((self.vend) * 2, 0, 16) else: words = _SHIFTL(f3d.G_CULLDL, 24, 8) | ((0x0F & (self.vstart)) * 40), ((0x0F & ((self.vend) + 1)) * 40)