Skip to content

Release 0.15.0: thin fork - #40

Merged
gnosticgeek merged 25 commits into
mainfrom
release-0.15.0
Nov 7, 2026
Merged

gnosticgeek merged 25 commits into
mainfrom
release-0.15.0

Conversation

@gnosticgeek

Copy link
Copy Markdown
Owner

Requested by Tom · project thread

The thin-fork release: NXForge becomes the Converter plus a Mac player layer on nxnative's engine (plan). The full list is in CHANGELOG.md under 0.15.0.

Changes

Testing

  • scripts/smoke_mac.sh on Tom's M4 Pro (release-0.15.0 at 82976d1, code identical to this head): host unit tests, every GPU graphics test and the Converter's unit tests passed ("All automatic checks passed", exit 0).
  • release_check.py check 0.15.0 passes.
  • Not done yet: a hands-on game run, and the fullscreen comparison between our no-display-link mode and upstream's (in BACKLOG). The first game build after this rebuilds its shaders once (new chain).

Generated by Claude Code

seedds and others added 24 commits November 7, 2026 19:59
…the zstd, dotnet, xcodegen or python3 it selects are the ones that run (PR seedds#1475)

TOOL_PATHS (/opt/homebrew/bin, /usr/local/bin, the .NET folders) went in front of the caller's PATH, so a venv's python3 or mise's zstd and dotnet lost to Homebrew's. tool_env() puts them after it: they still find what a GUI app such as Spyder lacks. An unset or empty PATH takes the system PATH (an empty entry would search the current folder).

Fixes seedds#1474.

Co-authored-by: qixi <72856252+smile-alive@users.noreply.github.com>
…nd every extra entry point calls main0's one body: Mario Kart 8 Deluxe's cold Mac shader build 22:46 to 14:47 and Super Mario Odyssey's 30:02 to 22:23 on an M2 (PR seedds#1520)

The readable-tile version <key>_bf was a source and a Metal compile of its own (13,666 of Mario Kart 8 Deluxe's 29,651 shaders), and main0_u / main0_m (fs_uint) and main0_d (fs_dual) were each a copy of main0's body, built again for each (4,990 of Pokémon Legends: Arceus's 6,160 pixel shaders have main0_u).

- dmsh_side.shared_body makes main0's body one always_inline dm_body_main0; main0, main0_u, main0_m and main0_d call it.
- dmsh_fetch.fold rewrites the samples in that body alone, with fx_i and fb0-fb7 as its parameters, and adds main0_f; every other caller passes -1 and zeros, which the inlined body folds back, so main0 declares no function constants. Rewriting a copy elsewhere would not build (no fb_k there).
- ps_libraries cuts <key>_b, <key>_bx and <key>_bf from the one compile (dmsh_side.cut with groups, compile_split with a list). A folded unit that does not build is compiled again without main0_f, so its alpha test and logic operation entry points stay; each step down prints a red line.
- The host's bf_fn specializes main0_f.
- metal and metallib are found once and run with -isysroot: the same .air and .metallib bytes as xcrun -sdk macosx. MAC_RECORD and the Metal cache format are unchanged, so unchanged MSL keeps its cached compiles.
- dmsh_side.py is in the shader cache key (CODE_FP); Metal cache prefill skips libraries cut with a _bf; the separate variant cache is gone.

Measured: Metal CPU on 60 of Odyssey's pixel shaders 0.53x (readable-tile) and 0.64x (integer only), main0's bitcode 5.7% smaller at the median. Partial cold builds of the same shaders on chain 3ee493b2c3a4 and this one: Arceus 239 in 24 to 18 s, Metroid Prime Remastered 182 in 26 to 19 s, every shader with the same libraries, entry points and reflections (MPR's ab809a7456f59614 has main0_d and main0_f). check_t_feedback, check_t_alpha, check_t_logic and the gfx run pass. No game run: a boot needs both chains' full libraries.

Also: test_shader_toolchain_cache lists partner_inputs, which vs_pad calls since main's vs_pad change.

Chain 1d619d0f520b.

Co-authored-by: Alexandre Melo <121455563+melocalex@users.noreply.github.com>
…holds it: the texture cpu_unc_drop hands to a package's serials stays resident while any of them draws with it (Ocarina of Time's GPU page faults 15-45 minutes into play, seedds#1326; PR seedds#1644)

At the GPU's first write over storage a packaged texture was taken from, every serial of the package, live or finalized in the ghost ring, gets the package's texture itself, and no table owns it. The package's release (NvPkg dealloc) took it out of the residency set at once, and so did any one holder's release (the ghost-ring eviction, serial_put, serial_del), while the others still drew with it.

- defer_drain asks whether a serial entry still holds a resource it retires (g_by_serial_held, and g_ghost_held, a new count for the ghost ring). One that does keeps its residency and waits in g_retire_held; the last entry's release retires it again (retire_resume), unless the sharing table owns it by then.
- The package's release retires its textures through defer_obj_release instead of res_remove at once. A re-upload does not write into a spare that serials still hold.
- alias_take_over no longer scans the ghost ring to choose unref over release: the same rule covers it.
- The memory report counts the waiting textures ("retired but held by a serial").

PR seedds#1644 found the cause and measured it (one binary: 107 minutes with no fault against a fault at 37 minutes; the residency diagnostic named 260x260 terrain tiles handed over this way). It kept handed-over textures in a table by storage key until their pool went; this covers every holder instead, with no key collision and nothing kept past the last holder.

Checked: t_cpu_tex --metal with the new handover_holders, 98 checks, 0 failed (3 failed on main's host code: the package's release alone took the texture out). The whole gfx test run, test_docs, test_lint_c and test_env_switches pass. Ocarina of Time v131072 on a 16 GB Mac, test hosts on chain 3ee493b2c3a4 (the newest libraries on disk) with DM_RES_CHECK=1, 230 s village walk each: both reach frame 9,000+, no residency or page-fault lines, the same two red lines; at most one texture waited at a time. That walk makes no hand-over, so it shows no regression, not the fault gone. regress.py check not run: main's chain 1d619d0f520b has no libraries for any game here.

Co-authored-by: bettterlabs <279109819+bettterlabs@users.noreply.github.com>
…aders: keys_hash asks mtl_addr_known, which makes no pool resident and no chunk (The Great Ace Attorney Chronicles: three chunks, about 5 MB; PR seedds#1484)

keys_hash (hle_nvn.c) checks that a shader's code lies in a memory pool before it
hashes it, and asked mtl_addr, which also puts the pool into the residency set and,
in a pool in chunks, makes the chunk that holds the code. The shaders run translated
and their c1 constants are copied into host buffers, so the GPU never reads that
code. It now asks mtl_addr_known, the side-effect-free lookup texture uploads already
used, made public; for a pool in chunks it now checks the whole range lies in the
pool. Keys are unchanged.

Measured with hosts built from main with and without the change:
- The Great Ace Attorney Chronicles (DM_CTL restop): shared buffers in the
  residency set 19 / 241 MB -> 16 / 236 MB at 30 s, 22 / 353 MB -> 19 / 348 MB at 60 s.
- Ocarina of Time: no change (4,276 resident pools before, 4,277 after on the
  village walk; 2,324 on both at the title). Its shaders read raw global memory,
  and the raw pool table (gmem_map) makes every pool it took an address in
  resident. The PR's 222 pools / 334 MB came from a build with other changes.

t_addr_known checks the lookup, ranges past a pool's end, that asking makes no pool
resident and, in a large pool, no chunk.

Co-authored-by: aksmith-git <242086076+aksmith-git@users.noreply.github.com>
…ntent metas, never from its file name: untagged updates pack however many there are, a wrong [v<N>] tag is said and ignored, and a base game served at 1.1.0 is its profile's v65536 (Castlevania: Belmont's Curse; issue seedds#1629)

scan_nsps took an update's version from the [v<N>] tag in its file name and a
package's title ID from its ticket or tag; it read the content metas only of a
package that held several. Untagged updates beside another update were left out,
a lone one was taken for the newest version the profile registers, and a wrong
tag was found only after the whole extraction, by the build IDs, and packed from
a derived profile. A base game was always v0: Castlevania's base NSZ tagged [v0]
is 1.1.0 (v65536) by its content meta, the only version its profile registers.

nx_extract --contents now takes every package and prints "package <index> <name>"
before each one's "content <type> <title ID> <version>" lines, a [RED] line for a
package it cannot read (exit 5), and stops on keys that cannot open the meta NCAs
(exit 6). The scan reads the whole folder in that one run, which reads headers
and meta NCAs only. nsp_versions names the base game by its own version. A
package that cannot be read or holds no base game, update or DLC is skipped in
red. title_of, nx_extract --title, is_merged, the PFS0 / HFS0 name readers and the
untagged-update guess are gone; DLC extraction always names the DLC's title ID.

Checked with the real nx_extract on renamed links to real packages: Balatro with
an untagged update (base v0, update v851968, packed from its profile's row); Mario
Kart 8 Deluxe with its update tagged [v65536] (taken as v1441792, said), an
untagged DLC (v65536) and a truncated package (skipped); NieR:Automata's XCI;
keys holding only header_key stop with the missing key_area_key named. No
Castlevania or NSZ file here: the base-at-v65536 case is a unit test.
test_nsp_scan.py covers the scan, the reader's command line and output, wrong
tags, untagged updates, the base version and skipped packages; the NSP suite
passes but for the existing ShaderWatch signature failure in test_nsp_mac_app.

Supersedes PR seedds#393's bare v<N> file-name parsing.

Fixes seedds#1629

Co-authored-by: CatPawPrint <28086202+CatPawPrint@users.noreply.github.com>
…tes the compressed texture: Monster Jam Steel Titans 2's arena floor is no longer black (PR seedds#1617)

Unreal's runtime virtual texture compresses its tiles in a compute shader into 16-byte RGBA32UI texels, one per BC3 block, and copies them with a texture copy into an
RGBA32UI texture made over the same storage as its BC3 tile cache, one texel per block. On the Switch the copy writes the cache's bytes. Here every NVN texture object has a
Metal texture of its own, so the bytes went to the alias and the BC3 cache stayed all zero.

nvn_mtl.m bc_alias_copy, called from copy_t2t before the destination's Metal texture is made: a plain copy into an uncompressed 2D texture of 8 or 16 bytes per texel, whose
storage is that of a registered, live block-compressed 2D texture of the same block size and whose size is that texture's size in 4x4 blocks, goes into the compressed texture
at four times the block position, through a private staging buffer kept resident and reused (a blit does not copy between an uncompressed and a compressed format; buffer
copies to and from both are allowed). The alias gets no Metal texture of its own (31 MB for each of the game's two caches), and the compressed texture's zero-source mark is
cleared. It uses the blit forms without options:, which deferred encoding records. DM_NVN_BC_ALIAS=0 turns it off for an A/B test. Open, in the reference entry: draws and
image stores into such an alias object, and a readback into CPU-visible storage.

Checked: dm/gfx/tests/t_bc_alias.m (new, Metal validation): 19 checks pass, 6 fail with DM_NVN_BC_ALIAS=0; the host builds and its tests pass; test_docs passes. The PR
author's run of Monster Jam Steel Titans 2 1.0.2 (not here: no profile or game files): on the Mac the tile caches went from all zero to about 530,000 of 1.92 million blocks
with colour and the floor shows its dirt, tracks and ramps, with no new red line; on an iPhone the copy runs at the arena with no stop at 30 fps.
regress.py check --quick on NieR:Automata (BC textures and texture copies): PASS, frames 6967 → 7018, no new
red line, screenshot within its usual noise; a first candidate run stopped at frame 2094 with no exit or crash line, and its rerun passed.

Co-authored-by: 528z4b2kgc-ux <352909663+528z4b2kgc-ux@users.noreply.github.com>
…ader files: 9c2fb0c made it read dmsh_build.py's progress file and left test_shader_watch_counts_the_list failing on the old signature; a partial build's total is the keys it is given, which test_partial_library checks
… encrypted, with the key from main: Little Nightmares II's shaders are found (PR seedds#1646)

Little Nightmares II keeps its shaders inline in the cooked packages of
Helios-Switch.pak, a version 8 pak whose index and entries are
AES-encrypted. ue_legacy_jobs opened legacy paks without a key, so the
split said the pak cannot be read and found no shaders.

uepak.Keys is now the one key finder for a game's paks: no key for a
pak whose index is in the clear, else the key find_key finds in the
first main (base game, then update) that opens it; each main is loaded
once, and a key found is tried first on the next pak. ue_legacy_jobs
passes the key with each job, so ue_legacy_shaders decrypts the entries
too; uepak.job (the shader code archives) and uepak.py list use it in
place of their own lookups. find_key tries the known keys before
code_keys, which reads all of main's code.

Little Nightmares II (1.4): 20,170 shaders from 57 files; it reaches the
title screen and plays on the Mac. OCTOPATH TRAVELER 0's uepak.job
output is byte-identical to before.

Co-authored-by: Haseenesss <18624035+Haseenesss@users.noreply.github.com>
…undle ID are settled: its title ID, name, version, bundle ID, the app it will write and the game's icon, before the shader stages (issue seedds#1627)

A front end showing the build could name the game only from the "app name: ..." say text, which
is for people, until the app existed. The event goes out once, from builds that make an app (an
IPA, an Xcode project with out null, a Mac app), not from the programs a multi-program game's
first build packs, nor from --platform mac. The icon is picked before the event, the same one the
app is given; docs/frontends.md says to copy it before the build ends, as an IPA build without
--keep-extracted deletes the extraction it is in.

Front-ends: new: --progress json sends a game event (title_id, name, version, bundle_id, out, icon) before the shader stages
Fixes seedds#1627
…h has a space: dm/gfx/tests quotes the three $(CURDIR) paths it hands to the Python tests, and sanitizers.mk names its folder relative to the including Makefile's (ASAN_FLAGS travels inside quotes, so it cannot quote an absolute path)

check_t_border, check_t_transform_feedback and check_t_pso_archive split the path at the space (Error 127, or unittest taking the second half as a test name); make asan and tsan-threads failed the same way in san_fresh's cat of ignorelist.txt.

Fixes seedds#1341
…led slot does: Mario Kart World's mip passes write one-layer 2D-array views through slots declared 2D, and Metal dropped the stores (the blob shadows' zero mips drew a dark box around each kart's shadow)

img_fit makes the image view with the shader's pixel class and declared type together, the layers from fit_layers (now shared with tex_fit), and its cache is keyed by type. A type no view bridges is bound as is and logged red once. t_image_type stores through a 2D slot into one layer and level of a 2D array.

Fixes seedds#1648
…sks for more players than it sees gets a prompt over the game that holds its thread until enough controllers have joined (a first button with Press a button to join, a connection without) or the player closes it, as the Switch's player screen; without a window or with DM_KEYS / DM_CTL it is canceled at once as before

Mario Kart World 1.8.0 closed its player screen when the second controller had not pressed yet (pull request seedds#1596). The rules are in dm/host/ctrl_prompt.h (t_ctrl_prompt): B or Escape cancels; with the minimum in, A or Return, the game's maximum, or no connected controller left to join finishes it; the press that makes a controller join counts for neither; no time limit and no close on a focus loss. The game gets no input while it is up, nor until the A or B that closed it is let go. hle_hid.c ctrl_support calls the new front_ctrl_support (front.h) and counts the players again after it (dm_players_reported). The Mac draws it on the Home prompt's card (hq_card_view), iOS as a card with Done and Cancel buttons (DmGamepad.m). The docs no longer ask players to press on every controller first.

Front-ends: a front end may pass the controller prompt's texts in the player's language as DM_CTRL_PROMPT_TEXT, one per line: title, hint, count, done, cancel, with {min}, {max} and {joined} replaced by the numbers (docs/frontends.md#starting-a-game)

Fixes seedds#1640
…ignal that completed it, as Horizon's KSynchronizationObject::Wait: a signal followed by a clear before the waiter resumed timed out the wait, a closed and reused handle redirected it, and a wait on a thread whose handle was closed never completed; every handle is validated up front (more than 64 is out of range, the pseudo-handle and an event's write end are invalid), a ready object and a zero-time poll precede a pending cancellation, the retained list lives in the thread's record, and ResetSignal on a thread is an invalid handle (PR seedds#1284)

Co-authored-by: Michael Straßburger <1259904+rastapasta@users.noreply.github.com>
… on the Mac: Unreal's fixed frame rate is written at fps_target, found by its reflection entry in any build; encrypted Unreal paks and IoStore containers with the shaders inline are read with main's key; every call of FMallocBinned3's constructor is cut

- uepak.py: a pak key opens any short path mount point (the empty paks beside the containers mount at "/"); an encrypted pak index is read with main's key (unreal_paks mains=), and a pak that names no file holds neither kind; Toc decrypts an encrypted directory index, and container labels are joined to its mount point
- shader_split.py: encrypted containers are read with the key of the pak beside them; Oodle shader tables also take LSLG records (Unreal 5.1 for the Switch, SP_GLSL_SWITCH)
- ue_malloc.py / patch_nso.py: every bl to the constructor is cut and gated (Sparking! ZERO inlines one lazy construction twice); a b to it still stops the chain
- ue_fixed_rate.py (new) / hle_fpslock.c: UEngine::FixedFrameRate from its reflection entry (UE 4.22-5.0 and 5.1+ layouts) and GEngine from the code that reaches it; the host writes fps_target there, following the 120 / 60 hold. Sparking! ZERO's config fixes its battles at 30 (bUseFixedFrameRate=True), which rhi.SyncInterval alone did not raise
- The profile dm/games/010035F022078000 (fps_target 60, fps_max 120), its note, the reference entries (shaders.md, memory.md, frame-rate.md), the games table, the gallery

Tests: test_uepak_key, test_uepak_iostore, test_ue_malloc, test_ue_fixed_rate, t_fpslock 22-23; lint baseline unchanged. Played on the Mac through the tutorial battle; speed judged right at 60 and 120 in a window. No regress run: offline, the changed rules give the same results on every Unreal main and RomFS on the drive except this game's
…ogram finds them: Ori and the Will of the Wisps draws its loading screen instead of grey noise (PR seedds#1521)

Ori and the Will of the Wisps (01008DD013200000, Unity 2018.4.24f1, 1.2.1) showed the same picture of grey
noise every frame, with about 135 "[RED] shader ... is not in the shader map" keys a boot, nearly all
different from run to run, all single-stage programs. nvnProgramSetShaders computed the stage keys while the
control sections were alive but kept them by the NVNprogram's address; Unity copies an NVNprogram after
SetShaders and draws with the copy, which had no record at its address, so its stages were keyed at the first
draw from control sections Unity had freed. DM_NVN_SHDUMP hid it, as it points every stage at a host copy.

hle_nvn.c: a key record belongs to prog_t.serial, which SetShaders sets and a copy carries, the identity the
Metal side already finds a program's translation by. One record per SetShaders, kept for the process (64
bytes): a copy can outlive the program it was made from. A stage keyed after SetShaders (its code in no
memory pool yet) is keyed from a host copy of its control section's key bytes taken in SetShaders, as NVN
reads the section there; DM_NVN_SHDUMP uses the same copy instead of a separate 0x880-byte allocation.
nvn_replay.m installs captured keys by serial.

The PR's three boots before and after on Ori (M4 Pro): 130-136 missing keys a boot and noise, then 1 and the
loading screen drawn. Checked here: dm/gfx/tests (38 checks, 0 failed), dm/host/tests, test_docs.py. No
regression run (skipped).

Co-authored-by: Georgi Shteryanov <98543201+gshteryanov@users.noreply.github.com>
… Time's foliage no longer flashes white for single frames (chain 3ee52af3ca18, PR seedds#1673)

The game draws its foliage twice with two different mesh shaders: a depth pre-pass (depth LESS, no colour) and a colour
pass with depth EQUAL and no depth write. A vertex shader's position is [[position, invariant]], compiled with
-fpreserve-invariance, but the mesh stage wrapper (dmsh_meshstage.to_stage) declared a plain [[position]], so Metal could
compile the two mesh shaders' position math to slightly different depths; pixels where the colour pass missed EQUAL
showed white for one frame. The wrapper now declares [[position, invariant]].

Checked: test_dmsh_meshstage.py (the new test fails without the mark and needs no Metal compiler; the compile test keeps
its skip), test_docs.py, test_nsp_chain.py. Ocarina of Time 1.1.1 on the Mac: no flashes in five 121-frame windows on an
M4 Max (14 on main); an overlay with the mark showed none in 240 frames at a flashing flower on an M4 Pro.

Co-authored-by: cocoman93 <353537723+cocoman93@users.noreply.github.com>
…erty through a global: a load alone named another object on Pikmin 4 (a gravity of -980 at UEngine::FixedFrameRate's offset), which the host's range check refused, so a game whose such object held a value in range could have had it overwritten; the NSP script's rewrite key covers fps_target.py and ue_fixed_rate.py

- ue_fixed_rate.py: str sT, [xB, #field] through a global only (Sparking! ZERO's Switch frame pace, main+0x1e1ea04); a main with loads alone gets no fixups. Sparking! ZERO's GEngine and field are unchanged; Pikmin 4 and Princess Peach: Showtime! get none
- build_ipa_from_nsp.py: the rewrite is redone when fps_target.py or ue_fixed_rate.py changes, as when the rewriter's other files do
- Pikmin 4's note: its 60 fps is open (the property at UEngine+0x7a8, no store through a global; the user saw 30 fps in a window with fps_target 60); frame-rate.md records the load-only case

Tests: test_ue_fixed_rate (a load alone is no proof); docs tests and the lint baseline unchanged
…d onto the guest clock, not from a later guest clock read: a delay between the sample and that read no longer enters the timestamp (PR seedds#1676)

rep_ticks took a boundary's age on the GPU clock back from guest_ns() read after sampleTimestamps, and gpu_s_guest_ns
read the guest clock and the host clock separately, so a preemption between the reads moved the time reported to the
game. guest_ns_at_host_time (hle_os.c) projects a host time onto the guest clock (pause offset, frozen clock and
deterministic clock respected); both conversions now use one host sample. On an M4 Pro the CPU sample is in ns on the
mach_absolute_time base, as now_ns, and equals the GPU sample. The effect in play is small: the old read came one line
after the sample (1-6 us measured here), so only a preemption in that window mattered.

Checked: t_counter_clock (new, in CI; 11 checks, 4 fail on the old conversion), t_timestamp, t_readback, host build,
test_docs.py.

Co-authored-by: nanotchec <145017345+nanotchec@users.noreply.github.com>
…me rate takes GEngine from where FEngineLoop::Init makes the engine, in any Unreal 4.22 to 5 build, and the host writes it only while the engine's bUseFixedFrameRate is set

- ue_fixed_rate.py: GEngine is the global the engine is stored into after a reference to the UTF-16 config key "GameEngine" (alone or the tail of "UGameEngine"), loaded back into x0 and followed by a virtual call; xR resolved back through adrp, adrp + add or a GOT slot, and every such store must name one global. bUseFixedFrameRate's byte and bit come from its reflection entry's set-bit function (UE_FIXED_RATE_FLAG). The previous version needed a store of the property through a global, which only Sparking! ZERO's Switch frame pace makes; Pikmin 4 sets it from its config and reads it through this
- hle_fpslock.c / loader.c / dm.h: the fixed frame rate row is written only while the flag is set on the object (an engine stepping by the time that passed, Princess Peach: Showtime!, is left as it is)
- Pikmin 4: fps_target 60, fps_max 120 (GEngine main+0x9fbf880, the property at +0x7a8, the flag bit 0x40 of +0x7a4, read in a run: 0x47 and 30.0); its note, the reference entry, the gallery caption
- Sparking! ZERO: the same GEngine (main+0x9aefa30) and field, now from its engine creation, with the flag at +0x9c4; checked at 120 in a window (119.2-119.4 fps, the write 30 → 120)

Tests: test_ue_fixed_rate (both layouts, page and GOT forms, a store with no reload, two globals), t_fpslock 22-24 (the flag off); docs tests and the lint baseline unchanged. The user played Pikmin 4's prologue at 120 and found it right
19 upstream commits: Pikmin 4 and DRAGON BALL Sparking! ZERO at 60 fps,
the controller applet's player prompt, WaitSynchronization's held objects,
content-meta title IDs for every package, and the landed versions of
seedds#1520 (shared pixel-shader body) and seedds#1484 (mtl_addr_known).

Resolution rule: where upstream landed its own version of a pull request
NXForge carried early, upstream's wins and ours goes (mtl_pool_holds and
t_pool_holds, seedds#1520's dmsh_build/fetch/side, the IoStore key reader).
NXForge's own work stays: posix_spawn launches and the shared Metal cache's
prune and use marks (on upstream's compile_split), seedds#1466's g_unlock (now
with sync_close and R_CANCELLED), keyboard remapping (beside the controller
prompt), the sRGB image view, defer_obj_at and the multi-application
cartridge pick (on upstream's content-meta scan).

NXForge's own red lines get their own table (NXF_RED_ONCE), so upstream's
RED_ONCE keys keep upstream's numbers (156, 157; g_red_once[158]) and no
longer move at each merge. Chain 4f4abb1e5355.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK4U7UAc5p3k4mJJhPJ
….md describe NXForge as the Converter plus a Mac player on nxnative

The Quick settings glue, controller sidebar navigation and control
remapping move out of front_mac.m into dm/host/nxforge/*.inc, and the
present filters out of nvn_mtl.m into dm/gfx/nvn/nxforge/present_filters.inc.
Each is included at the one line where it stood, so it keeps that file's
statics and compiles as before; an nxnative merge now meets one #include
line there instead of the whole block (front_mac.m: 468 lines against
nxnative's, now 159). The Makefiles track the new files.

UPSTREAM.md gets the thin-fork rules (merge main whole, early upstream PRs
only for a game being played, fixes headed upstream kept until upstream
lands them, player code in nxforge/ files, NXF_RED_ONCE).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK4U7UAc5p3k4mJJhPJ
…nd the prepared upstream PRs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK4U7UAc5p3k4mJJhPJ
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK4U7UAc5p3k4mJJhPJ
@github-actions github-actions Bot added host The Horizon host: SVCs, services, threads, memory (dm/host) nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) rewriter The NSO rewriter and per-build patches packing Building an IPA or a Mac app: the NSP script, extraction, the shader split, the packers ios The iPhone and iPad app (dm/ios); on an issue, seen on one mac The Mac front end and app (dm/mac, front_mac.m); on an issue, seen on a Mac labels Nov 7, 2026
@github-actions github-actions Bot added tools Tools and scripts (dm/tools, scripts) docs Documentation ci Continuous integration and repository settings (.github) shader chain The shader translation chain (DMSH_CHAIN): a change rebuilds every game's shaders game: 0100000000010000 Super Mario Odyssey game: 0100152000022000 Mario Kart 8 Deluxe game: 010035F022078000 A game profile new game A game main has no profile for yet: a pull request adding one, or an issue about one game: 010036E00FB20000 The Great Ace Attorney Chronicles game: 0100B7C00933A000 Pikmin 4 game: 0100BA4026E94000 Castlevania: Belmont's Curse game: 0400486024DEC000 The Legend of Zelda: Ocarina of Time game: 0400C3F00006E000 Mario Kart World labels Nov 7, 2026
…e.CallAndMessage)

The analyzer takes a prog_key path that leaves md unwritten and then
formats it. Same value whenever prog_key writes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK4U7UAc5p3k4mJJhPJ
@gnosticgeek
gnosticgeek merged commit 9dfdc39 into main Nov 7, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci Continuous integration and repository settings (.github) docs Documentation game: 0100000000010000 Super Mario Odyssey game: 0100152000022000 Mario Kart 8 Deluxe game: 0100B7C00933A000 Pikmin 4 game: 0100BA4026E94000 Castlevania: Belmont's Curse game: 0400C3F00006E000 Mario Kart World game: 010035F022078000 A game profile game: 010036E00FB20000 The Great Ace Attorney Chronicles game: 0400486024DEC000 The Legend of Zelda: Ocarina of Time host The Horizon host: SVCs, services, threads, memory (dm/host) ios The iPhone and iPad app (dm/ios); on an issue, seen on one mac The Mac front end and app (dm/mac, front_mac.m); on an issue, seen on a Mac new game A game main has no profile for yet: a pull request adding one, or an issue about one nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) packing Building an IPA or a Mac app: the NSP script, extraction, the shader split, the packers rewriter The NSO rewriter and per-build patches shader chain The shader translation chain (DMSH_CHAIN): a change rebuilds every game's shaders tools Tools and scripts (dm/tools, scripts)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants