From 9777c92b36cc12fdc5cae30c27f83aab678f563c Mon Sep 17 00:00:00 2001 From: Matt Stanley <5354790+mstan@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:52:04 -0700 Subject: [PATCH 1/4] Avoid the Windows RPC hyper macro in armor state --- src/mmx_armor.c | 4 ++-- src/mmx_armor.h | 2 +- src/mmx_armor_combat.inc | 4 ++-- src/mmx_armor_utilities.inc | 2 +- src/mods/mmx_armor_plugin.c | 2 +- tests/mmx_armor_test.c | 6 +++--- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/mmx_armor.c b/src/mmx_armor.c index 8e5899a9..fb022c0e 100644 --- a/src/mmx_armor.c +++ b/src/mmx_armor.c @@ -149,7 +149,7 @@ bool MmxArmorValidState(const MmxArmorState *s) { s->barrier_x>=-0x1000000 && s->barrier_x<=0x1000000 && s->barrier_y>=-0x1000000 && s->barrier_y<=0x1000000 && s->utility_ticks<=180 && s->radar_stage<=13 && s->radar_closing<=1 && - s->giga_tick<=242 && s->hyper_energy<=0x1c00 && s->giga_fired<=1 && s->hyper<=1 && + s->giga_tick<=242 && s->hyper_energy<=0x1c00 && s->giga_fired<=1 && s->hyper_active<=1 && !s->utility_reserved && s->anim_group<=1 && s->dash_recovery<=9 && !s->reserved_word && s->burst<=2 && s->burst_air<=1 && s->burst_fired<=1 && s->burst_transition<=1 && @@ -334,7 +334,7 @@ void MmxArmorPrePlayer(uint8_t *r) { if(!state.valid || state.stage!=r[0x1f7a]) { state.valid=1;state.stage=r[0x1f7a];state.barrier_ticks=state.barrier_level=state.barrier_delay=0; state.dash_used=state.dash_ticks=state.dash_up=state.dash_jump=state.dash_recovery=0; - state.giga_tick=state.giga_fired=state.hyper=0;MmxArmorRefill(); + state.giga_tick=state.giga_fired=state.hyper_active=0;MmxArmorRefill(); if(game!=3)state.tracer=0;state.recovery_ticks=0; state.burst=state.burst_end=state.burst_queued=state.stored_shot=state.charge=state.fire_held=0; memset(state.shots,0,sizeof(state.shots));state.shot_mask=0; diff --git a/src/mmx_armor.h b/src/mmx_armor.h index 512d9d23..30ada686 100644 --- a/src/mmx_armor.h +++ b/src/mmx_armor.h @@ -32,7 +32,7 @@ typedef struct MmxArmorState { uint16_t utility_ticks,radar_epoch; uint8_t radar_stage,radar_closing; uint16_t giga_tick,hyper_energy; - uint8_t giga_fired,hyper; + uint8_t giga_fired,hyper_active; uint16_t utility_reserved; MmxArmorShot shots[8]; } MmxArmorState; diff --git a/src/mmx_armor_combat.inc b/src/mmx_armor_combat.inc index 4af56b15..4fe631dd 100644 --- a/src/mmx_armor_combat.inc +++ b/src/mmx_armor_combat.inc @@ -176,12 +176,12 @@ static void buster_player(uint8_t *r) { bool held=(r[0xbdf]&64)!=0,pressed=(r[0xbe3]&64)!=0; unsigned live=0;for(unsigned i=0;i<8;++i)if(state.shots[i].active && MmxArmorProjectileOwned(r,0x1228+i*64))live|=1u<=cost) { state.hyper_energy=(uint16_t)(state.hyper_energy-cost);state.stored_shot=2;start_armor_burst(r,1); - } else state.hyper=0; + } else state.hyper_active=0; } if(state.burst==1 && state.burst_fired && pressed)state.burst_queued=1; if(state.burst_end) { diff --git a/src/mmx_armor_utilities.inc b/src/mmx_armor_utilities.inc index 561e3296..2e6e6838 100644 --- a/src/mmx_armor_utilities.inc +++ b/src/mmx_armor_utilities.inc @@ -38,7 +38,7 @@ bool MmxArmorFrameTick(uint8_t *r,uint16_t pad,bool available) { bool pressed=combo && state.utility_held!=combo;state.utility_held=(uint8_t)combo; if(!state.giga_tick && combo==2 && pressed && r[0xd3]==4 && !r[0x1f10] && !r[0x1f0c] && !r[0x1f23] && !r[0x1f48]) { - if(game==3) {state.hyper^=1;armor_sound(r,0x2c);} + if(game==3) {state.hyper_active^=1;armor_sound(r,0x2c);} else if(state.valid && state.stage==r[0x1f7a] && config[9] && state.energy>=0x1c00 && !body[0x35] && (body[2]<=10 || body[2]==0x14)) { state.energy=0;state.giga_tick=1;state.giga_fired=0;state.tracer=0; diff --git a/src/mods/mmx_armor_plugin.c b/src/mods/mmx_armor_plugin.c index a60a4df5..788bf830 100644 --- a/src/mods/mmx_armor_plugin.c +++ b/src/mods/mmx_armor_plugin.c @@ -72,7 +72,7 @@ static int armor_debug(const char *cmd,const char *args,DebugServerGameSendLine uint8_t *body=MmxCoopEnabled()?MmxCoopCharacterBody(g_ram,MMX_COOP_X):g_ram+0xba8; snprintf(out,sizeof(out),"{\"game\":%u,\"gold\":%u,\"charge\":%u,\"burst\":%u,\"stored\":%u,\"shots\":%u,\"tracer\":%u,\"radar_stage\":%u,\"pad\":%u,\"valid\":%u,\"giga\":%u,\"energy\":%u,\"hyper\":%u,\"hyper_energy\":%u,\"x\":%u,\"y\":%u,\"barrier\":%u,\"barrier_level\":%u,\"dash_used\":%u,\"dash_ticks\":%u,\"dash_up\":%u,\"recovery\":%u}", MmxArmorGame(),s.gold,s.charge,s.burst,s.stored_shot,active,s.tracer,s.radar_stage,RtlGetPadState(0),MmxArmorValidState(&s), - s.giga_tick,s.energy,s.hyper,s.hyper_energy,body?(unsigned)(body[5]|body[6]<<8):0,body?(unsigned)(body[8]|body[9]<<8):0, + s.giga_tick,s.energy,s.hyper_active,s.hyper_energy,body?(unsigned)(body[5]|body[6]<<8):0,body?(unsigned)(body[8]|body[9]<<8):0, s.barrier_ticks,s.barrier_level,s.dash_used,s.dash_ticks,s.dash_up,s.recovery_ticks); send(out);return 1; } diff --git a/tests/mmx_armor_test.c b/tests/mmx_armor_test.c index c4b91abb..400f3720 100644 --- a/tests/mmx_armor_test.c +++ b/tests/mmx_armor_test.c @@ -161,15 +161,15 @@ static void gold(void) { assert(MmxArmorGetState().hyper_energy==0x1c00); MmxArmorFrameTick(ram,0,true);MmxArmorFrameTick(ram,8,true); MmxArmorFrameTick(ram,0,true);MmxArmorFrameTick(ram,0,true); - MmxArmorFrameTick(ram,0xc20,true);assert(MmxArmorGetState().hyper); + MmxArmorFrameTick(ram,0xc20,true);assert(MmxArmorGetState().hyper_active); tick_buster(true,true);assert(MmxArmorGetState().burst==1 && MmxArmorGetState().hyper_energy==0x1a00); MmxArmorRefill();assert(MmxArmorGetState().hyper_energy==0x1c00); gold_recovery(); player();ram[0x1f99]=15;ram[0x1f7a]=3;MmxArmorPrePlayer(ram);MmxArmorHadoukenAward(ram); - MmxArmorState retry=MmxArmorGetState();retry.hyper=1;retry.hyper_energy=12;retry.dash_used=2; + MmxArmorState retry=MmxArmorGetState();retry.hyper_active=1;retry.hyper_energy=12;retry.dash_used=2; MmxArmorSetState(retry);ram[0xd2]=2;assert(!MmxArmorFrameTick(ram,0,true)); ram[0xd2]=4;MmxArmorPrePlayer(ram);retry=MmxArmorGetState(); - assert(retry.gold && retry.hyper_energy==0x1c00 && !retry.hyper && !retry.dash_used); + assert(retry.gold && retry.hyper_energy==0x1c00 && !retry.hyper_active && !retry.dash_used); assert(MmxArmorFrameTick(ram,0,true) && MmxArmorGetState().tracer); puts("Gold Hyper Charge cost/refill and source-timed health/Sub Tank recovery passed"); } From 976f590beb247407bb98e94b65dc85854aeaf8d0 Mon Sep 17 00:00:00 2001 From: Matt Stanley <5354790+mstan@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:54:26 -0700 Subject: [PATCH 2/4] Prepare whole-view background planes and compose native row spans --- CMakeLists.txt | 25 ++++++- docs/ROW_RENDER_PLAN.md | 35 ++++++++++ snesrecomp | 2 +- src/main.c | 6 +- src/mmx_renderer.c | 14 ++++ src/mmx_row_plan.inc | 111 +++++++++++++++++++++++++++++++ tests/mmx_native_renderer_test.c | 5 +- tests/mmx_row_plan_test.c | 105 +++++++++++++++++++++++++++++ 8 files changed, 299 insertions(+), 4 deletions(-) create mode 100644 docs/ROW_RENDER_PLAN.md create mode 100644 src/mmx_row_plan.inc create mode 100644 tests/mmx_row_plan_test.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ed620c0..d82a7a2d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,6 +45,11 @@ set(SNESRECOMP_BUILD_VERSION "${PROJECT_VERSION}" CACHE STRING "Release stamp fo set(RECOMP_UI_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/recomp-ui" CACHE PATH "recomp-ui checkout") option(MMX_RASTER_FULL_COPY "Use full-copy raster storage as the correctness/performance reference" OFF) option(MMX_STAGE_TILE_REUSE "Reuse exact stage-map lookups within one captured draw" ON) +option(MMX_NATIVE_ROW_PLAN "Prepare whole-view native row composition plans" OFF) +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 + MMX_NATIVE_ROW_PLAN=$) set_property(SOURCE src/mmx_renderer.c APPEND PROPERTY COMPILE_DEFINITIONS MMX_RASTER_FULL_COPY=$ MMX_STAGE_TILE_REUSE=$) @@ -53,7 +58,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}") +string(APPEND MMX_REPLAY_ENGINE "-apu${SNESRECOMP_APU_TIME_IMPL}-crc${SNESRECOMP_CRC32_IMPL}-raster${MMX_RASTER_IMPL}-row${MMX_NATIVE_ROW_PLAN}") 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) @@ -248,6 +253,24 @@ if(BUILD_TESTING) add_test(NAME mmx_custom_renderer_native COMMAND mmx_renderer_native_test) set_tests_properties(mmx_custom_renderer_native PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/renderer-native") + set(mmx_row_plan_test_sources ${mmx_renderer_native_test_sources}) + list(REMOVE_ITEM mmx_row_plan_test_sources tests/mmx_native_renderer_test.c) + add_executable(mmx_row_plan_test ${mmx_row_plan_test_sources} + tests/mmx_row_plan_test.c src/mmx_boss_rush_score.cpp) + target_include_directories(mmx_row_plan_test PRIVATE src ${SNESRECOMP_ROOT}/runner/src) + target_compile_features(mmx_row_plan_test PRIVATE cxx_std_17) + if(MSVC) + target_compile_options(mmx_row_plan_test PRIVATE /UNDEBUG) + else() + target_compile_options(mmx_row_plan_test PRIVATE -UNDEBUG) + endif() + if(NOT WIN32) + target_link_libraries(mmx_row_plan_test PRIVATE m) + endif() + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/renderer-row-plan") + add_test(NAME mmx_custom_renderer_row_plan COMMAND mmx_row_plan_test) + set_tests_properties(mmx_custom_renderer_row_plan PROPERTIES + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/renderer-row-plan") foreach(reference FULLCOPY TRACE) string(TOLOWER "${reference}" reference_name) add_executable(mmx_renderer_${reference_name}_test diff --git a/docs/ROW_RENDER_PLAN.md b/docs/ROW_RENDER_PLAN.md new file mode 100644 index 00000000..188caa70 --- /dev/null +++ b/docs/ROW_RENDER_PLAN.md @@ -0,0 +1,35 @@ +# Whole-view row rendering experiment + +`MMX_NATIVE_ROW_PLAN=ON` selects a prepared-row pipeline in the NATIVE custom +renderer. The default stays OFF while the candidate is being qualified. The +existing NATIVE compositor and the separate LLE renderer remain buildable. + +Each captured line/view prepares its background pixel and private-color planes +before composition. The shared `snes/raster_plan.h` job resolves widened hardware +windows, optional signed color-beam windows, enabled layers and native/margin +boundaries into at most eleven immutable spans. Composition selects main/sub +pixels with span masks rather than repeating window/enable decisions per pixel. + +Scene-map reconstruction, native tile rows, palette/CHR ownership, sprite order, +color math, captured raster changes and guest execution remain authoritative. +This first experiment does not assume that reconstructed tiles or asset phases +are linear across every stage boundary. Map-binding spans are a separate possible +follow-up; this package does not implement them or GPU rendering. + +The planner has no platform dependency, live PPU mutation, heap allocation or +persistent simulation state. The adapter uses bounded private row scratch; +rewind/rollback formats gain no host pointers or new serialized fields. The build +choice is recorded in the replay engine identity and benchmark JSON. No runtime +switch or cross-build state conversion is provided. + +Validation uses the existing renderer scenes, 512 randomized full-color row +comparisons with scalar decisions, 4096 independent span/window comparisons, +ROM-backed Magic Carpet including its negative control, current-build restore, +rewind and fresh-process replay, and matched built-game A/B workloads. Per-game +CPU reductions and neutral results require recorded balanced samples; compilation +and component tests alone are not performance qualification. + +Owning issue: `beads-8wg.2.161`; coordinating campaign: `beads-2wy8.1`. Bounded +raw receipts and source/binary/configuration identities are retained under +`G:/Temp/Codex/snesrecomp/full-view-20261027`. No original Xbox runtime or +cross-build netplay compatibility is claimed. diff --git a/snesrecomp b/snesrecomp index 5ac4b98e..068cea80 160000 --- a/snesrecomp +++ b/snesrecomp @@ -1 +1 @@ -Subproject commit 5ac4b98e065b668ed77b65fa6342ec296b8b9e23 +Subproject commit 068cea8047a53bfa1692c6bcab332227fc721de8 diff --git a/src/main.c b/src/main.c index f375ea6d..e2fcedb9 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_ROW_PLAN +#define MMX_NATIVE_ROW_PLAN 0 +#endif extern const RtlGameInfo kMmxGameInfo; extern int mmx_tier2_diagnostics_enabled(void); @@ -249,10 +252,11 @@ 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\",\"audio_output\":%s}\n", + "\"apu_time_impl\":\"%s\",\"raster_impl\":\"%s\",\"row_plan\":%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", s_benchmark_audio ? "true" : "false"); } } diff --git a/src/mmx_renderer.c b/src/mmx_renderer.c index 89bde49f..cf23529a 100644 --- a/src/mmx_renderer.c +++ b/src/mmx_renderer.c @@ -2310,6 +2310,13 @@ static bool raster_scene_owned(void) { zero_title_menu() || frame_rush.menu; } +#ifndef MMX_NATIVE_ROW_PLAN +#define MMX_NATIVE_ROW_PLAN 0 +#endif +#if MMX_NATIVE_RASTER && MMX_NATIVE_ROW_PLAN +#include "mmx_row_plan.inc" +#endif + bool MmxRendererDraw(uint32_t *out, MmxRenderView view, bool hud) { if (!out || !frame.valid || view.width < 256 || view.width > MMX_RENDER_MAX_WIDTH || view.extra != (view.width - 256) / 2 || (view.width & 1)) return false; @@ -2887,6 +2894,13 @@ bool MmxRendererDraw(uint32_t *out, MmxRenderView view, bool hud) { native_layers |= 1u << layer; } } +#endif +#if MMX_NATIVE_RASTER && MMX_NATIVE_ROW_PLAN + native_render_row(&p, r, y, view, stage, menu, dialogue_backdrop, + crystal_ripple[y], palette_fade, native_background, native_layers, + objects, object_colors, native_rgb, brightness, native_plain_layers, + beams, beam_count, out + y * view.width); + continue; #endif for (int sx = 0; sx < view.width; ++sx) { int x = sx - view.extra; diff --git a/src/mmx_row_plan.inc b/src/mmx_row_plan.inc new file mode 100644 index 00000000..0e22c6e5 --- /dev/null +++ b/src/mmx_row_plan.inc @@ -0,0 +1,111 @@ +/* Included after the compositor's scalar helpers. All data is private to this + * captured row/view. This pipeline never reads or writes the live machine. */ +#include "snes/raster_plan.h" +#include + +static void native_background_range(const Ppu *p, const Raster *r, + unsigned layer, int y, bool stage, int extra, unsigned begin, unsigned end, + uint16_t *pixels, int *colors) { + int by = y; + unsigned mosaic = (p->mosaic & (1u << layer)) ? (p->mosaic >> 4) + 1 : 0; + if (mosaic) by -= by % (int)mosaic; + for (unsigned sx = begin; sx < end; ++sx) { + int x = (int)sx - extra; + if (mosaic) x -= ((x % (int)mosaic) + (int)mosaic) % (int)mosaic; + pixels[sx] = background(p, r, layer, x, by, stage, &colors[sx]); + } +} + +static void native_compose_row(const Ppu *p, const Raster *r, + MmxRenderView view, const uint16_t backgrounds[3][MMX_RENDER_MAX_WIDTH], + const int colors[3][MMX_RENDER_MAX_WIDTH], const uint16_t *objects, + const int *object_colors, const uint32_t *rgb, const uint8_t brightness[32], + const unsigned plain_layers[2], const SnesRenderComposePlan *plan, + bool menu, bool dialogue_backdrop, uint32_t *out) { + (void)view; + for (unsigned s = 0; s < plan->count; ++s) { + const SnesRenderComposeSpan *span = &plan->spans[s]; + if (menu && span->margin) continue; /* The output starts cleared. */ + const unsigned plain = plain_layers[span->color_window]; + for (unsigned sx = span->begin; sx < span->end; ++sx) { + uint16_t main = 0x500, sub = 0x500; + uint16_t b0 = backgrounds[0][sx], b1 = backgrounds[1][sx], b2 = backgrounds[2][sx]; + uint16_t candidate = b0 & span->main[0]; + if (candidate > main) main = candidate; + candidate = b1 & span->main[1]; + if (candidate > main) main = candidate; + candidate = b2 & span->main[2]; + if (candidate > main) main = candidate; + uint16_t camouflage_bg = main; + candidate = objects[sx] & span->main[3]; + if (candidate > main) main = candidate; + candidate = b0 & span->sub[0]; + if (candidate > sub) sub = candidate; + candidate = b1 & span->sub[1]; + if (candidate > sub) sub = candidate; + candidate = b2 & span->sub[2]; + if (candidate > sub) sub = candidate; + candidate = objects[sx] & span->sub[3]; + if (candidate > sub) sub = candidate; + int bg_colors[3] = {colors[0][sx], colors[1][sx], colors[2][sx]}; + unsigned layer = (main >> 8) & 15; + bool camouflage = object_colors[sx] >= 0 && (object_colors[sx] & 0x8000) && + (layer == 4 || layer == 6); + if ((plain & (1u << layer)) && !camouflage) + out[sx] = native_plain_colour(r->palette, rgb, main, object_colors[sx], bg_colors); + else + out[sx] = colour(p, r->palette, brightness, main, sub, span->color_window, + object_colors[sx], bg_colors, dialogue_backdrop && span->margin, camouflage_bg); + } + } +} + +static void native_render_row(const Ppu *p, const Raster *r, int y, + MmxRenderView view, bool stage, bool menu, bool dialogue_backdrop, + int ripple, unsigned palette_fade, const uint16_t center[3][256], + unsigned center_layers, const uint16_t *objects, int *object_colors, + const uint32_t *rgb, const uint8_t brightness[32], const unsigned plain_layers[2], + const LightBeam beams[2], unsigned beam_count, uint32_t *out) { + uint16_t backgrounds[3][MMX_RENDER_MAX_WIDTH]; + int colors[3][MMX_RENDER_MAX_WIDTH]; + for (unsigned layer = 0; layer < 3; ++layer) { + memset(colors[layer], 0xff, (size_t)view.width * sizeof(colors[layer][0])); + if (!((p->screenEnabled[0] | p->screenEnabled[1]) & (1u << layer))) { + memset(backgrounds[layer], 0, (size_t)view.width * sizeof(backgrounds[layer][0])); + continue; + } + int by = y + 1 + ((stage && !layer) ? ripple : 0); + if (center_layers & (1u << layer)) { + memcpy(backgrounds[layer] + view.extra, center[layer], 256 * sizeof(uint16_t)); + native_background_range(p, r, layer, by, stage, view.extra, 0, + (unsigned)view.extra, backgrounds[layer], colors[layer]); + native_background_range(p, r, layer, by, stage, view.extra, + (unsigned)view.extra + 256, (unsigned)view.width, backgrounds[layer], colors[layer]); + } else { + native_background_range(p, r, layer, by, stage, view.extra, 0, + (unsigned)view.width, backgrounds[layer], colors[layer]); + } + if (palette_fade) for (int sx = 0; sx < view.width; ++sx) + if (colors[layer][sx] >= 0) + colors[layer][sx] = MmxRenderAssetsFadeColor((uint16_t)colors[layer][sx], palette_fade); + } + if (palette_fade) for (int sx = 0; sx < view.width; ++sx) + if (object_colors[sx] >= 0) + object_colors[sx] = MmxRenderAssetsFadeColor((uint16_t)object_colors[sx], palette_fade) | + (object_colors[sx] & 0x8000); + SnesRenderWindowPlan windows = SnesRenderWindows(p, + p->window1left == 0 ? -view.extra : p->window1left, + p->window1right == 255 ? 255 + view.extra : p->window1right, + p->window2left == 0 ? -view.extra : p->window2left, + p->window2right == 255 ? 255 + view.extra : p->window2right); + SnesRenderInterval color_windows[2]; + for (unsigned i = 0; i < beam_count; ++i) + color_windows[i] = (SnesRenderInterval){beams[i].left[y], beams[i].right[y]}; + SnesRenderComposePlan plan; + bool planned = SnesRenderPlanCompose(&plan, p, &windows, -view.extra, + (unsigned)view.width, color_windows, beam_count); + assert(planned); /* Validated renderer dimensions and at most two beams. */ + if (!planned) return; + native_compose_row(p, r, view, backgrounds, colors, objects, object_colors, + rgb, brightness, plain_layers, &plan, menu, dialogue_backdrop, out); +} diff --git a/tests/mmx_native_renderer_test.c b/tests/mmx_native_renderer_test.c index 5533662c..476f83eb 100644 --- a/tests/mmx_native_renderer_test.c +++ b/tests/mmx_native_renderer_test.c @@ -20,7 +20,10 @@ static uint32_t row_random(void) { return row_rng; } -int main(void) { +#ifndef MMX_NATIVE_TEST_ENTRY +#define MMX_NATIVE_TEST_ENTRY main +#endif +int MMX_NATIVE_TEST_ENTRY(void) { renderer_scene_suite(); MmxRendererReset(); MmxRendererSetRom(NULL, 0); memset(&frame_rush, 0, sizeof(frame_rush)); diff --git a/tests/mmx_row_plan_test.c b/tests/mmx_row_plan_test.c new file mode 100644 index 00000000..0c4109ff --- /dev/null +++ b/tests/mmx_row_plan_test.c @@ -0,0 +1,105 @@ +/* Keep the existing native scenes/jobs and compare planned whole-row output + * against the independent scalar compositor decisions over randomized data. */ +#define MMX_NATIVE_ROW_PLAN 1 +#define MMX_NATIVE_TEST_ENTRY existing_native_suite +#include "mmx_native_renderer_test.c" + +static void composition_rows(void) { + static uint16_t backgrounds[3][MMX_RENDER_MAX_WIDTH], objects[MMX_RENDER_MAX_WIDTH]; + static int colors[3][MMX_RENDER_MAX_WIDTH], object_colors[MMX_RENDER_MAX_WIDTH]; + static uint32_t expected[MMX_RENDER_MAX_WIDTH], actual[MMX_RENDER_MAX_WIDTH]; + const unsigned widths[] = {256, 342, 682, MMX_RENDER_MAX_WIDTH}; + for (unsigned sample = 0; sample < 512; ++sample) { + unsigned width = widths[sample % 4], extra = (width - 256) / 2; + MmxRenderView view = {(int)width, (int)extra, 16.0/9.0}; + Ppu p = {0}; Raster r = {0}; + p.inidisp = (uint8_t)(row_random() & 15); + p.screenEnabled[0] = (uint8_t)row_random(); + p.screenEnabled[1] = (uint8_t)row_random(); + p.screenWindowed[0] = (uint8_t)row_random(); + p.screenWindowed[1] = (uint8_t)row_random(); + p.windowsel = row_random() & 0xffffff; + p.wbgobjlog = (uint16_t)row_random(); + p.window1left = (uint8_t)row_random(); p.window1right = (uint8_t)row_random(); + p.window2left = (uint8_t)row_random(); p.window2right = (uint8_t)row_random(); + if (!(sample % 7)) p.window1left = 0; + if (!(sample % 11)) p.window2right = 255; + p.cgwsel = (uint8_t)row_random(); p.cgadsub = (uint8_t)row_random(); + p.fixedColor = (uint16_t)(row_random() & 0x7fff); + for (unsigned c = 0; c < 256; ++c) r.palette[c] = (uint16_t)(row_random() & 0x7fff); + for (unsigned x = 0; x < width; ++x) { + for (unsigned layer = 0; layer < 3; ++layer) { + unsigned priority = layer == 2 ? (row_random() & 1 ? 15 : 1) : + layer == 1 ? (row_random() & 1 ? 11 : 7) : (row_random() & 1 ? 12 : 8); + backgrounds[layer][x] = row_random() & 3 ? + (uint16_t)(priority << 12 | layer << 8 | (row_random() & 127)) : 0; + colors[layer][x] = row_random() & 1 ? -1 : (int)(row_random() & 0x7fff); + } + objects[x] = row_random() & 3 ? (uint16_t)((row_random() % 4 * 4 + 2) << 12 | + (row_random() & 1 ? 4 : 6) << 8 | 128 | (row_random() & 127)) : 0; + object_colors[x] = row_random() & 1 ? -1 : (int)(row_random() & 0xffff); + } + unsigned beam_count = sample % 3; + SnesRenderInterval beams[2]; + for (unsigned i = 0; i < beam_count; ++i) { + beams[i].left = (int)(row_random() % (width + 100)) - (int)extra - 50; + beams[i].right = (int)(row_random() % (width + 100)) - (int)extra - 50; + } + SnesRenderWindowPlan windows = SnesRenderWindows(&p, + p.window1left ? p.window1left : -(int)extra, + p.window1right == 255 ? 255 + (int)extra : p.window1right, + p.window2left ? p.window2left : -(int)extra, + p.window2right == 255 ? 255 + (int)extra : p.window2right); + SnesRenderComposePlan plan; + assert(SnesRenderPlanCompose(&plan, &p, &windows, -(int)extra, width, beams, beam_count)); + assert(plan.count && plan.count <= 11 && plan.spans[0].begin == 0 && + plan.spans[plan.count-1].end == width); + unsigned plain[2]; uint8_t brightness[32]; + for (unsigned c = 0; c < 32; ++c) + brightness[c] = (uint8_t)(((c << 3) | (c >> 2)) * (p.inidisp & 15) / 15); + for (unsigned inside = 0; inside < 2; ++inside) + plain[inside] = condition(p.cgwsel >> 6, inside) ? 0 : + condition((p.cgwsel >> 4) & 3, inside) ? 127 : (~p.cgadsub & 127); + bool menu = (sample % 5) == 0, dialogue = (sample % 7) == 0; + memset(actual, 0, width * sizeof(*actual)); + for (unsigned sx = 0; sx < width; ++sx) { + int x = (int)sx - (int)extra; + bool margin = x < 0 || x >= 256; + if (menu && margin) { expected[sx] = 0; continue; } + uint16_t screens[2] = {0x500, 0x500}, camouflage_bg = 0x500; + for (unsigned sub = 0; sub < 2; ++sub) { + for (unsigned layer = 0; layer < 3; ++layer) + if ((p.screenEnabled[sub] & (1u << layer)) && + (!(p.screenWindowed[sub] & (1u << layer)) || !window(&p, layer, x, extra)) && + backgrounds[layer][sx] > screens[sub]) screens[sub] = backgrounds[layer][sx]; + if (!sub) camouflage_bg = screens[0]; + if ((p.screenEnabled[sub] & 16) && + (!(p.screenWindowed[sub] & 16) || !window(&p, 4, x, extra)) && + objects[sx] > screens[sub]) screens[sub] = objects[sx]; + } + bool inside = window(&p, 5, x, extra); + if (beam_count) { + inside = false; + for (unsigned i = 0; i < beam_count; ++i) + inside |= x >= beams[i].left && x <= beams[i].right; + } + int bc[] = {colors[0][sx], colors[1][sx], colors[2][sx]}; + expected[sx] = colour(&p, r.palette, brightness, screens[0], screens[1], inside, + object_colors[sx], bc, dialogue && margin, camouflage_bg); + } + native_compose_row(&p, &r, view, backgrounds, colors, objects, object_colors, + native_brightness(p.inidisp & 15), brightness, plain, &plan, menu, dialogue, actual); + for (unsigned sx = 0; sx < width; ++sx) if (actual[sx] != expected[sx]) { + fprintf(stderr, "row plan differs sample=%u width=%u x=%u actual=%08x expected=%08x\n", + sample, width, sx, actual[sx], expected[sx]); + abort(); + } + } +} + +int main(void) { + existing_native_suite(); + composition_rows(); + puts("WHOLE-VIEW ROW PLAN: NATIVE SCENES AND 512 RANDOMIZED COMPOSITION ROWS PASS"); + return 0; +} From 31fb2fb2ef0ca91816ea980605f38c8bd14d2d8c Mon Sep 17 00:00:00 2001 From: Matt Stanley <5354790+mstan@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:39:06 -0700 Subject: [PATCH 3/4] Enable qualified Windows x64 whole-view rendering by default --- CMakeLists.txt | 6 +++++- docs/ROW_RENDER_PLAN.md | 12 ++++++++++-- snesrecomp | 2 +- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d82a7a2d..e360a826 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,7 +45,11 @@ set(SNESRECOMP_BUILD_VERSION "${PROJECT_VERSION}" CACHE STRING "Release stamp fo set(RECOMP_UI_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/recomp-ui" CACHE PATH "recomp-ui checkout") option(MMX_RASTER_FULL_COPY "Use full-copy raster storage as the correctness/performance reference" OFF) option(MMX_STAGE_TILE_REUSE "Reuse exact stage-map lookups within one captured draw" ON) -option(MMX_NATIVE_ROW_PLAN "Prepare whole-view native row composition plans" OFF) +set(_MMX_NATIVE_ROW_PLAN_DEFAULT OFF) +if(_MMX_RASTER_IMPL_DEFAULT STREQUAL "NATIVE" AND MMX_RASTER_IMPL STREQUAL "NATIVE") + 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}) 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 diff --git a/docs/ROW_RENDER_PLAN.md b/docs/ROW_RENDER_PLAN.md index 188caa70..975867da 100644 --- a/docs/ROW_RENDER_PLAN.md +++ b/docs/ROW_RENDER_PLAN.md @@ -1,8 +1,9 @@ # Whole-view row rendering experiment `MMX_NATIVE_ROW_PLAN=ON` selects a prepared-row pipeline in the NATIVE custom -renderer. The default stays OFF while the candidate is being qualified. The -existing NATIVE compositor and the separate LLE renderer remain buildable. +renderer. It defaults ON for the qualified Windows x64 NATIVE configuration; +other platforms retain their existing defaults. Explicit OFF selects the +existing NATIVE compositor, and the separate LLE renderer remains buildable. Each captured line/view prepares its background pixel and private-color planes before composition. The shared `snes/raster_plan.h` job resolves widened hardware @@ -33,3 +34,10 @@ Owning issue: `beads-8wg.2.161`; coordinating campaign: `beads-2wy8.1`. Bounded raw receipts and source/binary/configuration identities are retained under `G:/Temp/Codex/snesrecomp/full-view-20261027`. No original Xbox runtime or cross-build netplay compatibility is claimed. + +Five balanced A/B pairs per view on an Intel i5-4670 measured median process-CPU +reductions of 7.4% (custom 4:3), 9.6% (16:9) and 12.6% (32:9). The ordinary +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. diff --git a/snesrecomp b/snesrecomp index 068cea80..32e6ce06 160000 --- a/snesrecomp +++ b/snesrecomp @@ -1 +1 @@ -Subproject commit 068cea8047a53bfa1692c6bcab332227fc721de8 +Subproject commit 32e6ce065a1e06feeef5696d46499fa136528703 From 2c69dffdaa5c0b9d67184d27f84057aee55f8b0a Mon Sep 17 00:00:00 2001 From: Matt Stanley <5354790+mstan@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:43:34 -0700 Subject: [PATCH 4/4] Pin planner qualification with the Linux DMA harness fix --- snesrecomp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snesrecomp b/snesrecomp index 32e6ce06..9f111225 160000 --- a/snesrecomp +++ b/snesrecomp @@ -1 +1 @@ -Subproject commit 32e6ce065a1e06feeef5696d46499fa136528703 +Subproject commit 9f111225d1751d4d4913c887ed42b3f206838be4