Repository navigation
A storage takeover asks whether a live texture object holds each shared texture in one lookup instead of walking the serial table - #1439
Conversation
…ed texture in one lookup instead of walking the serial table: Ocarina of Time's cutscene texture creation 2.4 times cheaper For each sharing-table entry a takeover could remove, alias_take_over called serial_holds(&g_by_serial, ...), a walk over the serial table's whole capacity. With about 7,500 serials and thousands of shared textures, every new shared texture walked the table once per overlapping entry, and in Ocarina of Time's cutscenes, where the texture pool's walks make hundreds of textures in a submission, that walk was the submit thread's hottest function while frames took up to a second. A count per object of the serial-table entries holding it (g_by_serial_held) is now kept by every writer of the serial table (serial_put, serial_del, tex_put and the upgrade's in-place replacement), and alias_take_over asks it in one lookup. An entry holds its object, so a counted address is not reused while counted. t_alias_takeover: live-held entries stay, unheld ones go after ALIAS_IDLE submissions, two serials keep an object held until both go, and a takeover past 1,000 held entries with 20,000 serials takes 0.025 ms instead of 4.3 ms. Ocarina of Time (with PR seedds#1299), 60 s sample captures over Saria's cutscene: alias_take_over took 2,277 of 3,711 samples of texture creation (61 %), with the fix 38 of 618 (6 %). The reference entry records what is still open: the textures themselves are made again and again (84,000 to 103,000 storage takeovers in about 200 s of play). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…exture object holds each shared texture in one lookup instead of walking the serial table # Conflicts: # dm/gfx/nvn/nvn_mtl.m
…n and its resources before it uses them The test includes nvn_mtl.m, and with this branch's additions there the analyzer's path through the test's main() reached two calls it had not before: newComputePipelineStateWithFunction: with the function newFunctionWithName: may return nil, and copyFromTexture: with a texture newTextureWithDescriptor: may return nil (clang-analyzer-nullability.NullablePassedToNonnull, 1 in the file against a baseline of 0). The function is now checked before the pipeline is made, and the textures and buffers after they are made; the test fails with a message either way, as it already did for the library and the pipelines. lint_c.py tidy on the file: 0 findings (baseline 240, none above or below); the test passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… texture object holds each shared texture in one lookup (Ocarina of Time's cutscene texture creation 2.4 times cheaper) Resolved against NXForge's serial table (seedds#1360's program generation, seedds#1300's g_ghost_active): - serial_put / serial_del keep the program generation and add seedds#1439's held counts - tex_put keeps the same-object check and the g_ghost_active ring, and counts g_by_serial's names as seedds#1439 does - t_int_float_view.m keeps main's checks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qteK29T18U8p1c9LsVY8g
|
Measured on an M3 Max (36 GB, macOS 27.0.1, Xcode 27.2), Ocarina of Time v131072, running (not a cutscene): the same scripted run through Kokiri Forest, from the Kokiri balcony autosave, driven by
What the One note for whoever lands this on a tree with #1404's |
) replaces the pull request heads Upstream landed Ocarina of Time in main (chain adec7d81d970: "holds 60 fps in Kokiri Forest, 4 GB less") and closed the pull requests it folded in, with reworded and partly different code. Mixing a pull request head with its landed version hunk by hunk silently broke things (two GlobalToStorage tables, a duplicate AF2ip, two mtk_pso definitions), so the rule here is one version per feature: - Pull requests upstream closed (seedds#1299, seedds#1300, seedds#1312, seedds#1343, seedds#1345, seedds#1353, seedds#1354, seedds#1359, seedds#1360, seedds#1373, seedds#1428, seedds#1430, seedds#1434): their hunks are taken out of our side before the three-way merge, so upstream's landed version stands alone. Gone with them: bindless pool table patching (reg_bump), the ghost table's per-submission hold, the growing raw pool table and its write-log sizing, seedds#1428's pool_buf spares (upstream's pool_table_take is the same idea), the ixc-based indirect mesh draw (upstream uses mdx), and their tests (t_pool_table, t_mesh_indirect, t_gmem_raw, finalized_bindless_pool, test_bnsh_nvn2, test_ndve). - Open pull requests that upstream's landing supersedes: seedds#1394 (upstream's mtl_pool_addressed) and seedds#1393 pool_skip_retired (upstream's budgeted pool walk; the profile does not use it) are dropped with their tests and docs. seedds#1368's mesh pipeline prewarm is dropped too: upstream kept the background build in nvn_geometry.inc, and the prewarm's mtk_build / mtk_claim / g_warm_mps went with it. - Kept, as upstream does not have them: NXForge's own features (GPU watchdog, present filters and anti-aliasing, aniso floor, quick settings, pipeline archive compaction, upload breakdown, memory report lines) and the still open pull requests seedds#1304, seedds#1305, seedds#1308, seedds#1320, seedds#1331, seedds#1332, seedds#1355, seedds#1356, seedds#1369, seedds#1376, seedds#1377, seedds#1379, seedds#1382, seedds#1390, seedds#1391, seedds#1392, seedds#1414, seedds#1420, seedds#1436, seedds#1439, seedds#1446, seedds#1453. - Combined where both sides changed the same lines: GW_N 1 << 16 with seedds#1320's wide-write intervals (GW_PAGES = SBW_WPAGES, 128 slots), the CPU upload log with upstream's linear upload route, arch_open_end before the compaction limit, cbn_prog in geo_gs_one (seedds#1382). - Translator: upstream's sources and binaries, plus seedds#1414's InstEmitAmpereMem diff and the LDSM SubgroupBarrier in InstEmitAmpereTensor.cs. The binaries and dmsh_chain.lock are upstream's until the Mac rebuilds them (dotnet publish, tool_lock --update-lock, dmsh_chain --header); dmsh_chain.h is regenerated for now. - RED_ONCE keys: upstream took 153 and 154, so the present anti-aliasing lines move to 156 and 157 and seedds#1420's packaged-entry line to 158 (array 159). - Host: hle_net.c and t_net_eventfd.c are upstream's (its eventfd poll waits out the whole timeout, which our 5d7920c fixed another way). - Kept ours: dm/tools/romfs_image.py and its test, so the 26 GB RomFS image is not packed again. dm/tools tests on Linux: same failures as on 875449e (Linux-only quirks), plus test_nlg4 and test_dmsh_pool_arrays, which fail on upstream main too. Not yet built on the Mac. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qteK29T18U8p1c9LsVY8g
…n Kokiri Forest, about 4 GB less memory) beside this tree's own work Kept from this tree: the growing raw pool table with two-word hints (plus upstream's read-before-OR on its write log), seedds#1439's held count, seedds#1436's diagnostics, seedds#1447's texture release, seedds#1435, the idle evictions, the GPU watchdog, range-tracked GPU writes, the packaged-texture cache (g_pkg_alias), the synchronous task / mesh pipeline build with prewarm, this tree's F2IP. Taken from upstream: the pool tables (only what shaders read is made, within a budget; pool draws count as depth readers only when one was seen reading, replacing seedds#1430's rule; table reuse), sub_cut's busy rule, the indirect task / mesh draw through mdx, the empty mesh fragment, the larger GPU-write table (65,536 slots), linear textures by rows, the atomic alias on any 32-bit format, its eventfds (with this tree's select and fcntl on them), its prepo Send and audio update lock, its shader split and Ocarina profile and tables. Duplicates both sides added were removed (SDK 22 answers, F2IP, test stubs); RED_ONCE 153 split; t_pool_table (the replaced pool table code) removed; tests adapted to the taken rules. Translator rebuilt; chain 695a9393d125. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed texture in one lookup instead of walking the serial table: Ocarina of Time's long frames in Kokiri Forest fall from 332 to 212 (PR #1439) For each sharing-table entry a takeover could remove, alias_take_over called serial_holds(&g_by_serial, ...), a walk over the serial table's whole capacity. With thousands of serials and shared textures, every new shared texture walked the table once per overlapping entry, and in Ocarina of Time, where the texture pool's walks make hundreds of textures in a submission, that walk was the submit thread's hottest function while frames took up to a second. A count per object of the serial-table entries holding it (g_by_serial_held) is now kept by every writer of the serial table (serial_put, serial_del, tex_put and the upgrade's in-place replacement), and alias_take_over asks it in one lookup. An entry holds its object, so a counted address is not reused while counted. The reference entry notes that a change letting objects hold shared textures another way (PR #1404's views and packaged members) must count those holds too. t_alias_takeover: live-held entries stay, unheld ones go after ALIAS_IDLE submissions, two serials keep an object held until both go, and a takeover past 1,000 held entries with 20,000 serials takes 0.03 ms instead of 4.3 ms. Ocarina of Time on an M4 Pro, loading a Kokiri Forest save through Saria's dialogue and a walk: a host asking both the count and the walk compared them 393,216 times with no difference; [longframe] lines 332 -> 212, frame p99 72.8 -> 38.8 ms (one run each, hosts on the build's chain at 88c7e03). Measured by the author on an M5 Max and by a commenter on an M3 Max (frames over 100 ms 85 -> 2). Regress: Breath of the Wild's build is in the packed layout regress.py does not read, so Ocarina ran instead. Co-authored-by: enjoyerofslop <281774948+enjoyerofslop@users.noreply.github.com>
|
Landed as d4239a6, rebased onto main with your design and code unchanged (credited as co-author). Your second commit was not needed: main already has that What I checked on an M4 Pro, Ocarina of Time loading a Kokiri Forest save, through Saria's dialogue and a walk through the village:
One addition to the reference entry, after the note from jonpage0 (thanks for the M3 Max numbers): a change that lets a live object hold a shared texture another way (#1404's views and packaged members) must count those holds too. Whoever lands #1404 needs to carry the count there. |
…ng walked whole: registrations, memory pool changes, retired IDs at a new submission and storage takeovers each leave a note; Ocarina of Time's village walk builds its 33,025-entry table 16 times instead of over 4,000 (PR #1489) pool_gpu walked a table of handles traced to no slot whole at any change of g_reg_gen or g_ser_gen, though each change reaches only a few of its IDs. Ocarina of Time registers something between most of its draws, its streamed textures map and unmap virtual pool runs dozens of times a second, finalized descriptors stay registered, and textures take over each other's storage (alias_take_over), which bumped g_ser_gen with no note and so also sent every texture ID down the slow path. TheekshanaA measured about 53 s of a seven-minute walk through Kokiri Forest spent on 61,000 such builds (PR #1405, #1464). This is TheekshanaA's branch oot-pool-table-notes, ported to main's walk by PucaVaz, with the table patching of #1299 / #1387 it sat on: - A registration records its generation and ID (g_reg_log), a storage change its serial (g_ser_log, beside the notes ser_sync takes). A table built at an earlier generation takes again only the IDs they name, by the walk's own rules (pool_entry), and is kept when no entry changes, else copied and patched. - A memory pool added or removed records REG_POOL and no ID: a table takes again only the IDs it found without storage (pool_snap.dead); treg_gone notes each ID of a finalized pool. - A table keeps the IDs whose entries are views of retired registrations, those the read scan writes in place among them (pool_snap.trans); the next submission takes only those again, and such a table does not stand under POOL_SUB_MS. - A takeover records a note that names no texture (ser_note(0)): it changes no serial's texture (an entry a live serial holds is not evicted, serial_table_holds; one a finalized serial holds is only unref'd). A live sRGB / linear twin's view of an evicted texture is found stale by tex_mtl_ and dropped (serial_del), a note naming it. - A patch makes textures only within what the frame may still make (pool_room). Past POOL_IDS_MAX IDs, a note the ring has overwritten, a bump without a note, a resized table or a sparse mapping, the table is built whole as before. The log line says why; the power lines gain "[nvn] pool tables up to frame ...". On landing: the comment and the reference entry name serial_table_holds (#1439 replaced the serial table walk) and describe the sRGB / linear twin case; a registration note is read as a sequence lock (reg_read: its generation is checked before and after its ID, and the writer clears it first), as memory pool changes write notes from threads that do not hold g_rmu and a note the ring overwrote during the read could be taken torn. Checked: t_pool_notes 81 checks (patched tables compared with tables built whole), t_pool_chunks 102, t_alias_takeover 28, t_gmem_words 16, t_feedback 159, t_image_alias 329, t_sparse_tiles 4,253, t_cpu_tex --metal 68, all 0 failed; test_docs; the Mac host builds and the iOS compile check passes (261 files). Ocarina of Time on an M-series Mac, hosts at 51cc8bc with and without this (main's chain has no libraries on disk for it), the village walk from save_ref: tables built whole 4,000+ (5.85 s+) on the base, 12 and 16 (0.11 s, 0.12 s) with this; submit p50 in the village 11-16 ms on the base, about 7-12 ms with this; the same red lines and no GPU error. Vertical stripes in both sides' shots at the same balcony points come from that old-chain setup, not this change. The author measured submit p50 12-15 -> 3-5 ms and frames over 50 ms walking 23 and 48 -> 3 and 2 (M4, two runs per side). No second game: none local has libraries for main's chain, and the one whose regress runs reach these tables (Paper Mario: The Thousand-Year Door) needs 11,861 shaders translated first. Co-authored-by: PucaVaz <65235669+PucaVaz@users.noreply.github.com> Co-authored-by: TheekshanaA <10327965+TheekshanaA@users.noreply.github.com>
- Reconcile upstream PRs (seedds#1455, seedds#1447, seedds#1394, seedds#1439, seedds#1489, seedds#1590, seedds#1663) and commits (ffebfd5, e5f1ade) against local fork - Confirm PRs seedds#1455, seedds#1447, seedds#1394, and seedds#1439 are already implemented in ad99858 - Establish that host-only renderer optimizations require zero shader rebuilds and reuse the 42,854-entry b-dc815e0f38ca.img library - Establish that native subword stores (ffebfd5) affect only 515 shaders and leverage cas.db compilation caching - Rank candidates for base M3 Normal + Handheld (>=40 FPS target, ideal 60 FPS) and identify the top three candidates - Recommend the smallest independently verifiable first optimization (host-only PSO archive epoch separation from PR seedds#1590) - Update documentation index, plan, changelog, and known issues
…ed texture in one lookup instead of walking the serial table: Ocarina of Time's long frames in Kokiri Forest fall from 332 to 212 (PR seedds#1439) For each sharing-table entry a takeover could remove, alias_take_over called serial_holds(&g_by_serial, ...), a walk over the serial table's whole capacity. With thousands of serials and shared textures, every new shared texture walked the table once per overlapping entry, and in Ocarina of Time, where the texture pool's walks make hundreds of textures in a submission, that walk was the submit thread's hottest function while frames took up to a second. A count per object of the serial-table entries holding it (g_by_serial_held) is now kept by every writer of the serial table (serial_put, serial_del, tex_put and the upgrade's in-place replacement), and alias_take_over asks it in one lookup. An entry holds its object, so a counted address is not reused while counted. The reference entry notes that a change letting objects hold shared textures another way (PR seedds#1404's views and packaged members) must count those holds too. t_alias_takeover: live-held entries stay, unheld ones go after ALIAS_IDLE submissions, two serials keep an object held until both go, and a takeover past 1,000 held entries with 20,000 serials takes 0.03 ms instead of 4.3 ms. Ocarina of Time on an M4 Pro, loading a Kokiri Forest save through Saria's dialogue and a walk: a host asking both the count and the walk compared them 393,216 times with no difference; [longframe] lines 332 -> 212, frame p99 72.8 -> 38.8 ms (one run each, hosts on the build's chain at a4b9b0f). Measured by the author on an M5 Max and by a commenter on an M3 Max (frames over 100 ms 85 -> 2). Regress: Breath of the Wild's build is in the packed layout regress.py does not read, so Ocarina ran instead. Co-authored-by: enjoyerofslop <281774948+enjoyerofslop@users.noreply.github.com>
…ng walked whole: registrations, memory pool changes, retired IDs at a new submission and storage takeovers each leave a note; Ocarina of Time's village walk builds its 33,025-entry table 16 times instead of over 4,000 (PR seedds#1489) pool_gpu walked a table of handles traced to no slot whole at any change of g_reg_gen or g_ser_gen, though each change reaches only a few of its IDs. Ocarina of Time registers something between most of its draws, its streamed textures map and unmap virtual pool runs dozens of times a second, finalized descriptors stay registered, and textures take over each other's storage (alias_take_over), which bumped g_ser_gen with no note and so also sent every texture ID down the slow path. TheekshanaA measured about 53 s of a seven-minute walk through Kokiri Forest spent on 61,000 such builds (PR seedds#1405, seedds#1464). This is TheekshanaA's branch oot-pool-table-notes, ported to main's walk by PucaVaz, with the table patching of seedds#1299 / seedds#1387 it sat on: - A registration records its generation and ID (g_reg_log), a storage change its serial (g_ser_log, beside the notes ser_sync takes). A table built at an earlier generation takes again only the IDs they name, by the walk's own rules (pool_entry), and is kept when no entry changes, else copied and patched. - A memory pool added or removed records REG_POOL and no ID: a table takes again only the IDs it found without storage (pool_snap.dead); treg_gone notes each ID of a finalized pool. - A table keeps the IDs whose entries are views of retired registrations, those the read scan writes in place among them (pool_snap.trans); the next submission takes only those again, and such a table does not stand under POOL_SUB_MS. - A takeover records a note that names no texture (ser_note(0)): it changes no serial's texture (an entry a live serial holds is not evicted, serial_table_holds; one a finalized serial holds is only unref'd). A live sRGB / linear twin's view of an evicted texture is found stale by tex_mtl_ and dropped (serial_del), a note naming it. - A patch makes textures only within what the frame may still make (pool_room). Past POOL_IDS_MAX IDs, a note the ring has overwritten, a bump without a note, a resized table or a sparse mapping, the table is built whole as before. The log line says why; the power lines gain "[nvn] pool tables up to frame ...". On landing: the comment and the reference entry name serial_table_holds (seedds#1439 replaced the serial table walk) and describe the sRGB / linear twin case; a registration note is read as a sequence lock (reg_read: its generation is checked before and after its ID, and the writer clears it first), as memory pool changes write notes from threads that do not hold g_rmu and a note the ring overwrote during the read could be taken torn. Checked: t_pool_notes 81 checks (patched tables compared with tables built whole), t_pool_chunks 102, t_alias_takeover 28, t_gmem_words 16, t_feedback 159, t_image_alias 329, t_sparse_tiles 4,253, t_cpu_tex --metal 68, all 0 failed; test_docs; the Mac host builds and the iOS compile check passes (261 files). Ocarina of Time on an M-series Mac, hosts at 66e6e4c with and without this (main's chain has no libraries on disk for it), the village walk from save_ref: tables built whole 4,000+ (5.85 s+) on the base, 12 and 16 (0.11 s, 0.12 s) with this; submit p50 in the village 11-16 ms on the base, about 7-12 ms with this; the same red lines and no GPU error. Vertical stripes in both sides' shots at the same balcony points come from that old-chain setup, not this change. The author measured submit p50 12-15 -> 3-5 ms and frames over 50 ms walking 23 and 48 -> 3 and 2 (M4, two runs per side). No second game: none local has libraries for main's chain, and the one whose regress runs reach these tables (Paper Mario: The Thousand-Year Door) needs 11,861 shaders translated first. Co-authored-by: PucaVaz <65235669+PucaVaz@users.noreply.github.com> Co-authored-by: TheekshanaA <10327965+TheekshanaA@users.noreply.github.com>
…h residency commit stops the game instead of freezing the Mac Ported onto current main (the PR was written on a base before several upstream changes): - alias_let_go keeps main's rule for an entry leaving the sharing table, defer_obj_release (a texture a finalized object still resolves through g_ghost stays resident through defer_drain / g_retire_held). The PR carried the older serial_holds / defer_obj_unref walk that upstream replaced in PR seedds#1439, so that part is not taken. - front_mac.m: main's front_gpu_lost already was the PR's alert code inline. It is now one helper, front_stop_alert(title, text, status), used by front_gpu_lost (exit status 70, as before) and by the new front_gpu_memory_stop (quits as Cmd-Q). - The memory report line keeps main's extra fields and adds the PR's 'idle' count; Makefile and front.h keep both sides. On this Mac (Apple M4, 16 GB) Metal's working set is 11.84 GB: the guard lets go of idle sharing entries, spare textures and staging blocks from 9.47 GB and stops the game at 11.25 GB; DM_NVN_RES_LIMIT_MB=0 turns it off. Checked: t_res_guard 20 + 2 checks, the neighbouring residency / alias tests, the host builds for Ocarina of Time. Not run: a game, so how often the 80% level fires in play is not measured. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
What and why
Symptom: while a game makes many textures, frames take hundreds of milliseconds and up to a second, all of it in
submit+present(the[longframe]lines). Asampleof the process namesalias_take_overthe submit thread's hottest function. Found in The Legend of Zelda: Ocarina of Time's cutscenes (Switch 2,0400486024DEC000).Cause: when a new shared texture takes over storage,
alias_take_over(dm/gfx/nvn/nvn_mtl.m) checks each sharing-table entry it could remove. For each one it asked whether a live texture object still resolves to it withserial_holds(&g_by_serial, ...), a walk over the serial table's whole capacity. With about 7,500 serials and thousands of shared textures, every new shared texture walked the table once per overlapping entry. The disassembly puts the hot samples (alias_take_over+452..468) in that inlined walk. Ocarina of Time's texture-pool walks make hundreds of textures in one submission, each of them going through it.Change: a count per object of the serial-table entries that hold it (
g_by_serial_held). It is kept by every writer of the serial table:serial_put,serial_del,tex_put(when it writes the live table) and the upgrade's in-place replacement.alias_take_overnow asks it in one lookup. An entry holds its object (CFBridgingRetain), so a counted address cannot be reused while it is counted. A slot written past these helpers (onlyt_upgrade_aliasdoes that, to fill the table) is never counted, and taking it away leaves the count alone. The rulealias_take_overapplies is unchanged.Reference entry: A storage takeover walked the serial table for each entry, next to the takeover's own entry. The symptom index is regenerated. The entry also records what is still open: in that game the textures themselves are made again and again (84,000 to 103,000 storage takeovers in about 200 s of play), which I am looking into separately.
Relation to other PRs: independent of #1428 (pool tables), which touches other functions of the same file. #1299 reworks
serial_put,serial_delandtex_put. On it, the sameheld_addcalls go beside its own lines; that is how the Ocarina of Time host below was built.Checked
make -C dm/gfx/tests GAME=01007EF00011E000 t_alias_takeover: 28 checks OK. It covers:ALIAS_IDLEsubmissions;t_upgrade_alias(13),t_copy_alias(101),t_srgb_alias(8),t_image_alias(318) andt_cpu_tex(56) pass.make -k -C dm/gfx/tests GAME=01007EF00011E000 run: every test passes butt_nvn. It aborts inup_owner_check("up_begin called on thread ''", frompresent_enqueue), and it aborts the same way onmain(865cc35).python3 -m unittest dm/tools/tests/test_docs.py: OK.0400486024DEC000, update v131072, with [WIP] The Legend of Zelda: Ocarina of Time (Switch 2, 0400486024DEC000, v0, SDK 22.2.8) runs on the Mac through the title movie into Kokiri Forest #1299, Kokiri Forest through Saria's cutscene, M5 Max.samplecaptures of the process over the cutscene. Before,alias_take_overtook 2,277 of the 3,711 samples the submit thread spent making textures (tex_mtl_), 61 %. After, it took 38 of 618, 6 %, so each texture creation costs less than half as much.submit+present, and this function was the largest single part of it.DM_DETCLOCK. The cutscene's heaviest part lands inside some 250 s runs and outside others. In the runs that reached it, frames over 100 ms went from 101 and 102 to 58, and their total from 27 and 28 s to 19 s. One run with the fix is too few to call that more than a direction.python3 scripts/regress.py check --baseline 865cc35f --candidate HEAD --games 01007EF00011E000,0100000000010000(the full run).regress.py impactlists Breath of the Wild among the games that reach the serial table.saves/outdoorsandsaves/look_aroundare not on this Mac. Breath of the Wild waits at the load prompt (frame 2500 is not reached, in the baseline too), and Odyssey plays its opening from a new game.regress.py'sshot_hashes/shot_distanceagainst five runs of 865cc35:regress.py check (full): REGRESSION
Baseline
865cc35f865cc35, candidateHEAD4599612; Apple M5 Max, macOS 27.0.1, Xcode 27.2; 490.9 s01007EF00011E0000100000000010000The Legend of Zelda Breath of the Wild
01007EF00011E000: CHANGEDSuper Mario Odyssey
0100000000010000: REGRESSIONREGRESSION: screenshot at 95 s differs (dHash 12 bits, RGB 14.82)
Devices and OS: M5 Max (Mac17,6), macOS 27.0.1.
Docs
docs/reference/rendering.mdentry, andpython3 dm/tools/docs_symptoms.pywas run (the symptom index)🤖 Generated with Claude Code