Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions dm/games/01004AB00A260000/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ No `render_cond`: `SetRenderEnableConditional` is drawn unconditionally, as in B

FromSoftware's engine draws forward into a 1920x1080 RGBA16F target, with depth of field, light shafts and bloom at quarter size.

- **Current: image-only ambient occlusion loses its attachment layer ([#187](https://github.com/seedds/nxnative/issues/187#issuecomment-6519569185)).** Jacob-Tate traced near-black Asylum areas to the zone-dependent ambient-occlusion pass `c1d0b7f1a8a50799`, whose image writes target layers of its own color attachment. The [attachment-store fix](../../../docs/reference/rendering.md#image-stores-into-an-attachment-of-their-own-pass) is reproduced and verified using synthetic GPU shaders on an M2 Max; the game's route and the reported iPhone torch patches have not been tested locally. The report also identified a separate PrimitiveID pipeline fault, already handled by the existing general pipeline path. The artifacts in [#308](https://github.com/seedds/nxnative/issues/308) are not established to have this cause.

- **Resolved: distant walls and floors smeared sideways.** The map materials (`FRPG_Phn_*Lit*`) read their combined texture / sampler handles from a uniform buffer (`fp_t_cb13_*`, resolved at replay by `ubo_handle`), and the game keeps 12 sampler pools of 2,304 entries, each with its own samplers under the same IDs (ID 261 is clamp-to-edge with LOD 0..0 in one pool, repeat with mips in others), one per command buffer that records the scene. The replay resolved uniform-buffer handles in the first pool, so the walls' tiling base textures (u ≈ −3.45) took a clamp-to-edge sampler and repeated their edge column across the wall. The replay now keeps the texture and sampler pools bound (`OP_SET_TEX_POOL` / `OP_SET_SAMP_POOL`) and adds their bases to handles read from uniform buffers (`ubo_tex_id` / `ubo_samp_id` in `nvn_mtl.m`). Found by highlighting the six most common fragment shaders of the scene pass (`DM_NVN_HILITE`), then the next six, and reading the wall draw's binding lines (`DM_NVN_PASSDUMP_ALL`).
- **Resolved: 6,734 red lines in one run.** The list of command buffers that bound a non-first pool had 64 entries and was never cleared, and this game binds a non-first pool in more than 64. The bases of bound handles now live in each command buffer (`cmd_t` `tex_base` / `samp_base`).

Expand Down Expand Up @@ -66,6 +68,8 @@ SDK 4.5.0 boots, one each:

General host changes first needed here; each applies to every game:

- **Image stores preserved across an unchanged color attachment’s tile store (synthetic fix; game validation open):** [Image stores into an attachment of their own pass](../../../docs/reference/rendering.md#image-stores-into-an-attachment-of-their-own-pass).

- **SDK 4's GLSLC outputs read through their section headers** (`glslc_section`) and **uniform-buffer handles resolved against the bound pools** (`OP_SET_TEX_POOL` / `OP_SET_SAMP_POOL`, `ubo_tex_id` / `ubo_samp_id`, `cmd_t` `tex_base` / `samp_base`): [GLSLC outputs read through their section headers](../../../docs/reference/shaders.md#glslc-outputs-read-through-their-section-headers), [Handles from uniform buffers take the bound pools' bases](../../../docs/reference/shaders.md#handles-from-uniform-buffers-take-the-bound-pools-bases), [Bases of bound handles kept in the command buffer](../../../docs/reference/shaders.md#bases-of-bound-handles-kept-in-the-command-buffer).
- **The dvdbnd / DCX / BND3 reader** (`bdt_shaders`): [GLSLC outputs in FromSoftware's BND3 and dvdbnd archives](../../../docs/reference/shaders.md#glslc-outputs-in-fromsoftwares-bnd3-and-dvdbnd-archives).
- **SDK 4's movie decoder constructor and its buffer without a sync object** (`hle_movie.c`), **`friends::GetBlockedUserList` without a `Uid`**, **the UTF-16 profanity masking by name** (`hle_misc.c`), and **`Poll` / `Select` with nothing to wait on sleeping out their timeout** (`hle_net.c`): [SDK 4's `movie::Decoder` constructor without a priority](../../../docs/reference/movies.md#sdk-4s-moviedecoder-constructor-without-a-priority), [SDK 4's `movie::Buffer` has no sync object](../../../docs/reference/movies.md#sdk-4s-moviebuffer-has-no-sync-object), [SDK 4's `friends::GetBlockedUserList` without a `Uid`](../../../docs/reference/services.md#sdk-4s-friendsgetblockeduserlist-without-a-uid), [`nn::ngc::ProfanityFilter` vtable and by-name overloads](../../../docs/reference/services.md#nnngcprofanityfilter-vtable-and-by-name-overloads), [Socket `Poll` and `Select` with nothing to wait on](../../../docs/reference/services.md#socket-poll-and-select-with-nothing-to-wait-on).
Expand Down
4 changes: 2 additions & 2 deletions dm/gfx/nvn/nvn_geometry.inc
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ static int geo_mesh_draw(NvProg *pr, const p_draw *dw, const p_draw_idx *di) {
g_geo_omit_colors = 0;
g_geo_program = nil; g_geo_mesh.on = 0; args_dirty_all();
if (g_geo_mesh.miss) { g_geo_mesh_wait++; return 0; }
swrite_note(pr); udc_note_draw(pr); g_wr_epoch++; g_mdx_epoch++; // the kernel's storage writes
swrite_note(pr); udc_note_draw(pr, 0); g_wr_epoch++; g_mdx_epoch++; // the kernel's storage writes
if (!g_geo_mesh_n++) MLOG("geometry mesh: first mesh draw, vertex %s kernel %s, %u triangles", pr->sh[0]->key, g_geo_mesh.lean->key, g_geo_mesh.a.count / 3);
return 1;
}
Expand Down Expand Up @@ -660,7 +660,7 @@ static void geo_gs_one(id<MTL4ComputeCommandEncoder> ce, const geo_job *j) {
enc_c_args(ce, j->lean_job, 0); enc_c_pso(ce, g_geo_lean_k[j->direct_count ? 2 : 0]);
enc_c_dispatch(ce, MTLSizeMake((j->groups + 63) / 64, 1, 1), MTLSizeMake(64, 1, 1));
}
swrite_note(j->pr); udc_note_draw(j->pr); g_wr_epoch++; g_mdx_epoch++;
swrite_note(j->pr); udc_note_draw(j->pr, 0); g_wr_epoch++; g_mdx_epoch++;
}
static void geo_raster(const geo_job *j, int last_in_run) {
g_geo_bind[1] = j->output;
Expand Down
45 changes: 28 additions & 17 deletions dm/gfx/nvn/nvn_mtl.m
Original file line number Diff line number Diff line change
Expand Up @@ -3016,21 +3016,21 @@ static void pg_commit(id<MTLCommandBuffer> cb) { // Hand this submission's pas
// ---------- Store lookahead: at pass end, decide from the later commands of the same submission whether each attachment's write-back is redundant ----------
// On TBDR every attachment is written back to memory in full when the pass ends; Switch (Maxwell immediate-mode rendering) has no such cost. If an attachment is cleared in full before being read, the write-back is redundant (DontCare).
// For the decision see mtl_sub_index / sdx_decide; only what can be proven within the same submission is accepted, and anything still undecided at the end of the submission is always written back.
typedef struct { uint64_t ser, bytes; uint32_t lv, sl; int idx; uint8_t depth, state, ridx, drawn, why, load, wrote, wwhy; const char *wkey; int wslot; } sd_att; // depth: 0 color 1 depth 2 stencil (the two parts of a combined depth-stencil texture are decided separately) // state 0 undecided 1 needed 2 redundant; idx: color attachment index, -1 for depth
typedef struct { uint64_t ser, bytes; uint32_t lv, sl; int idx; uint8_t depth, state, ridx, drawn, why, load, wrote, wwhy, img, rast; const char *wkey; int wslot; } sd_att; // depth: 0 color 1 depth 2 stencil (the two parts of a combined depth-stencil texture are decided separately) // state 0 undecided 1 needed 2 redundant; idx: color attachment index, -1 for depth
// Depth and stencil have independent store decisions, even in one combined texture.
// Eight color targets therefore need ten records (XC2 uses the full set).
enum { SD_ATT_MAX = 8 + 2 };
static sd_att g_sd[SD_ATT_MAX]; static int g_nsd;
static int g_udc_ab = -1, g_udc_ab_on;
// Attachments this pass did not write (depth / stencil test only, color write mask 0, etc.) get their write-back changed to DontCare: the content is already in memory, saving one full write-back.
// On by default: on Mac, same-process on / off screenshots follow the same distribution as the null control; on A19 Pro, private Depth32F_S8 / RGBA8 read back after DontCare differ in 0 places.
// DM_NVN_UNWRITTEN_DC=0 turns it off (for equivalence comparison)
// DM_NVN_UNWRITTEN_DC=0 turns optional savings off (for equivalence comparison); the image-only color store rule in sd_finish remains mandatory.
static int udc_on(void) {
if (g_udc_ab < 0) { const char *e = getenv("DM_NVN_UNWRITTEN_DC"); g_udc_ab = !(e && !strcmp(e, "0"));
const char *a = getenv("DM_NVN_UDC_AB"); if (a && !strcmp(a, "1")) { g_udc_ab = 0; g_udc_ab_on = 0; } }
return g_udc_ab || g_udc_ab_on;
}
// DM_NVN_TBDR=0 disables lookahead and pass merging. The independent unwritten-attachment proof remains active; DM_NVN_UNWRITTEN_DC=0 disables that too.
// DM_NVN_TBDR=0 disables lookahead and pass merging. DM_NVN_UNWRITTEN_DC=0 also disables optional unwritten-attachment savings; neither disables image-write preservation.
// Only for equivalence comparison (the same replay run on / off, comparing the picture byte by byte), not a product configuration
// Mario: off by default (only DM_NVN_TBDR=1 turns it on). These optimizations infer read / write relations from "which textures are bound in texture slots", while 38% of this game's shaders use bindless textures (handles in uniform buffers, NVN_RB_TEXU),
// so the inference would miss readers - e.g. deciding an attachment is never read afterwards and not storing it. Turn them back on once they recognize bindless readers.
Expand Down Expand Up @@ -3516,7 +3516,7 @@ static void attach(MTLRenderPassAttachmentDescriptor *a, const tex_t *t, const v
g_nsd = 0;
{
for (int i = 0; i < R.rt.n && i < 8; i++) { MTLRenderPassColorAttachmentDescriptor *c = pd.colorAttachments[i]; if (!c.texture || bcrt_is(c.texture)) continue;
g_sd[g_nsd] = (sd_att){0}; g_sd[g_nsd].ser = R.rt.color[i]->serial; g_sd[g_nsd].bytes = att_bytes(c.texture, c.level); g_sd[g_nsd].lv = (uint32_t)c.level; g_sd[g_nsd].sl = (uint32_t)(c.texture.textureType == MTLTextureType3D ? c.depthPlane : c.slice); g_sd[g_nsd].idx = i; g_sd[g_nsd].load = c.loadAction == MTLLoadActionLoad; g_sd[g_nsd].wrote = R.clear_c[i] != 0; g_nsd++;
g_sd[g_nsd] = (sd_att){0}; g_sd[g_nsd].ser = R.rt.color[i]->serial; g_sd[g_nsd].bytes = att_bytes(c.texture, c.level); g_sd[g_nsd].lv = (uint32_t)c.level; g_sd[g_nsd].sl = (uint32_t)(c.texture.textureType == MTLTextureType3D ? c.depthPlane : c.slice); g_sd[g_nsd].idx = i; g_sd[g_nsd].load = c.loadAction == MTLLoadActionLoad; g_sd[g_nsd].wrote = g_sd[g_nsd].rast = R.clear_c[i] != 0; g_nsd++;
c.storeAction = MTLStoreActionUnknown; }
id<MTLTexture> dt = pd.depthAttachment.texture, stt = pd.stencilAttachment.texture;
uint64_t db = att_bytes(dt, pd.depthAttachment.level), sb = 0;
Expand Down Expand Up @@ -4881,21 +4881,14 @@ static void swrite_note(NvProg *pr) {
if (g_pass_swrite) { g_vbuf_write_epoch++; return; } // further shader writes also invalidate aligned vertex snapshots
if (prog_swrites(pr)) { g_pass_swrite = 1; g_vbuf_write_epoch++; }
}
static void udc_note_draw(NvProg *pr) {
static void udc_note_draw(NvProg *pr, uint32_t raster_mask) {
int td = tbdr_on();
if (!td && !udc_on()) {
uint32_t cm = pr && pr->sh[1] ? pr->sh[1]->rf.color_mask : 0;
if (R.has_rt && cm >> 1) for (int i = 1; i < R.rt.n && i < 8; i++)
if ((cm >> i & 1) && ((S.chmask.mask >> (4 * i)) & 0xf) && rt_dup_slot(&R.rt, i))
RED_ONCE(79, "color slot %d holds the same texture as an earlier slot (ser=%llu) and this draw writes it (mask %#x): these writes are lost", i, (unsigned long long)R.rt.color[i]->serial, (S.chmask.mask >> (4 * i)) & 0xf);
return;
}
if (td && R.has_rt) for (int i = 0; i < R.rt.n && i < 8; i++) if (R.rt.color[i]) { zskip_del(R.rt.color[i]->serial); g_zskip_cand_ok[i] = 0; } // Zero-source set: once really drawn it is no longer all zeros
if (td && pr) for (int st = 0; st < 6; st++) { __unsafe_unretained NvShader *sh = pr->sh[st]; if (!sh) continue; for (int i = 0; i < sh->rf.nb; i++) if (sh->rf.b[i].kind == NVN_RB_IMG) { zskip_clear(); break; } } // Image slots are writable: conservatively clear
int image_write = 0;
uint32_t shader_color_mask = 0;
NvShader *fs = pr ? pr->sh[1] : nil;
uint32_t shader_color_mask = fs ? (fs->rf.color_known ? fs->rf.color_mask : UINT32_MAX) : 0;
if (pr) for (int st = 0; st < 6; st++) { __unsafe_unretained NvShader *sh = pr->sh[st]; if (!sh) continue;
if (st == 1) shader_color_mask = sh->rf.color_known ? sh->rf.color_mask : UINT32_MAX;
// A writable image, atomic image pointer or storage pointer may alias an attachment. Do not infer its identity from bound texture slots (bindless shaders need not use them).
for (int i = 0; i < sh->rf.nb; i++) { const nvn_rbind *b = &sh->rf.b[i];
if (b->rw && (b->kind == NVN_RB_IMG || b->kind == NVN_RB_SBUFP || b->kind == NVN_RB_SBUFU)) { image_write = 1; break; } }
Expand All @@ -4906,6 +4899,8 @@ static void udc_note_draw(NvProg *pr) {
RED_ONCE(79, "color slot %d holds the same texture as an earlier slot (ser=%llu) and this draw writes it (mask %#x): these writes are lost", i, (unsigned long long)R.rt.color[i]->serial, (S.chmask.mask >> (4 * i)) & 0xf);
for (int i = 0; i < g_nsd; i++) {
sd_att *t = &g_sd[i];
if (image_write) t->img = 1;
if (t->depth == 0 && t->idx >= 0 && (raster_mask >> t->idx & 1)) t->rast = 1;
if (image_write) { t->wrote = 1; t->wwhy = 1; }
else if (t->depth == 0 && t->idx >= 0 && (((S.chmask.mask >> (4 * t->idx)) & 0xf) || (shader_color_mask >> t->idx & 1))) { t->wrote = 1; t->wwhy = (S.chmask.mask >> (4 * t->idx)) & 0xf ? 2 : 3; }
else if (t->depth == 1 && S.ds.dw) { t->wrote = 1; t->wwhy = 4; }
Expand Down Expand Up @@ -7473,7 +7468,10 @@ static void sd_finish(void) {
&& !xv_disabled(xtag = xv_tag_pass(R.rt.color[t->idx], t->lv, t->idx, &R.rt));
int xvdead = xvcand && xv_trial_get(xtag, 0) >= XV_TRIAL; // Only after the trial period is the write-back really skipped
if (xvcand && !xvdead) xv_set(t->ser, t->lv, t->sl, XV_PROBE, xtag); // Trial period: write back as usual, only track the next touch
int dead = t->state == 2 || udc_dead || xvdead; MTLStoreAction sa = dead ? MTLStoreActionDontCare : MTLStoreActionStore;
// A loaded color tile with no raster output or clear holds only the old memory contents. Storing it can overwrite image writes to the same texture
// (Dark Souls' image-only AO pass). This is required for correctness even when store optimizations are disabled; mixed tile/image writers retain their store.
int img_dead = t->depth == 0 && t->load && t->img && !t->rast;
int dead = t->state == 2 || udc_dead || xvdead || img_dead; MTLStoreAction sa = dead ? MTLStoreActionDontCare : MTLStoreActionStore;
if (xvdead) { xv_set(t->ser, t->lv, t->sl, XV_INVALID, xtag); g_xv_dc++; }
if (t->depth == 1) [g_enc setDepthStoreAction:sa]; else if (t->depth == 2) [g_enc setStencilStoreAction:sa]; else [g_enc setColorStoreAction:sa atIndex:(NSUInteger)t->idx];
g_sd_n_all++; if (dead) { g_sd_n_dead++; g_sd_dead_bytes += t->bytes; g_bw_store -= t->bytes; if (udc_dead) g_udc_saved_bytes += t->bytes; }
Expand Down Expand Up @@ -9477,6 +9475,18 @@ static void cs_hoist_try(NvProg *pr0, const p_draw *dw0, const p_draw_idx *di0)
void mtl_geomesh_stats(uint64_t *n, uint64_t *wait) { *n = g_geo_mesh_n; *wait = g_geo_mesh_wait; }
void mtl_geohoist_stats(uint64_t *moved, uint64_t *runs, uint64_t *stops) { *moved = g_geo_hoisted; *runs = g_geo_hoist_runs; memset(stops, 0, 6 * sizeof *stops); stops[3] = g_geo_dependency_splits; }
void mtl_cshoist_stats(uint64_t *moved, uint64_t *depth, uint64_t *stops) { *moved = g_csh_moved; *depth = g_csh_depth; for (int i = 0; i < 8; i++) stops[i] = g_csh_stop[i]; }
// Physical color attachments the selected pipeline can write. A merged pass remaps logical outputs and channel masks; a specialized fragment's outputs are unknown here.
static uint32_t draw_color_writes(NvProg *pr) {
NvShader *fs = pr ? pr->sh[1] : nil;
if (!fs || S.rast_discard) return 0;
uint32_t outputs = g_pso_fsfn || ms_fs_override(fs) || !fs->rf.color_known ? UINT32_MAX : fs->rf.color_mask;
uint32_t mask = 0;
for (int i = 0; i < 8; i++) {
unsigned l = g_pso_lmap ? g_pso_lmap[i] : (unsigned)i;
if (l < 8 && ((S.chmask.mask >> (4 * l)) & 0xf) && (outputs >> l & 1)) mask |= 1u << i;
}
return mask;
}
static void draw_(const p_draw *dw, const p_draw_idx *di);
static void draw(const p_draw *dw, const p_draw_idx *di) { cp_stamp start = cp_begin(); draw_(dw, di); cp_end(5, start); }
static void draw_(const p_draw *dw, const p_draw_idx *di) {
Expand Down Expand Up @@ -9596,6 +9606,7 @@ static void draw_(const p_draw *dw, const p_draw_idx *di) {
g_sm_nwait++; PD("M %llu slice-merged pass: no amplified pipeline yet for vertex %s fragment %s; split here", g_stat_pass, pr->sh[0] ? pr->sh[0]->key : "-", pr->sh[1] ? pr->sh[1]->key : "-");
rp_end(); en = rp_begin_ex(mode); if (!en) return;
cur_ps = pso_get(pr, nil); cur_pull = g_pso_pull; cur_pr = pr; cur_ps_pass = g_pass.serial; g_pso_dirty = !cur_ps || cur_pull; }
uint32_t raster_mask = draw_color_writes(pr); // Capture the physical mapping before resetting the merged pipeline state.
if (g_pso_lmap) { g_pso_lmap = NULL; g_pso_fsfn = nil; memset(&g_pso_fz, 0, sizeof g_pso_fz); g_pso_dirty = 1; } // The next draw fetches again according to its own situation
__unsafe_unretained id<MTLRenderPipelineState> ps = cur_ps;
if (!ps) { PD("Z %llu draw skipped: no render pipeline (vertex %s fragment %s)", g_stat_pass, pr->sh[0] ? pr->sh[0]->key : "-", pr->sh[1] ? pr->sh[1]->key : "-"); return; }
Expand Down Expand Up @@ -9735,7 +9746,7 @@ static void draw_(const p_draw *dw, const p_draw_idx *di) {
#ifdef NVN_RT_RANGE_TEST
rr_snap(dw,di);
#endif
udc_note_draw(pr);
udc_note_draw(pr, raster_mask);
swrite_note(pr); order_note(pr);
if (g_geo_mesh.on) { geo_mesh_encode(en); return; }
if (g_geo_program) {
Expand Down Expand Up @@ -10600,7 +10611,7 @@ static void clear_tex_tri(id<MTLTexture> t, unsigned lv, unsigned l0, const p_cl
static void clr_note(int slot, int depth, int stencil) { // as udc_note_draw for a draw: the attachment was written (zero-source skip, unwritten-attachment store)
if (slot >= 0 && R.rt.color[slot]) { zskip_del(R.rt.color[slot]->serial); g_zskip_cand_ok[slot] = 0; }
for (int i = 0; i < g_nsd; i++) { sd_att *t = &g_sd[i];
if ((t->depth == 0 && slot >= 0 && t->idx == slot) || (t->depth == 1 && depth) || (t->depth == 2 && stencil)) { t->wrote = 1; t->wwhy = 2; } }
if ((t->depth == 0 && slot >= 0 && t->idx == slot) || (t->depth == 1 && depth) || (t->depth == 2 && stencil)) { t->wrote = t->rast = 1; t->wwhy = 2; } }
}
static uint64_t g_clr_tri[2]; // clears drawn as a triangle: colour, depth / stencil
static void clear_color_tri(const p_clear_color *c) {
Expand Down
3 changes: 3 additions & 0 deletions dm/gfx/tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,9 @@ check_t_attachment_capacity: t_attachment_capacity
@for compiler in 0 1; do for td in 0 1; do for enc in 0 1; do \
DM_PSO_MTL4=$$compiler DM_NVN_TBDR=$$td DM_NVN_ENC_THREAD=$$enc MTL_DEBUG_LAYER=1 ./t_attachment_capacity || exit $$?; \
done; done; done
t_image_attachment_store: t_image_attachment_store.m $(MTL_T_DEP)
clang $(CFLAGS) -fobjc-arc -o $@ t_image_attachment_store.m $(MTL_T_SRC) $(FW)
check_t_image_attachment_store: RUN_ENV = MTL_DEBUG_LAYER=1
t_unwritten_store: t_unwritten_store.m $(MTL_T_DEP)
clang $(CFLAGS) -fobjc-arc -o $@ t_unwritten_store.m $(MTL_T_SRC) $(FW)
check_t_unwritten_store: RUN_ENV = MTL_DEBUG_LAYER=1
Expand Down
Loading