From 0fd28a3f5eb6f7ea59623c52a7de6ea523cb124d Mon Sep 17 00:00:00 2001 From: Taldariner <71024372+Taldariner@users.noreply.github.com> Date: Sat, 19 Sep 2026 16:29:14 +0300 Subject: [PATCH] fix(f3d): sort flag sets so exported C is reproducible GbiMacro.to_c joined set fields in iteration order. Python randomizes string hashes per process, so exporting the same scene twice could emit gsSPClearGeometryMode(G_SHADING_SMOOTH | G_LIGHTING), in one run and gsSPClearGeometryMode(G_LIGHTING | G_SHADING_SMOOTH), in the next, with no change anywhere in the blend file. Diffing an export against what is already committed in a decomp repo then shows noise, and there is no way to confirm that a change to the exporter left the output alone. Sorting is safe for these fields: they are flag unions, ORed into one word by to_binary, so order carries no meaning. Affects SPGeometryMode, SPSetGeometryMode, SPClearGeometryMode, SPLoadGeometryMode and DPSetOtherMode. DPSetRenderMode keeps its own to_c, which is positional, and is untouched. Binary output never depended on the order. Checked on a generated corpus of 22 meshes exported to C for F3D, F3DEX2 and F3DEX3, plus a skinned geolayout export: before the change 8 runs of the same export produced 2 distinct outputs; after it, 8 runs produce 1. Comparing the two trees, 130 files are identical except for 8 reordered flag commands, and to_binary output is identical for both orders of the same flags in every affected macro and microcode. --- fast64_internal/f3d/f3d_gbi.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fast64_internal/f3d/f3d_gbi.py b/fast64_internal/f3d/f3d_gbi.py index 86201a54d..7dc02703c 100644 --- a/fast64_internal/f3d/f3d_gbi.py +++ b/fast64_internal/f3d/f3d_gbi.py @@ -3445,7 +3445,12 @@ def getattr_virtual(self, field, static): else: return field.name if hasattr(field, "__iter__") and type(field) is not str: - return " | ".join(map(str, field)) if len(field) else "0" + if not len(field): + return "0" + values = [str(value) for value in field] + if isinstance(field, (set, frozenset)): + values.sort() # sort for reproducible exports + return " | ".join(values) if self._hex > 0 and isinstance(field, int): temp = field if field >= 0 else (1 << (self._hex * 4)) + field return f"{temp:#0{self._hex + 2}x}" # + 2 for the 0x part