From 7e558267fd48dc9131b44242ae729ccc3ae1c636 Mon Sep 17 00:00:00 2001 From: Matt Stanley <5354790+mstan@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:32:37 -0700 Subject: [PATCH 1/4] Bind reconstructed background maps and resources per source span --- CMakeLists.txt | 7 +- docs/ROW_RENDER_PLAN.md | 23 ++++ src/main.c | 6 +- src/mmx_background_span.inc | 202 ++++++++++++++++++++++++++++++++++++ src/mmx_render_assets.c | 30 ++++++ src/mmx_render_assets.h | 12 +++ src/mmx_row_plan.inc | 18 ++++ tests/mmx_row_plan_test.c | 107 +++++++++++++++++++ 8 files changed, 403 insertions(+), 2 deletions(-) create mode 100644 src/mmx_background_span.inc diff --git a/CMakeLists.txt b/CMakeLists.txt index e360a826..cedf3fa4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,11 @@ if(_MMX_RASTER_IMPL_DEFAULT STREQUAL "NATIVE" AND MMX_RASTER_IMPL STREQUAL "NATI set(_MMX_NATIVE_ROW_PLAN_DEFAULT ON) endif() option(MMX_NATIVE_ROW_PLAN "Prepare whole-view native row composition plans" ${_MMX_NATIVE_ROW_PLAN_DEFAULT}) +option(MMX_NATIVE_BG_SPANS "Bind reconstructed background tiles and palettes per source span" OFF) +set_property(SOURCE src/mmx_renderer_native.c tests/mmx_native_renderer_test.c + tests/mmx_row_plan_test.c src/main.c tests/mmx_adaptive_state_test.c + APPEND PROPERTY COMPILE_DEFINITIONS + MMX_NATIVE_BG_SPANS=$) set_property(SOURCE src/mmx_renderer_native.c tests/mmx_native_renderer_test.c src/main.c tests/mmx_adaptive_state_test.c APPEND PROPERTY COMPILE_DEFINITIONS @@ -62,7 +67,7 @@ set(SNESRECOMP_ENABLE_MODS ON CACHE BOOL "Shared mod package runtime" FORCE) # keeps the unmodified Japanese edition and small unit tests independent. set(SNESRECOMP_NET_ICE ON CACHE BOOL "Internet netplay transport" FORCE) include(${SNESRECOMP_ROOT}/runner/runner.cmake) -string(APPEND MMX_REPLAY_ENGINE "-apu${SNESRECOMP_APU_TIME_IMPL}-crc${SNESRECOMP_CRC32_IMPL}-raster${MMX_RASTER_IMPL}-row${MMX_NATIVE_ROW_PLAN}") +string(APPEND MMX_REPLAY_ENGINE "-apu${SNESRECOMP_APU_TIME_IMPL}-crc${SNESRECOMP_CRC32_IMPL}-raster${MMX_RASTER_IMPL}-row${MMX_NATIVE_ROW_PLAN}-bgspans${MMX_NATIVE_BG_SPANS}") set_property(SOURCE src/mmx_replay.c APPEND PROPERTY COMPILE_DEFINITIONS MMX_REPLAY_REVISION="${MMX_REPLAY_REVISION}" MMX_REPLAY_ENGINE="${MMX_REPLAY_ENGINE}") include(${RECOMP_UI_ROOT}/recomp_ui.cmake) diff --git a/docs/ROW_RENDER_PLAN.md b/docs/ROW_RENDER_PLAN.md index 975867da..7c920459 100644 --- a/docs/ROW_RENDER_PLAN.md +++ b/docs/ROW_RENDER_PLAN.md @@ -41,3 +41,26 @@ stock-renderer workload was neutral/inconclusive. These are additional results against the preceding optimized MMX build, not additive percentages or original Xbox qualification. Raw samples, exact binary identities and settings are in `campaign-results.json` beneath the evidence directory above. + +## Reconstructed background source spans (R1) + +`MMX_NATIVE_BG_SPANS=ON` additionally resolves full-view map tiles, source-row +planes and private CHR/palette bindings per source span. Its development default +is OFF pending built-game measurement and owner review. The old scalar +`background()` remains the independent reference and handles per-pixel imported +menu art and the Rush ceiling; mosaic retains its existing path. + +Spans end at native-view edges, source characters, scene reflections/clamps, +door cells, submarine bands and authored resource-phase changes. Resource +columns may advance forward, backward, at half-camera scale or remain clamped; +CHR and palette changes need not align with a tile. Bindings are private to a +captured row, with no new persistent cache, guest write or serialized state. + +The prototype passes existing native scenes, 1,536 scalar-compared full-width +rows, separate non-aligned CHR/palette phase checks, all four Magic Carpet +gates and eight captured view/HUD comparisons. The row suite also executes on +Linux. This is correctness evidence, not yet a performance claim or owner +approval. Current-build state and matched built-game performance qualification +remain pending at this checkpoint. Tracking: `beads-8wg.1.150`, roadmap +`beads-49cy`, campaign `beads-2wy8.1`. Evidence: +`G:/Temp/Codex/snesrecomp/background-spans-20261027/evidence`. diff --git a/src/main.c b/src/main.c index e2fcedb9..8f386d26 100644 --- a/src/main.c +++ b/src/main.c @@ -43,6 +43,9 @@ #ifndef MMX_DESKTOP_ENTRY #define MMX_DESKTOP_ENTRY main #endif +#ifndef MMX_NATIVE_BG_SPANS +#define MMX_NATIVE_BG_SPANS 0 +#endif #ifndef MMX_NATIVE_ROW_PLAN #define MMX_NATIVE_ROW_PLAN 0 #endif @@ -252,11 +255,12 @@ static void MmxAfterFrame(const SnesDesktopHostFrameStats *stats) { double seconds = stats->run_seconds; printf("SNESRECOMP_BENCHMARK {\"game\":\"Mega Man X\",\"frames\":%u," "\"seconds\":%.9f,\"fps\":%.3f,\"ms_per_frame\":%.6f,\"native_raster_lines\":%llu," - "\"apu_time_impl\":\"%s\",\"raster_impl\":\"%s\",\"row_plan\":%s,\"audio_output\":%s}\n", + "\"apu_time_impl\":\"%s\",\"raster_impl\":\"%s\",\"row_plan\":%s,\"background_spans\":%s,\"audio_output\":%s}\n", stats->frame, seconds, stats->frame / seconds, seconds * 1000.0 / stats->frame,(unsigned long long)s_benchmark_native_lines, apu_time_implementation(), MMX_NATIVE_RASTER ? "NATIVE" : "LLE", MMX_NATIVE_RASTER && MMX_NATIVE_ROW_PLAN ? "true" : "false", + MMX_NATIVE_RASTER && MMX_NATIVE_ROW_PLAN && MMX_NATIVE_BG_SPANS ? "true" : "false", s_benchmark_audio ? "true" : "false"); } } diff --git a/src/mmx_background_span.inc b/src/mmx_background_span.inc new file mode 100644 index 00000000..7ab83127 --- /dev/null +++ b/src/mmx_background_span.inc @@ -0,0 +1,202 @@ +/* Full-view background source spans. The scalar background() above remains + * the independent build/reference path. All bindings belong to this captured + * row; no persistent cache or live machine state is introduced. */ +static void background_span_cut(unsigned *count, int coordinate, int step, int edge) { + int distance = step > 0 && edge > coordinate ? (edge - coordinate + step - 1) / step : + step < 0 && edge <= coordinate ? (coordinate - edge) / -step + 1 : 0; + if (distance > 0 && (unsigned)distance < *count) *count = (unsigned)distance; +} + +static unsigned background_source_span(const Ppu *p, const Raster *r, + unsigned layer, int x, int y, bool stage, unsigned count, + uint16_t *pixels, int *colors) { + static const unsigned low[] = {8, 7, 1}, high[] = {12, 11, 3}; + const unsigned stage_id = frame.ram[0x1f7a]; + /* Menus have per-pixel imported icons. The Rush ceiling is a separately + * reconstructed image, not a tiled source binding. */ + if (!stage || (layer == 0 && stage_id == 4 && + (frame_rush.mode == MMX_RUSH_PLAYING || frame_rush.mode == MMX_RUSH_PREPARING))) { + for (unsigned i = 0; i < count; ++i) + pixels[i] = background(p, r, layer, x + (int)i, y, stage, &colors[i]); + return count; + } + background_span_cut(&count, x, 1, 0); + background_span_cut(&count, x, 1, 256); + bool margin = x < 0 || x >= 256; + bool water = stage_id == 1 && (p->screenEnabled[0] & 4) && + !(p->screenEnabled[1] & 4) && (p->cgwsel & 2) && (p->cgadsub & 0x44) == 0x44; + if (layer == 2 && margin && !water) { + bool blizzard = frame.ram[0x1f3f] && stage_id == 8; + if (frame.ram[0x1f3f] && frame_rush.mode == MMX_RUSH_PLAYING) + for (unsigned i = 0; i < 2; ++i) + blizzard |= frame_rush.bosses[i].id == 0 && frame_rush.bosses[i].phase != 0; + if (!blizzard) { memset(pixels, 0, count * sizeof(*pixels)); return count; } + } + bool slime_surface = layer == 1 && stage_id == 6 && frame.ram[0x1e89] == 0x0c; + bool lower_shaft = false; + unsigned bpp = layer == 2 ? 2 : 4, size = PPU_bigTiles(p, layer) ? 16 : 8; + int px = (x + p->hScroll[layer]) & 1023, py = (y + p->vScroll[layer]) & 1023; + if (layer == 2 && water) py = (py + view_dy) & 1023; + unsigned sc = p->bgXsc[layer], tx = px / size, ty = py / size; + unsigned a = (sc & 0xfc) * 256 + (tx & 31) + (ty & 31) * 32; + if ((sc & 1) && (tx & 32)) a += 1024; + if ((sc & 2) && (ty & 32)) a += (sc & 1) ? 2048 : 1024; + uint16_t tile = r->vram[a & 0x7fff]; + int step = 1, asset_x = -1, asset_step = 0; + if (size == 8 && layer < 2 && !slime_surface && + (margin || view_dx || view_dy || airport_upper_sky)) { + int wx, wy; + if (layer == 0) { + wx = MmxDisplay_ExpandStageScroll((uint16_t)word(frame.ram, 0x1e4d), p->hScroll[0]) + view_dx + x; + wy = MmxDisplay_ExpandStageScroll((uint16_t)word(frame.ram, 0x1e50), p->vScroll[0]) + view_dy + y; + if (stage_id == 3 && word(frame.ram, 0x1e4d) == 0x1f00 && + word(frame.ram, 0x1e50) >= 0x500 && word(frame.ram, 0x1e50) <= 0x600 && wy >= 0x600) { + wx -= 0x1e00; wy += 0x200; lower_shaft = true; + } + for (unsigned i = 0; i < submarine_count; ++i) { + const SubmarineBody *body = &submarine_bodies[i]; + if (body->scrolling && (p->vScroll[0] & 1023) == (word(frame.ram, 0xc4) & 1023)) { + background_span_cut(&count, wx, step, body->left); + background_span_cut(&count, wx, step, body->left + 160); + if (wx < body->left || wx >= body->left + 160) wy += body->shift; + break; + } + } + } else { + int stream_x = (int16_t)word(frame.ram, 0x1e8d), stream_y = (int16_t)word(frame.ram, 0x1e90); + wx = stream_x + (((p->hScroll[1] - stream_x + 512) & 1023) - 512) + x; + wy = stream_y + (((p->vScroll[1] - stream_y + 512) & 1023) - 512) + y; + unsigned mode = frame.ram[0x1e89]; + int nx = word(frame.ram, 0x1e4d), ny = word(frame.ram, 0x1e50); + if (mode == 2 || mode == 14) wx += ((nx + view_dx) >> 1) - (nx >> 1); + else if (mode == 4 || mode == 16) wx += view_dx; + if (mode == 2 || mode == 16) wy += ((ny + view_dy) >> 1) - (ny >> 1); + else if (mode == 4 || mode == 14) wy += view_dy; + if (stage_id == 1 && mode == 0x0c) { + background_span_cut(&count, wx, step, 0xb00); + background_span_cut(&count, wx, step, 0xc00); + if (wx < 0xb00 || wx >= 0xc00 || wy < 0 || wy >= 256) { + memset(pixels, 0, count * sizeof(*pixels)); return count; + } + } + if (stage_id == 1 && mode == 0x0e && wy >= 0x370 && wy < 0x390) { + background_span_cut(&count, wx, step, 0x500); + background_span_cut(&count, wx, step, 0x700); + if (wx >= 0x500 && wx < 0x700) wx = 0x700 | (wx & 255); + } + if (stage_id == 1 && mode == 0x0e && stream_x >= 0xc00 && stream_x < 0xe00) { + background_span_cut(&count, wx, step, 0xc00); + if (wx < 0xc00) { wx = 0xc00; step = 0; } + } + if (stage_id == 0 && stream_x >= 0xa00) { + background_span_cut(&count, wx, step, 0xa00); + if (wx < 0xa00) { wx = 0xa00; step = 0; } + } + if (airport_upper_sky) { wx = 0x300 + ((unsigned)wx & 255); wy = (unsigned)wy & 255; step = 1; } + } + background_span_cut(&count, wx, step, 0); + if (wx < 0) { wx = -wx - 1; step = -step; } + if (layer == 1 && airport_sky_width) { + background_span_cut(&count, wx, step, airport_sky_width); + if (wx >= airport_sky_width) { + wx %= 2 * airport_sky_width; + background_span_cut(&count, wx, step, 0); + background_span_cut(&count, wx, step, airport_sky_width); + background_span_cut(&count, wx, step, 2 * airport_sky_width); + if (wx >= airport_sky_width) { wx = 2 * airport_sky_width - wx - 1; step = -step; } + } + } + if (layer == 0) { + if (stage_id < 13 && rom_size > 0x30d24 + stage_id * 3 + 2) { + const uint8_t *t = rom + 0x30d24 + stage_id * 3; + const uint8_t *bounds = rom_at(word(t, 0) | (t[2] << 16), 2); + if (bounds && bounds[0] && bounds[0] <= 32) { + int width = bounds[0] * 256; + background_span_cut(&count, wx, step, width); + if (wx >= width) { wx = width * 2 - wx - 1; step = -step; } + background_span_cut(&count, wx, step, 0); + if (wx < 0) { wx = 0; step = 0; } + } + } + for (unsigned i = 0; i < submarine_count; ++i) { + const SubmarineBody *body = &submarine_bodies[i]; + if (body->hidden && wy >= body->top && wy < body->bottom) { + background_span_cut(&count, wx, step, body->left); + background_span_cut(&count, wx, step, body->left + 160); + if (wx >= body->left && wx < body->left + 160) { wx -= 256; break; } + } + } + /* Door ownership changes only at a 16px map cell. Eight-pixel source + * boundaries also cover that transition, for either traversal direction. */ + unsigned cell = step < 0 ? ((unsigned)wx & 7) + 1 : step > 0 ? 8 - ((unsigned)wx & 7) : count; + if (cell < count) count = cell; + if (door_body(wx, wy)) { + bool left = door_body(wx - 16, wy), right = door_body(wx + 16, wy); + if (left || right) { + int boundary = (wx & ~15) + (right ? 16 : 0); + bool view_left = view_camera(frame.ram, 0x1e4d) + 128 < boundary; + if (view_left && left) wx += 16; + if (!view_left && right) wx -= 16; + } + } + } + background_span_cut(&count, wx, step, 0); + background_span_cut(&count, wx, step, 8192); + unsigned cell = step < 0 ? ((unsigned)wx & 7) + 1 : step > 0 ? 8 - ((unsigned)wx & 7) : count; + if (cell < count) count = cell; + uint16_t mapped; + if (stage_tile_for_draw(layer, wx, wy, &mapped)) { + tile = mapped; px = wx; py = wy; + bool half = layer == 1 && (stage_id <= 2 || stage_id == 4); + asset_x = half ? wx * 2 - 128 : wx; + asset_step = half ? step * 2 : step; + if (layer == 1 && stage_id == 1 && frame.ram[0x1e89] == 0x0e && word(frame.ram, 0x1e8d) >= 0xc00) { + background_span_cut(&count, asset_x, asset_step, 0x1a20); + if (asset_x >= 0x1a20) { asset_x = 0x1a1f; asset_step = 0; } + } + if (layer == 1 && (stage_id == 8 || (stage_id == 0 && frame.ram[0x1e89] == 0x0c))) { + asset_x = -1; asset_step = 0; + } + } else { + if (airport_upper_sky) { memset(pixels, 0, count * sizeof(*pixels)); return count; } + step = 1; /* Unmapped source retains the original circular VRAM binding. */ + } + } + unsigned cell = step < 0 ? ((unsigned)px & 7) + 1 : step > 0 ? 8 - ((unsigned)px & 7) : count; + if (cell < count) count = cell; + int cx = px & (size - 1), cy = py & (size - 1); + if (tile & 0x4000) cx = size - 1 - cx; + if (tile & 0x8000) cy = size - 1 - cy; + unsigned number = ((tile & 1023) + cx / 8 + cy / 8 * 16) & 1023; + unsigned address = (PPU_bgTileAdr(p, layer) + number * bpp * 4) & 0x7fff; + const uint8_t *bits = NULL; + const MmxBackgroundPalette *palette = NULL; + if (g_mmx_render_asset_repairs) { + MmxBackgroundBinding binding; + count = MmxRenderAssetsBindBackground(frame.ram, asset_x, asset_step, address, count, + bpp == 4 && !(stage_id == 1 && layer == 1), lower_shaft ? 1 : -1, &binding); + bits = binding.tile; palette = binding.palette; + } + unsigned p01, p23 = 0; + if (bits) { + bits += (cy & 7) * 2; + p01 = bits[0] | ((unsigned)bits[1] << 8); + p23 = bits[16] | ((unsigned)bits[17] << 8); + } else { + unsigned row_address = (address + (cy & 7)) & 0x7fff; + p01 = r->vram[row_address]; + if (bpp == 4) p23 = r->vram[(row_address + 8) & 0x7fff]; + } + unsigned priority = tile & 0x2000 ? (layer == 2 && (p->bgmode & 8) ? 15 : high[layer]) : low[layer]; + unsigned base = (((tile >> 10) & 7) << bpp); + unsigned prefix = (priority << 12) | (layer << 8) | base; + int bit = 7 - (cx & 7), bit_step = (tile & 0x4000) ? step : -step; + for (unsigned i = 0; i < count; ++i, bit += bit_step) { + unsigned pixel = ((p01 >> bit) & 1) | (((p01 >> (bit + 8)) & 1) << 1); + if (bpp == 4) pixel |= (((p23 >> bit) & 1) << 2) | (((p23 >> (bit + 8)) & 1) << 3); + pixels[i] = pixel ? (uint16_t)(prefix | pixel) : 0; + unsigned index = base | pixel; + if (pixel && palette && palette->valid[index]) colors[i] = palette->colors[index]; + } + return count; +} diff --git a/src/mmx_render_assets.c b/src/mmx_render_assets.c index edea6345..043e415f 100644 --- a/src/mmx_render_assets.c +++ b/src/mmx_render_assets.c @@ -900,3 +900,33 @@ unsigned MmxRenderAssetsDeathPaletteFade(const uint8_t ram[0x20000], const uint1 } return 0; } + +unsigned MmxRenderAssetsBindBackground(const uint8_t ram[0x20000], + int world_x, int step, unsigned vram_word, unsigned count, bool use_chr, + int palette_phase, MmxBackgroundBinding *binding) { + if (!binding || !ram || !count || count > 1024 || step < -2 || step > 2) return 0; + binding->tile = use_chr ? MmxRenderAssetsBackgroundTile(ram, world_x, vram_word) : NULL; + binding->palette = palette_phase >= 0 ? + MmxRenderAssetsBackgroundPalettePhase(ram, (unsigned)palette_phase) : + MmxRenderAssetsBackgroundPalette(ram, world_x); + if (!step || count == 1) return count; + unsigned stage = ram[0x1f7a]; + bool chr = use_chr && (stage == 0 || stage == 1); + bool palette = palette_phase < 0 && + (stage == 0 || stage == 1 || stage == 2 || stage == 4 || stage == 8); + if ((!chr && !palette) || !prepare_background(ram)) return count; + /* Resource events need not share a tile boundary. Check their prepared + * column phases, without repeating map/CHR/palette resolution per pixel. + * An invalid column is a distinct binding so crossing into/out of ROM's + * authored 8192px resource range also ends the span. */ + bool valid = world_x >= 0 && world_x < 8192; + unsigned chr_phase = valid ? bg_phase[0][world_x] : 16; + unsigned pal_phase = valid ? bg_phase[1][world_x] : 16; + for (unsigned i = 1; i < count; ++i) { + int64_t next = (int64_t)world_x + (int64_t)i * step; + bool next_valid = next >= 0 && next < 8192; + if ((chr && (next_valid ? bg_phase[0][next] : 16) != chr_phase) || + (palette && (next_valid ? bg_phase[1][next] : 16) != pal_phase)) return i; + } + return count; +} diff --git a/src/mmx_render_assets.h b/src/mmx_render_assets.h index 1f5a6492..3e8a5bcc 100644 --- a/src/mmx_render_assets.h +++ b/src/mmx_render_assets.h @@ -51,3 +51,15 @@ const uint8_t *MmxRenderAssetsBackgroundTile(const uint8_t ram[0x20000], const MmxBackgroundPalette *MmxRenderAssetsBackgroundPalette(const uint8_t ram[0x20000], int world_x); const MmxBackgroundPalette *MmxRenderAssetsBackgroundPalettePhase(const uint8_t ram[0x20000], unsigned phase); + +/* One captured tile's immutable CHR/palette binding. The returned count ends + * before a resource-phase change. step is -2..2 authored columns per pixel; + * count is 1..1024. Zero rejects invalid arguments. palette_phase=-1 follows + * authored columns; a nonnegative phase binds an explicit continuation. */ +typedef struct MmxBackgroundBinding { + const uint8_t *tile; + const MmxBackgroundPalette *palette; +} MmxBackgroundBinding; +unsigned MmxRenderAssetsBindBackground(const uint8_t ram[0x20000], + int world_x, int step, unsigned vram_word, unsigned count, bool use_chr, + int palette_phase, MmxBackgroundBinding *binding); diff --git a/src/mmx_row_plan.inc b/src/mmx_row_plan.inc index 0e22c6e5..7f26c1eb 100644 --- a/src/mmx_row_plan.inc +++ b/src/mmx_row_plan.inc @@ -2,6 +2,9 @@ * captured row/view. This pipeline never reads or writes the live machine. */ #include "snes/raster_plan.h" #include +#if MMX_NATIVE_BG_SPANS +#include "mmx_background_span.inc" +#endif static void native_background_range(const Ppu *p, const Raster *r, unsigned layer, int y, bool stage, int extra, unsigned begin, unsigned end, @@ -9,6 +12,21 @@ static void native_background_range(const Ppu *p, const Raster *r, int by = y; unsigned mosaic = (p->mosaic & (1u << layer)) ? (p->mosaic >> 4) + 1 : 0; if (mosaic) by -= by % (int)mosaic; +#if MMX_NATIVE_BG_SPANS + if (!mosaic) { + for (unsigned sx = begin; sx < end;) { + unsigned count = background_source_span(p, r, layer, (int)sx - extra, + by, stage, end - sx, pixels + sx, colors + sx); + assert(count && count <= end - sx); + if (!count || count > end - sx) { + pixels[sx] = background(p, r, layer, (int)sx - extra, by, stage, &colors[sx]); + count = 1; + } + sx += count; + } + return; + } +#endif for (unsigned sx = begin; sx < end; ++sx) { int x = (int)sx - extra; if (mosaic) x -= ((x % (int)mosaic) + (int)mosaic) % (int)mosaic; diff --git a/tests/mmx_row_plan_test.c b/tests/mmx_row_plan_test.c index 0c4109ff..46ef4733 100644 --- a/tests/mmx_row_plan_test.c +++ b/tests/mmx_row_plan_test.c @@ -97,7 +97,114 @@ static void composition_rows(void) { } } + +#if MMX_NATIVE_BG_SPANS +static void background_span_resource_edges(void) { + background_resources(); /* Existing independent CHR/palette values. */ + ram[0x1f7a] = 0; + /* Put CHR and palette changes on different, non-tile-aligned columns. */ + rom_bytes[0x29005]=1; rom_bytes[0x2900c]=1; + rom_word(0x29006, 0x0853); rom_word(0x2900d, 0x8855); + MmxRenderAssetsSetRom(NULL,0); MmxRenderAssetsSetRom(rom_bytes,sizeof(rom_bytes)); + MmxBackgroundBinding edge; + assert(MmxRenderAssetsBindBackground(ram,0x850,1,0x3500,17,true,-1,&edge)==3); + assert(MmxRenderAssetsBindBackground(ram,0x854,1,0x3500,17,true,-1,&edge)==1); + assert(MmxRenderAssetsBindBackground(ram,0x857,-1,0x3500,17,true,-1,&edge)==3); + assert(MmxRenderAssetsBindBackground(ram,0x850,1,0x3500,17,true,1,&edge)==5); + const int starts[] = {-5,-1,0,1,0x84d,0x852,0x853,0x854,0x855,0x856,8190,8191,8192,8196}; + for (unsigned s = 0; s < sizeof(starts)/sizeof(*starts); ++s) + for (int step = -2; step <= 2; ++step) + for (unsigned fixed = 0; fixed < 2; ++fixed) { + MmxBackgroundBinding binding; + unsigned count = MmxRenderAssetsBindBackground(ram,starts[s],step,0x3500,17,true, + fixed ? 1 : -1,&binding); + assert(count && count <= 17); + for (unsigned i = 0; i < count; ++i) { + int x = starts[s]+(int)i*step; + assert(binding.tile == MmxRenderAssetsBackgroundTile(ram,x,0x3500)); + const MmxBackgroundPalette *pal = fixed ? MmxRenderAssetsBackgroundPalettePhase(ram,1) : + MmxRenderAssetsBackgroundPalette(ram,x); + assert(binding.palette == pal); + } + } +} +static void background_span_rows(void) { + static uint16_t actual[MMX_RENDER_MAX_WIDTH], expected[MMX_RENDER_MAX_WIDTH]; + static int colors[MMX_RENDER_MAX_WIDTH], reference_colors[MMX_RENDER_MAX_WIDTH]; + const int cameras[] = {0, 1, 127, 255, 256, 0x500, 0x700, 0xaff, 0xb00, + 0xbff, 0xc00, 0x1eff, 0x1f00, 0x2000}; + /* Valid random source maps retain authored coordinate/table structure. + * Resource-phase edge cases are also exercised by the existing scene suite. */ + MmxRendererReset(); MmxRendererSetRom(NULL, 0); + memset(rom_bytes, 0, sizeof(rom_bytes)); + for (unsigned i = 0; i < 0x10000; ++i) rom_bytes[i] = (uint8_t)row_random(); + for (unsigned i = 0x8000; i < 0x10000; ++i) rom_bytes[i] = (uint8_t)row_random(); + MmxRendererSetRom(rom_bytes, sizeof(rom_bytes)); + memset(&frame_rush, 0, sizeof(frame_rush)); + memset(&frame_weapons, 0, sizeof(frame_weapons)); + Raster raster = {0}; raster.vram = ppu.vram; + for (unsigned i = 0; i < 0x8000; ++i) ppu.vram[i] = (uint16_t)row_random(); + for (unsigned i = 0; i < 0x20000; ++i) frame.ram[i] = (uint8_t)row_random(); + /* Point both map descriptor tables into valid, randomized ROM banks. */ + frame.ram[0xb95] = frame.ram[0xb98] = 0; + frame.ram[0xb96] = frame.ram[0xb99] = 0x80; + frame.ram[0xb97] = frame.ram[0xb9a] = 0x80; + for (unsigned sample = 0; sample < 1536; ++sample) { + unsigned layer = sample % 3, stage = (sample / 3) % 13; + frame.ram[0x1f7a] = (uint8_t)stage; + frame.ram[0x1e89] = (uint8_t)((sample / 39) % 9 * 2); + frame.ram[0x1f3f] = (uint8_t)(sample & 1); + frame.ram[0x1f96] = (uint8_t)(sample & 0x40); + for (unsigned c = 0; c < 4; ++c) { + unsigned address = (unsigned[]){0x1e4d, 0x1e50, 0x1e8d, 0x1e90}[c]; + unsigned value = (unsigned)cameras[(sample / (c + 1)) % (sizeof(cameras)/sizeof(*cameras))]; + frame.ram[address] = (uint8_t)value; frame.ram[address+1] = (uint8_t)(value >> 8); + } + /* Some foreground views genuinely straddle the authored right edge. */ + rom_long(0x30d24 + stage * 3, 0x81f000); + rom_bytes[0xf000] = (uint8_t)(1 + sample % 32); + ppu.bgmode = (uint8_t)(1 | ((sample % 17 == 0) ? 0x70 : 0)); + ppu.bgTileAdr = (uint16_t)row_random(); + ppu.bgXsc[layer] = (uint8_t)row_random(); + ppu.hScroll[layer] = (uint16_t)(row_random() & 1023); + ppu.vScroll[layer] = (uint16_t)(row_random() & 1023); + ppu.screenEnabled[0] = (uint8_t)(sample & 1 ? 7 : 3); + ppu.screenEnabled[1] = 0; ppu.cgwsel = 2; ppu.cgadsub = 0x44; + view_dx = sample & 2 ? cameras[sample % (sizeof(cameras)/sizeof(*cameras))] - 256 : 0; + view_dy = sample & 4 ? (int)(sample % 1024) - 512 : 0; + airport_upper_sky = stage == 5 && (sample & 8); + airport_sky_width = stage == 5 && !airport_upper_sky ? 640 : 0; + submarine_count = stage == 1 ? 2 : 0; + submarine_bodies[0] = (SubmarineBody){0xb00, 0x100, 0x180, 0x28, true, true}; + submarine_bodies[1] = (SubmarineBody){0x700, 0x200, 0x280, 0x20, true, false}; + unsigned width = (unsigned[]){256,342,682,1024}[sample % 4]; + int extra = (int)(width - 256) / 2, y = (int)(sample % 1024); + g_mmx_render_asset_repairs = (sample & 1) != 0; + for (unsigned i = 0; i < width; ++i) colors[i] = reference_colors[i] = -1; + memset(door_cache, 0, sizeof(door_cache)); + for (unsigned i = 0; i < 2; ++i) stage_tile_cache[i].key = ~0u; + for (unsigned i = 0; i < width; ++i) + expected[i] = background(&ppu, &raster, layer, (int)i - extra, y, true, &reference_colors[i]); + for (unsigned i = 0; i < width;) { + unsigned count = background_source_span(&ppu, &raster, layer, (int)i - extra, + y, true, width - i, actual + i, colors + i); + assert(count && count <= width - i); i += count; + } + for (unsigned i = 0; i < width; ++i) if (actual[i] != expected[i] || colors[i] != reference_colors[i]) { + fprintf(stderr,"background span mismatch sample=%u stage=%u layer=%u x=%d y=%d pixel=%04x/%04x color=%d/%d\n", + sample,stage,layer,(int)i-extra,y,actual[i],expected[i],colors[i],reference_colors[i]); abort(); + } + } + MmxRendererReset(); MmxRendererSetRom(NULL, 0); + puts("BACKGROUND SOURCE SPANS: 1536 SCALAR-COMPARED FULL-WIDTH ROWS PASS"); +} +#endif + int main(void) { +#if MMX_NATIVE_BG_SPANS + background_span_resource_edges(); + background_span_rows(); +#endif existing_native_suite(); composition_rows(); puts("WHOLE-VIEW ROW PLAN: NATIVE SCENES AND 512 RANDOMIZED COMPOSITION ROWS PASS"); From 725377b847969ee2dc371bfb7cb9cf496ea091b1 Mon Sep 17 00:00:00 2001 From: Matt Stanley <5354790+mstan@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:47:40 -0700 Subject: [PATCH 2/4] Verify render width and draw count do not change rollback replay --- CMakeLists.txt | 9 ++++++++ docs/ROW_RENDER_PLAN.md | 9 +++++--- tests/mmx_adaptive_state_test.c | 40 +++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cedf3fa4..3571d886 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -353,6 +353,15 @@ if(TARGET mmx_state_tests) tests/mmx_adaptive_state_test.c ${mmx_state_sources}) if(BUILD_TESTING AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/mmx.sfc") set(rush_report_dir "${CMAKE_CURRENT_BINARY_DIR}/boss-rush-reports") + set(render_rollback_dir "${CMAKE_CURRENT_BINARY_DIR}/render-rollback") + file(MAKE_DIRECTORY "${render_rollback_dir}") + add_test(NAME mmx_render_rollback + COMMAND ${CMAKE_COMMAND} -E env + SDL_AUDIODRIVER=dummy SDL_VIDEODRIVER=dummy MMX_REPLAY_RECORD=0 + MMX_RENDER_ROLLBACK_TEST=1 + $ "${CMAKE_CURRENT_SOURCE_DIR}/mmx.sfc") + set_tests_properties(mmx_render_rollback PROPERTIES + WORKING_DIRECTORY "${render_rollback_dir}" LABELS "rom;rollback;renderer") file(MAKE_DIRECTORY "${rush_report_dir}") add_test(NAME mmx_boss_rush_reports COMMAND ${CMAKE_COMMAND} -E env SDL_AUDIODRIVER=dummy MMX_REPLAY_RECORD=0 diff --git a/docs/ROW_RENDER_PLAN.md b/docs/ROW_RENDER_PLAN.md index 7c920459..b41b625a 100644 --- a/docs/ROW_RENDER_PLAN.md +++ b/docs/ROW_RENDER_PLAN.md @@ -59,8 +59,11 @@ captured row, with no new persistent cache, guest write or serialized state. The prototype passes existing native scenes, 1,536 scalar-compared full-width rows, separate non-aligned CHR/palette phase checks, all four Magic Carpet gates and eight captured view/HUD comparisons. The row suite also executes on -Linux. This is correctness evidence, not yet a performance claim or owner -approval. Current-build state and matched built-game performance qualification -remain pending at this checkpoint. Tracking: `beads-8wg.1.150`, roadmap +Linux. Both clean-source OFF/ON builds pass current-build restore, rewind, +deterministic replay and fresh-process replay. A dedicated ROM-backed rollback +check also compares identical guest inputs while varying render widths and +draw counts; the resulting rollback state is identical. This is correctness +evidence, not yet a performance claim or owner approval. Matched built-game +performance qualification remains pending. Tracking: `beads-8wg.1.150`, roadmap `beads-49cy`, campaign `beads-2wy8.1`. Evidence: `G:/Temp/Codex/snesrecomp/background-spans-20261027/evidence`. diff --git a/tests/mmx_adaptive_state_test.c b/tests/mmx_adaptive_state_test.c index 43ed1258..d98b0dbf 100644 --- a/tests/mmx_adaptive_state_test.c +++ b/tests/mmx_adaptive_state_test.c @@ -1863,6 +1863,43 @@ static void zero_state_checks(const char *assets, const char *fixture, uint8 *st puts("MMX ZERO RUNTIME CHECKS PASSED"); } #include "mmx_frame_cost_test.inc" +static void render_rollback_checks(uint8 *start, uint8 *expected, uint8 *actual, size_t cap) { + hitbox_boot(); + unsigned waited; + for (waited = 0; waited < 600; ++waited) { + if (g_ram[0xba9] == 2 && !g_ram[0xbaa] && (g_ram[0xbd3] & 4) && + (g_ram[0xbcf] & 127)) break; + frame(0); + } + check(waited < 600, "render rollback starts on real Highway ground"); + g_config.widescreen = true; + int width, height; MmxPrepareFrame(1280, 720, &width, &height); + check(g_mmx_custom_renderer, "render rollback uses custom compositor"); + frame(0); + uint32_t *pixels = malloc((size_t)MMX_RENDER_MAX_WIDTH * 224 * sizeof(*pixels)); + check(pixels != NULL, "render rollback buffer allocates"); + size_t n = RtlRollbackSaveToMemory(start, cap); + check(n != 0, "render rollback saves initial state"); + const MmxRenderView views[] = {{342,43,16.0/9.0},{682,213,32.0/9.0},{256,0,4.0/3.0}}; + for (unsigned i = 0; i < 16; ++i) { + frame((i < 8 ? SNES_PAD_RIGHT : 0) | SNES_PAD_Y); + check(MmxRendererDraw(pixels, views[0], true), "initial branch renders widescreen"); + } + size_t en = RtlRollbackSaveToMemory(expected, cap); + check(RtlRollbackLoadFromMemory(start, n), "render rollback restores initial state"); + size_t an = RtlRollbackSaveToMemory(actual, cap); + same(start, n, actual, an, "render rollback complete immediate restore"); + for (unsigned i = 0; i < 16; ++i) { + frame((i < 8 ? SNES_PAD_RIGHT : 0) | SNES_PAD_Y); + for (unsigned view = 0; view < 3; ++view) + check(MmxRendererDraw(pixels, views[view], true), "resimulation renders alternate width/count"); + } + an = RtlRollbackSaveToMemory(actual, cap); + same(expected, en, actual, an, "render width/count leaves rollback replay identical"); + free(pixels); + puts("MMX RENDER ROLLBACK CHECKS PASSED"); +} + int main(int argc, char **argv) { check(argc == 2 || argc == 3, "ROM supplied"); SDL_SetMainReady(); @@ -1958,6 +1995,9 @@ int main(int argc, char **argv) { MkDir("saves"); size_t cap = 2u * 1024u * 1024u; uint8 *start = malloc(cap), *expected = malloc(cap), *actual = malloc(cap); + if (getenv("MMX_RENDER_ROLLBACK_TEST")) { + render_rollback_checks(start, expected, actual, cap); return 0; + } if(getenv("MMX_ZERO_BLOCKS_TEST")) {zero_blocks_checks(getenv("MMX_ZERO_TEST_ASSETS"));return 0;} if (getenv("MMX_FRAME_COST_TEST")) { frame_cost_checks(start, expected, actual, cap); From ef821a32ce7ccb8d65b8ec15795710cfb4eaec84 Mon Sep 17 00:00:00 2001 From: Matt Stanley <5354790+mstan@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:49:52 -0700 Subject: [PATCH 3/4] Exercise source spans in the ROM-free row regression target --- CMakeLists.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3571d886..e670a2a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,9 +52,12 @@ endif() option(MMX_NATIVE_ROW_PLAN "Prepare whole-view native row composition plans" ${_MMX_NATIVE_ROW_PLAN_DEFAULT}) option(MMX_NATIVE_BG_SPANS "Bind reconstructed background tiles and palettes per source span" OFF) set_property(SOURCE src/mmx_renderer_native.c tests/mmx_native_renderer_test.c - tests/mmx_row_plan_test.c src/main.c tests/mmx_adaptive_state_test.c + src/main.c tests/mmx_adaptive_state_test.c APPEND PROPERTY COMPILE_DEFINITIONS MMX_NATIVE_BG_SPANS=$) +# Exercise the candidate in ROM-free CI even when the product selects its floor. +set_property(SOURCE tests/mmx_row_plan_test.c APPEND PROPERTY COMPILE_DEFINITIONS + MMX_NATIVE_BG_SPANS=1) set_property(SOURCE src/mmx_renderer_native.c tests/mmx_native_renderer_test.c src/main.c tests/mmx_adaptive_state_test.c APPEND PROPERTY COMPILE_DEFINITIONS From 90916f7a73f070138043e47fb17b6f799aacd340 Mon Sep 17 00:00:00 2001 From: Matt Stanley <5354790+mstan@users.noreply.github.com> Date: Wed, 28 Oct 2026 12:55:33 -0700 Subject: [PATCH 4/4] Document qualified background batching and shared-engine scope --- docs/ROW_RENDER_PLAN.md | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/ROW_RENDER_PLAN.md b/docs/ROW_RENDER_PLAN.md index b41b625a..a181a5f6 100644 --- a/docs/ROW_RENDER_PLAN.md +++ b/docs/ROW_RENDER_PLAN.md @@ -46,7 +46,7 @@ Xbox qualification. Raw samples, exact binary identities and settings are in `MMX_NATIVE_BG_SPANS=ON` additionally resolves full-view map tiles, source-row planes and private CHR/palette bindings per source span. Its development default -is OFF pending built-game measurement and owner review. The old scalar +remains OFF by owner direction. The old scalar `background()` remains the independent reference and handles per-pixel imported menu art and the Rush ceiling; mosaic retains its existing path. @@ -62,8 +62,27 @@ gates and eight captured view/HUD comparisons. The row suite also executes on Linux. Both clean-source OFF/ON builds pass current-build restore, rewind, deterministic replay and fresh-process replay. A dedicated ROM-backed rollback check also compares identical guest inputs while varying render widths and -draw counts; the resulting rollback state is identical. This is correctness -evidence, not yet a performance claim or owner approval. Matched built-game -performance qualification remains pending. Tracking: `beads-8wg.1.150`, roadmap +draw counts; the resulting rollback state is identical. + +Five balanced 4,200-frame Highway pairs per profile on an i5-4670 measured +median process-CPU reductions of 12.81% at 16:9 and 34.69% at 32:9, with all +five pairs positive in each wider view. Custom 4:3 and stock were +neutral/inconclusive; repeated OFF/ON, identical-binary and canonical-main +controls are retained. These results belong to this MMX title adapter and +package, not every SNES game; do not add percentages to the preceding row +package. Exact measured source is `ef821a32` (runtime code from `7e558267`). +Raw samples, source/runtime/binary identities, feature settings and size costs +are indexed in `campaign-results.json` under the task directory below. +Automated qualification is complete; combined owner playtest approval and +merge remain pending. No original Xbox or cross-build netplay claim is made. + +Owner direction on 2026-10-28: finish this already-authorized title candidate, +then prioritize game-agnostic engine mechanisms. Reuse or elevate shared +operations with actual consumer validation; retain X1 scene and asset policy +here. X2/X3 already use the shared live PPU and a shared background provider, +so enabling this X1-specific build flag is not a direct port. Substantial +adoption belongs after the shared-engine burndown or is deferred. + +Tracking: `beads-8wg.1.150`, roadmap `beads-49cy`, campaign `beads-2wy8.1`. Evidence: `G:/Temp/Codex/snesrecomp/background-spans-20261027/evidence`.