Skip to content

Sort flag sets so exported C is reproducible - #737

Merged
Dragorn421 merged 1 commit into
Fast-64:mainfrom
Taldariner:fix-geo-mode-flag-order
Sep 22, 2026
Merged

Dragorn421 merged 1 commit into
Fast-64:mainfrom
Taldariner:fix-geo-mode-flag-order

Conversation

@Taldariner

Copy link
Copy Markdown
Contributor

Problem

Exporting the same scene twice can produce different C files, with nothing changed in the blend file. Only the order of flags inside a command differs:

-	gsSPClearGeometryMode(G_SHADING_SMOOTH | G_LIGHTING),
+	gsSPClearGeometryMode(G_LIGHTING | G_SHADING_SMOOTH),

Reproduction, run it a few times (each run is a fresh process):

blender -b --python-expr "import addon_utils; addon_utils.enable('fast64'); from fast64.fast64_internal.f3d.f3d_gbi import SPClearGeometryMode; print(SPClearGeometryMode({'G_LIGHTING','G_SHADING_SMOOTH'}).to_c())"

On my machine 6 runs printed G_LIGHTING | G_SHADING_SMOOTH five times and G_SHADING_SMOOTH | G_LIGHTING once. Exporting a corpus of test meshes, 8 runs of the same export produced 2 distinct sets of files.

This matters when an export is diffed against what is already committed in a decomp repo: the diff shows changes that are not changes. It also makes it impossible to confirm that a change to the exporter left the output alone, which is what I ran into while working on f3d_writer.py.

Cause

GbiMacro.getattr_virtual joins iterable fields with " | " in iteration order, and several macros keep their flags in a set. Python randomizes string hashes per process, so the iteration order of a set of flag names changes from run to run.

Fix

Sort the values when the field is a set or frozenset. Lists keep their order, so nothing positional is affected.

This is safe for every field it touches, because they are flag unions that to_binary ORs into a single word:

Macro Field
SPGeometryMode clearFlagList, setFlagList
SPSetGeometryMode flagList
SPClearGeometryMode flagList
SPLoadGeometryMode flagList
DPSetOtherMode mode0, mode1

DPSetRenderMode has its own positional to_c and is not affected. Binary output never depended on the order.

Import is unaffected as well: f3d_parser splits flag lists on | and does not care about order.

Verification

A corpus of 22 generated meshes (flat and smooth, vertex colors, fog, UV seams, large texture mode, F3DEX3 packed normals and cel shading) exported to C for F3D, F3DEX2 and F3DEX3, plus a skinned geolayout export, on Blender 4.2.16 LTS:

  • before the change, 8 runs of the same export produced 2 distinct outputs; after it, 1;
  • comparing the two trees, 130 files are identical except for 8 reordered flag commands, and the sorted output matches one of the orders the current code already emits;
  • to_binary output is identical for both orders of the same flags, for every affected macro in F3D, F3DEX, F3DEX2 and F3DEX3.

Comment thread fast64_internal/f3d/f3d_gbi.py Outdated
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.
@Taldariner
Taldariner force-pushed the fix-geo-mode-flag-order branch from d2eabbf to 0fd28a3 Compare September 20, 2026 08:36
@Dragorn421 Dragorn421 added the merge soon Will be merged in a few days at most if nothing else comes up label Sep 20, 2026
@Dragorn421
Dragorn421 merged commit 1e405f9 into Fast-64:main Sep 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge soon Will be merged in a few days at most if nothing else comes up

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants