diff --git a/dm/games/01007EF00011E000/c1_full.tsv b/dm/games/01007EF00011E000/c1_full.tsv index f450f37..c3ef939 100644 --- a/dm/games/01007EF00011E000/c1_full.tsv +++ b/dm/games/01007EF00011E000/c1_full.tsv @@ -1,3 +1,3 @@ -# chain b-f5259a6c6fb6 2026-10-20T13:33:37+08:00 +# chain b-d3a07e79011d 2026-10-20T14:40:41+08:00 21fe759b8114941c 60e2041ed375cae1 diff --git a/dm/games/01007EF00011E000/dmsh_chain.h b/dm/games/01007EF00011E000/dmsh_chain.h index 80c1604..8fd2f69 100644 --- a/dm/games/01007EF00011E000/dmsh_chain.h +++ b/dm/games/01007EF00011E000/dmsh_chain.h @@ -1,2 +1,2 @@ // Generated file (dm/gfx/tools/dmsh_chain.py --header, game 01007EF00011E000): the B shader library translation chain version this source tree expects (default options); the host loads only /b-/ -#define DMSH_CHAIN "f5259a6c6fb6" +#define DMSH_CHAIN "d3a07e79011d" diff --git a/dm/games/01007EF00011E000/shader_facts.tsv b/dm/games/01007EF00011E000/shader_facts.tsv index 62866e0..7a9b839 100644 --- a/dm/games/01007EF00011E000/shader_facts.tsv +++ b/dm/games/01007EF00011E000/shader_facts.tsv @@ -65,6 +65,7 @@ 8e20a8168bbce06d {"local":[32,1,1],"shared":896} 8e2cc2671cae6020 {"fout":{"0":"uint"}} 8e61f68132df7a66 {"local":[32,1,1],"shared":0} +93aa5e22a8202409 {"fout":{"0":"uint"}} 958eabd8395dd82c {"local":[32,1,1],"shared":0} 9aed8577b5dc6b59 {"local":[32,1,1],"shared":0} 9b305fb5093e2b96 {"local":[32,1,1],"shared":0} diff --git a/dm/games/01007EF00011E000/shader_facts_manual.tsv b/dm/games/01007EF00011E000/shader_facts_manual.tsv index 9ec9bdf..952edb1 100644 --- a/dm/games/01007EF00011E000/shader_facts_manual.tsv +++ b/dm/games/01007EF00011E000/shader_facts_manual.tsv @@ -3,3 +3,5 @@ # (the host's red line then prints the row to add here). # 8e2cc2671cae6020 (agl_common BAHS@0x453f0 in Bootup_Graphics.pack, pixel): the BC4 block encoder. It writes 8-byte blocks through an RGBA16UI view into a 32x32 BC4 texture (nvn_mtl.m bcrt_attach) 8e2cc2671cae6020 {"fout":{"0":"uint"}} +# 93aa5e22a8202409 (agl_common BAHS@0x44918 in Bootup_Graphics.pack, pixel): the same BC4 block encoder for a 2D source (8e2cc2671cae6020 reads a 3D one); its pipeline failed in an iPhone run once the BC4 target was attached +93aa5e22a8202409 {"fout":{"0":"uint"}} diff --git a/dm/gfx/nvn/nvn_mtl.m b/dm/gfx/nvn/nvn_mtl.m index ec7aad4..a5f0d98 100644 --- a/dm/gfx/nvn/nvn_mtl.m +++ b/dm/gfx/nvn/nvn_mtl.m @@ -3049,8 +3049,16 @@ static int vconv_int(int f, int *bits, int *sgn) { // components of an 8 / 16- for (int i = 0; i < 16; i++) if (T[i] == f) { *bits = i & 2 ? 16 : 8; *sgn = i & 1; return i / 4 + 1; } return 0; } +// *_UI2F / *_I2F formats (0x51-0x68, R8 R16 R32 RG8 … RGBA32, unsigned then signed): integers that the fetch converts to float without normalizing (200 → 200.0). Metal has no such vertex +// format and refuses an integer format into a float input, so they are converted here too; an integer input of the same format takes the float32 bits, as from Maxwell's fetch. +// Breath of the Wild: an RG8_UI2F attribute (0x57, at the title) had no Metal format and read 0 +static int vconv_i2f(int f, int *bits, int *sgn) { // components of an integer-read-as-float format (with its component size and signedness); 0 = not one + if (f < 0x51 || f > 0x68) return 0; + int i = (f - 0x51) >> 1; *sgn = (f - 0x51) & 1; *bits = 8 << (i % 3); return i / 3 + 1; +} static int vconv_size(int f) { // bytes of one element int ib, is, in = vconv_int(f, &ib, &is); if (in) return in * ib / 8; + if ((in = vconv_i2f(f, &ib, &is))) return in * ib / 8; switch (f) { case 0x01: case 0x02: return 1; case 0x05: case 0x06: case 0x07: case 0x0D: case 0x0E: return 2; case 0x19: case 0x1A: return 3; case 0x1D: case 0x1E: case 0x1F: return 6; case 0x29: case 0x2A: case 0x2B: return 8; } return 4; @@ -3070,6 +3078,12 @@ static void vconv_elem(int f, const uint8_t *p, float o[4]) { else { uint16_t h; memcpy(&h, p + 2 * i, 2); u[i] = is ? (uint32_t)(int32_t)(int16_t)h : h; } } memcpy(o, u, sizeof u); return; } o[0] = o[1] = o[2] = 0; o[3] = 1; + if ((in = vconv_i2f(f, &ib, &is))) { // integers read as floats: each value as a float, the missing components (0, 0, 0, 1.0) + for (int i = 0; i < in; i++) { + if (ib == 8) o[i] = is ? (float)(int8_t)p[i] : (float)p[i]; + else if (ib == 16) { uint16_t h; memcpy(&h, p + 2 * i, 2); o[i] = is ? (float)(int16_t)h : (float)h; } + else { uint32_t w; memcpy(&w, p + 4 * i, 4); o[i] = is ? (float)(int32_t)w : (float)w; } } + return; } int n = vconv_ncomp(f); for (int i = 0; i < n; i++) switch (f) { case 0x01: case 0x0D: case 0x19: case 0x25: o[i] = p[i] / 255.0f; break; @@ -3093,6 +3107,7 @@ static int vconv_wanted(const nvn_refl *rf, int v, const pso_key *k) { // does int a = rf->v[v].nvn, ct = rf->v[v].ct, ib, is; if (a >= 16) return 0; int f = k ? k->attr[a].fmt : S.attr[a].fmt; + if (vconv_i2f(f, &ib, &is)) return 1; // whatever the input type if (ct == NVN_CT_FLOAT) return vconv_int(f, &ib, &is) > 0; if (ct != NVN_CT_UINT && ct != NVN_CT_INT) return 0; return vconv_ncomp(f) || (vconv_int(f, &ib, &is) && is != (ct == NVN_CT_INT)); @@ -3113,7 +3128,9 @@ static void vconv_bind(id en, const nvn_refl *rf) { for (uint64_t i = 0; i < nv; i++) vconv_elem(f, (const uint8_t *)(uintptr_t)(addr + off + i * stride), d + 4 * i); gpu = b.gpuAddress + o; int c = g_vconv_next++ % 64; g_vconv[c].addr = addr; g_vconv[c].size = size; g_vconv[c].serial = g_sub_serial; g_vconv[c].fmt = (uint32_t)f; g_vconv[c].off = off; g_vconv[c].stride = stride; g_vconv[c].gpu = gpu; - if (rf->v[v].ct == NVN_CT_FLOAT) RED_ONCE(99, "vertex attribute format %#x (8 / 16-bit integer) feeds a float input: the 32-bit register values Maxwell's fetch makes are bound as float bit patterns, converted on the CPU at replay", f); + int ib, is; + if (vconv_i2f(f, &ib, &is)) { static int once; if (!once++) MLOG("vertex attribute format %#x (integers read as floats, which Metal has no vertex format for): converted on the CPU at replay and bound separately", f); } + else if (rf->v[v].ct == NVN_CT_FLOAT) RED_ONCE(99, "vertex attribute format %#x (8 / 16-bit integer) feeds a float input: the 32-bit register values Maxwell's fetch makes are bound as float bit patterns, converted on the CPU at replay", f); else if (vconv_ncomp(f)) RED_ONCE(89, "vertex attribute format %#x feeds an integer input: converted to float32 bit patterns on the CPU at replay and bound separately (the conversion Maxwell does on vertex fetch)", f); else RED_ONCE(96, "vertex attribute format %#x (8 / 16-bit integer) feeds an integer input of the opposite signedness: extended to 32 bits with the format's own signedness on the CPU at replay and bound separately", f); } else if (nv) RED_ONCE(90, "vertex attribute conversion: a range of %llu vertices exceeds the per-frame staging (format %#x)", (unsigned long long)nv, f); @@ -3138,6 +3155,12 @@ static MTLVertexFormat vfmt(int f) { case 0x2D: return MTLVertexFormatShort4; case 0x2E: return MTLVertexFormatFloat4; case 0x2F: return MTLVertexFormatUInt4; case 0x30: return MTLVertexFormatInt4; case 0x3D: return MTLVertexFormatUInt1010102Normalized; case 0x69: return MTLVertexFormatInt1010102Normalized; case 0x3F: return MTLVertexFormatFloatRG11B10; case 0x9A: return MTLVertexFormatUChar4Normalized_BGRA; } + int ib, is, in = vconv_i2f(f, &ib, &is); // integers read as floats: the integer format of the same layout (the pipeline reads vconv_bind's conversion) + if (in) { + static const MTLVertexFormat U[3][4] = { { MTLVertexFormatUChar, MTLVertexFormatUChar2, MTLVertexFormatUChar3, MTLVertexFormatUChar4 }, { MTLVertexFormatUShort, MTLVertexFormatUShort2, MTLVertexFormatUShort3, MTLVertexFormatUShort4 }, { MTLVertexFormatUInt, MTLVertexFormatUInt2, MTLVertexFormatUInt3, MTLVertexFormatUInt4 } }; + static const MTLVertexFormat I[3][4] = { { MTLVertexFormatChar, MTLVertexFormatChar2, MTLVertexFormatChar3, MTLVertexFormatChar4 }, { MTLVertexFormatShort, MTLVertexFormatShort2, MTLVertexFormatShort3, MTLVertexFormatShort4 }, { MTLVertexFormatInt, MTLVertexFormatInt2, MTLVertexFormatInt3, MTLVertexFormatInt4 } }; + return (is ? I : U)[ib == 8 ? 0 : ib == 16 ? 1 : 2][in - 1]; + } return MTLVertexFormatInvalid; } static MTLBlendFactor bfac(int f) { diff --git a/docs/agents/04-graphics.md b/docs/agents/04-graphics.md index 1c7c76d..d6138af 100644 --- a/docs/agents/04-graphics.md +++ b/docs/agents/04-graphics.md @@ -74,7 +74,7 @@ So the translator leaves z alone (`QueryGraphicsState` depth mode false in `Prog - **SPIRV-Cross failures**: 2 agl post-effect compute shaders still produce SPIR-V that SPIRV-Cross can't parse (`nullptr` exception). Before the compute start-offset fix it was 7: misdecoded code produced the bad SPIR-V. - **CPU-written linear textures**: a texture with `NVN_TEXTURE_FLAGS_LINEAR` or `LINEAR_RENDER_TARGET` in a CPU-visible pool (Mario's glyph cache; its four 64×1 RGBA16F sky colour ramps, without which the sky is black) is created on the pool's `MTLBuffer` (`tex_linear`), so CPU writes (Mario's glyph cache) are visible to the GPU. Block-linear CPU-written textures still need the packaged-texture re-upload path. - **Integer vertex inputs**: without PC reflection the translator declares every vertex input `float4`. Skinned models read their bone index (`R8UI` / `RG8UI` / `RGBA8UI`) as raw integer bits (`as_type(in.m_N.x) * 48`); Metal converted the integers to floats, every bone index was garbage, and all characters were stretched into streaks. `dmsh_build.py` (`int_inputs`) now re-translates a vertex shader with `uint` inputs when every use of an input is an integer bit cast (529 of 4,854 in Mario), and the host makes the Metal vertex format agree with the declared type (`vfmt_for_ct`; signedness swaps of 32-bit components and 32-bit float→int reinterpretation keep the bits, while 8- and 16-bit integers of the other sign are widened on the CPU, see Three Houses below). A remaining mismatch is reported as `vertex attribute format … does not match shader input type …`. -- **Vertex formats**: Mario uses `RGB10A2SN` (0x69) → `MTLVertexFormatInt1010102Normalized`. Unmapped formats fall back to a 4-component read and print `vertex attribute format … not mapped` once. +- **Vertex formats**: Mario uses `RGB10A2SN` (0x69) → `MTLVertexFormatInt1010102Normalized`. Unmapped formats fall back to a 4-component read and print `vertex attribute format … not mapped` once. The integer-read-as-float formats (`*_UI2F` / `*_I2F`, 0x51–0x68), which Metal has no vertex format for, are converted on the CPU at replay like the other conversions (`vconv_i2f`); Breath of the Wild has an `RG8_UI2F` attribute. - **Instanced array draws** (`DrawArraysInstanced`) carry instance count and base instance in the `OP_DRAW` record. - **Metal 4 does not order operations inside one encoder.** The host batches consecutive copies, fills and inline uniform updates (`UpdateUniformBuffer`) into one compute encoder. Mario writes the same uniform slot twice in a row each frame (4 bytes of −1, then 16 bytes of wave coefficients); without an intra-encoder barrier the writes occasionally landed in reverse order, the fountain's GPU wave simulation (a 512×1 RGBA16F texture updated by render → copy back → splash pass every frame) ran with −1 as its self coefficient, diverged to ±65504, and its vertex shader pushed the water surface to the top of the screen (tall blue / textured panels that flickered frame to frame). `blit_buf` now inserts `barrierAfterEncoderStages` when an operation reads or writes a guest range an earlier operation in the same encoder wrote; texture copies always get one. `DM_NVN_BLIT_ORDER=0` turns the barriers off to reproduce. Render passes and compute dispatches each have their own encoder with full barriers, so they were not affected. - **New textures start as zero.** Metal leaves a new texture's content undefined; a freshly mapped Switch memory pool reads as 0, and feedback effects (such as the wave simulation above) never clear their state texture. Non-packaged colour textures are zero-filled from a shared zero buffer when created (`tex_zero_fill`, batched with the uploads). @@ -129,7 +129,7 @@ So the translator leaves z alone (`QueryGraphicsState` depth mode false in `Prog - **Fixed: the 3D scene black, only particles and the HUD drawn.** The engine binds almost every texture with `nvnCommandBufferBindTexturesDeferred` (`main+0xec2e64`, one handle, the pointer being the handle field inside the texture-sampler object). "Deferred" means the GPU reads the handle from that address *when the command executes*; the game records its command blocks once, submits them every frame and writes the handles afterwards. The host copied the handles at recording, mostly zeros, so the deferred-lighting composite (one full-screen triangle per material class, blending into the HDR target) read the dummy texture 0 for all of its inputs. The bind record now keeps the source address (`p_bind_handles.defer / src`) and every replay-time reader refreshes the handles first (`bh_fresh` in `nvn_mtl.m`). How it was found: a frame data-flow table built from `DM_NVN_PASSDUMP_ALL` (per pass: attachments, shader keys, sampled serials) showed the half-resolution lighting outputs never being read, and the composite pass's `B` lines were all `tid=0 dummy`. Without `_ALL` the dump lists only the first draw of each pass, which hid the composite. - **Fixed: the same texture attached at two colour slots.** The G-buffer passes attach the albedo and another buffer twice each (c1 = c2, c3 = c4); the duplicate slots have write mask 0. On Maxwell the ROP writes only masked channels and the slots do not interfere. On Apple's TBDR each attachment has its own tile memory and the whole tile is stored at the end of the pass, so the copy stored later (loaded, never written) overwrote the other one's writes, in a different order for each 32×32 tile: the albedo held only scattered squares. A slot whose texture, level and layer repeat an earlier slot is now left unattached (`rt_dup_slot`), also in pipeline prediction; a draw that really writes such a slot is reported once (`color slot … holds the same texture as an earlier slot`). Records hold copies of texture objects (`tex_snap`), so slots are compared by serial, not by pointer. - **Cube textures sampled as cube arrays.** 1,512 shaders sample the environment probes as `texturecube_array` while a single cube texture is bound. Metal can make a cube-array view only of a 2D-array base texture, so NVN cube textures are now created as 6-layer 2D arrays and bound through cube / cube-array views by `tex_fit` (which also maps a cube array read as a cube to its first cube). -- **Fixed: a compressed texture as a render target.** agl's BC4 block encoder (`8e2cc2671cae6020`) writes a 32×32 RGTC1 (BC4) texture through an `RGBA16UI` view (one texel per 8-byte block: BC4 encoded on the GPU). Metal can neither attach a compressed format nor make an uncompressed view of one, and the pipeline-descriptor validation aborts the process, so the attachment used to be left out and its draws did nothing (`render target is a compressed-format texture …`). Such a pass now draws into an uncompressed shadow of blocks × blocks in the view's format: the level's (or layer's, or depth plane's) blocks are copied into it through a buffer before the pass and back after it (`bcrt_attach` / `bcrt_flush` in `nvn_mtl.m`; the method comes from PR #11). The shadows stay resident and are reused, and they are kept out of the load-skip and store-elision bookkeeping. The encoder's output must be declared `uint`: the game has no PC version, so the profile's `shader_facts_manual.tsv` lists it (without it the pipeline fails and the red line prints the row to add). +- **Fixed: a compressed texture as a render target.** agl's BC4 block encoder (`8e2cc2671cae6020`) writes a 32×32 RGTC1 (BC4) texture through an `RGBA16UI` view (one texel per 8-byte block: BC4 encoded on the GPU). Metal can neither attach a compressed format nor make an uncompressed view of one, and the pipeline-descriptor validation aborts the process, so the attachment used to be left out and its draws did nothing (`render target is a compressed-format texture …`). Such a pass now draws into an uncompressed shadow of blocks × blocks in the view's format: the level's (or layer's, or depth plane's) blocks are copied into it through a buffer before the pass and back after it (`bcrt_attach` / `bcrt_flush` in `nvn_mtl.m`; the method comes from PR #11). The shadows stay resident and are reused, and they are kept out of the load-skip and store-elision bookkeeping. The encoder's output must be declared `uint`: the game has no PC version, so the profile's `shader_facts_manual.tsv` lists it (without it the pipeline fails and the red line prints the row to add), and so does its twin for a 2D source (`93aa5e22a8202409`), whose failure showed up in an iPhone run. - **Big-stack NVN entry points**: the render thread's stack is small, and Metal's driver compiles internal programs (a fast-clear blit) on the calling thread while the host ends a render pass inside `nvnQueueSubmitCommands`. That overflowed the guest stack (SIGBUS in `AGCDeserializedReply`, address in the guest stack region). All implemented NVN functions except the `nvnCommandBuffer*` recording calls now run on the per-thread 512 KB host stack ([03](03-bringup.md), big-stack entry; `nvn_big_entry` in `nvn_entry.S`). - **60 fps** is now the host's frame step in whole vsyncs ([development.md](../development.md), Breath of the Wild): a stub at the framework's `setWaitVsyncInterval` hands the game the host's step, and frames stay on screen exactly that long. The notes below are about the method before it, which `DM_FRAME_STEP=legacy` still runs: a rewriter cut on the game's frame-interval logic ([03](03-bringup.md)); the engine's variable frame rate (`ksys::VFR`) scales its time step by the present interval, so the speed is right as long as the interval matches the frames. **Fixed: slow motion when the GPU is the bottleneck (GitHub issue #9).** The interval controller (`main+0x112660c`) measured a frame as framework +0x250, the main thread's CPU span from the start of `procFrame` to before its draw step (`main+0x1126cec` → `main+0x1126d20`): about 2.7 ms in the shrine (probe at `main+0x11266a4`, `@x19+0x250`). Only while the object at `[main+0x1e3a638] + 0x1d2b8` exists did it take max(CPU span, GPU time), in 64 of 6,046 shrine frames. So the interval stepped up only on CPU spikes: an iPhone 16 Pro Max at a 25 ms frame p50 never left interval 1 in 4,800 frames, and the game ran at about two thirds of its speed. The GPU time (framework +0x220) is the difference between the frame's two timestamp reports: `TIMESTAMP_TOP` at the start of the frame's command buffer (`main+0xe94584`) and `TIMESTAMP` at its end (`main+0xe94664`), read after the draw step has waited for the frame's fence (`main+0xe949bc`). It read 0–6 ticks, because the host wrote all 5 reports of the frame's one submission with one time in its completion handler. Two more cuts (`main+0x112691c`, `main+0x1126974`: cbz → b) make the controller always take the GPU path. The profile key `gpu_timestamps segment` makes each report take the GPU start (`TIMESTAMP_TOP`) or end (other types) of the `sub_cut` segment it was encoded in (`rep_ticks` in `nvn_mtl.m`: MTL4CommitFeedback times converted to the guest clock, written in the last segment's handler as before). The GPU time now reads the frame's GPU span, gaps where the GPU waits for the host's translation included. Measured on the Mac with two BotW runs sharing the GPU under a heavy CPU load (load average about 75, a shader rebuild running): GPU time p50 16–23 ms; the fixed run stayed at interval 2 (3 while the GPU time passed 1.9 vsyncs, 1 for under 2% of the frames) at 0.93–0.98 of real time (Σ VFR ratio / 30 per wall second, probe `main+0x151d3e0` `@x0+0x30`: +0x30 ratio, +0x34 interval). At the same time a run with the old timestamps (`DM_NVN_GPU_TS=submission`) spent 10–70% of its frames at interval 1 and ran at 0.78–0.95. The same GPU time drives the game's dynamic resolution (`main+0x1267070`): a running average as a percentage of a 60 Hz frame scales the resolution down above 175% (29 ms) and back up below 150% (25 ms). With 0 it always sat at its maximum, and it still does whenever the game holds 30. - **Render scale (`DM_RENDER_SCALE`)**: the dynamic-resolution method (`main+0x12666e4`) keeps its scale in s8 (0.8–1.0 in 0.05 steps, or the forced 0.75 / 0.5 of `+0x284 & 0x1800`) and applies it from `main+0x1266880`: the views' scale (`+0x28a0` / `+0x28a4`), from which the size function (`main+0x106340c`) sizes the scene buffers each frame, the shader ratio `uking_dynamic_resolution_ratio` and the other readers. That block runs only when the dynamic path has just recomputed the scale (`+0x280` bit 4, set at `main+0x1266830`) or in a forced mode, else `main+0x126686c` skips it. The stub sets s8 at the block and, for a factor other than 1.0, takes the block every frame, so the factor holds wherever the dynamic path does not run. A multiply in the size function (Tears of the Kingdom's way) would leave the ratio and those readers at the old scale.