Skip to content
Draft
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
31 changes: 31 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,22 @@ if(BUILD_TESTING)
tests/mmx_wide_policy_test.c src/mmx_wide_policy.c)
target_include_directories(mmx_wide_policy_test PRIVATE src)
add_test(NAME mmx_wide_policy COMMAND mmx_wide_policy_test)
add_executable(mmx_renderer_test tests/mmx_renderer_test.c src/mmx_renderer.c src/mmx_render_assets.c src/mmx_display.c src/mmx_wide_policy.c)
target_include_directories(mmx_renderer_test PRIVATE src ${SNESRECOMP_ROOT}/runner/src)
add_test(NAME mmx_custom_renderer COMMAND mmx_renderer_test)
add_executable(mmx_render_capture tools/mmx_render_capture.c src/mmx_renderer.c src/mmx_render_assets.c src/mmx_display.c src/mmx_wide_policy.c)
target_include_directories(mmx_render_capture PRIVATE src ${SNESRECOMP_ROOT}/runner/src)
foreach(mmx_test mmx_display_test mmx_wide_policy_test mmx_renderer_test)
if(MSVC)
target_compile_options(${mmx_test} PRIVATE /UNDEBUG)
else()
target_compile_options(${mmx_test} PRIVATE -UNDEBUG)
endif()
endforeach()
if(NOT WIN32)
target_link_libraries(mmx_renderer_test PRIVATE m)
target_link_libraries(mmx_render_capture PRIVATE m)
endif()
endif()

include(${SNESRECOMP_ROOT}/runner/runner.cmake)
Expand Down Expand Up @@ -83,6 +99,8 @@ function(add_mmx_variant TARGET GEN_DIR CFG_INCLUDE_DIR)
${SNESRECOMP_RUNNER_SOURCES}
# game-specific runtime (shared across both variants)
src/mmx_display.c
src/mmx_renderer.c
src/mmx_render_assets.c
src/main.c
src/mmx_wide_preview.c
src/mmx_wide_policy.c
Expand All @@ -108,10 +126,23 @@ function(add_mmx_variant TARGET GEN_DIR CFG_INCLUDE_DIR)
# pristine generation directory is reused. Rockman X intentionally ships
# authentic 4:3 only, so its generated output remains untouched.
if(NOT "${TARGET}" STREQUAL "RockmanXSNESRecomp")
# Ninja plans object rebuilds before running the injector. A script
# edit must invalidate the affected bank in this same build.
set_property(SOURCE ${GEN_DIR}/bank00_part0a_v2.c APPEND PROPERTY
OBJECT_DEPENDS ${CMAKE_SOURCE_DIR}/tools/apply_renderer_capture.py)
set_property(SOURCE ${GEN_DIR}/bank82_part07_v2.c APPEND PROPERTY
OBJECT_DEPENDS ${CMAKE_SOURCE_DIR}/tools/apply_overrides.py)
set_property(SOURCE ${GEN_DIR}/bank83_part01_v2.c ${GEN_DIR}/bank83_part0b_v2.c
${GEN_DIR}/bank87_part04_v2.c ${GEN_DIR}/bank01_v2.c
${GEN_DIR}/bank81_part0f_v2.c APPEND PROPERTY
OBJECT_DEPENDS ${CMAKE_SOURCE_DIR}/tools/apply_overrides.py)
add_custom_target(${TARGET}_widescreen_overrides
COMMAND ${Python3_EXECUTABLE}
${CMAKE_SOURCE_DIR}/tools/apply_overrides.py
--gen-dir ${GEN_DIR}
COMMAND ${Python3_EXECUTABLE}
${CMAKE_SOURCE_DIR}/tools/apply_renderer_capture.py
--gen-dir ${GEN_DIR}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Applying ${TARGET} widescreen generated-code overrides"
VERBATIM)
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# MegaManXSNESRecomp

This branch contains the [custom widescreen renderer spike](docs/custom-renderer-spike.md),
with 16:9, 21:9, 32:9, Adaptive, and HUD anchoring in the widescreen mod.
The linked notes include the isolated launcher, validation and remaining
acceptance work before replacing the legacy renderer.

Static recompilation of *Mega Man X* (SNES) into native C, using the
[snesrecomp](https://github.com/mstan/snesrecomp) framework. This repo
is the per-game side: the runtime, the recompiled C output, the
Expand Down
835 changes: 835 additions & 0 deletions docs/custom-renderer-spike.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ id = "megaman-x.enhancement.widescreen"
version = "1.0.0"
name = "Mega Man X Widescreen"
author = "mstan"
description = "Extends Mega Man X's horizontal field of view by one third using its game-specific HUD, background-margin, stage-overlay, spawn, cull, and draw-window work."
description = "Custom widescreen renderer spike with adaptive stage rendering and HUD anchoring."
license = "MIT"
resolver = "declarative"
save_compatibility = "shared"
Expand All @@ -15,10 +15,70 @@ rom_sha256 = "b8f70a6e7fb93819f79693578887e2c11e196bdf1ac6ddc7cb924b1ad0be2d32"
[[feature]]
id = "widescreen"
name = "Widescreen (Extended view)"
description = "Add one-third more horizontal game area. Output follows Display aspect: 16:9 (CRT), 32:21 (square pixels), or 4:3 (square frame)."
description = "Extend the stage view with live enemies, preserved encounter timing, and anchored health bars."
group = "Display"
default_enabled = false

[[option]]
feature = "widescreen"
id = "renderer"
label = "Renderer"
type = "choice"
default = "custom"
[[option.choice]]
value = "custom"
label = "Custom renderer (spike)"
[[option.choice]]
value = "legacy"
label = "Legacy widescreen"

[[option]]
feature = "widescreen"
id = "aspect"
label = "Aspect ratio"
description = "Custom renderer: adapt to the window, or select a fixed aspect ratio."
type = "choice"
default = "adaptive"
[[option.choice]]
value = "adaptive"
label = "Adaptive"
[[option.choice]]
value = "16:9"
label = "16:9"
[[option.choice]]
value = "21:9"
label = "21:9"
[[option.choice]]
value = "32:9"
label = "32:9"

[[option]]
feature = "widescreen"
id = "hud"
label = "HUD anchoring"
type = "choice"
default = "edges"
[[option.choice]]
value = "edges"
label = "Screen edges"
[[option.choice]]
value = "center"
label = "Native positions"

[[option]]
feature = "widescreen"
id = "expanded_sprites"
label = "Expanded sprite capacity"
description = "Custom renderer: draw submitted sprites beyond the original frame limit. Experimental; disabled by default."
type = "choice"
default = "off"
[[option.choice]]
value = "off"
label = "Off"
[[option.choice]]
value = "on"
label = "On (experimental)"

[[plugin]]
feature = "widescreen"
id = "megaman-x.widescreen"
84 changes: 68 additions & 16 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
#include "snes/msu1.h"
#include "snes/ws_shadow.h"
#include "widescreen.h"
#include "mmx_wide_preview.h"
#include "mmx_wide_preview.h"
#include "mmx_renderer.h"

#include "types.h"
#include "mmx_rtl.h"
Expand Down Expand Up @@ -160,6 +161,22 @@ static GamepadInfo g_gamepad[2];
extern Snes *g_snes;

static void MmxDisplay_PreparePpuFrame(void) {
if (g_mmx_custom_renderer) {
int w = 16, h = 9;
if (g_window && g_renderer_funcs.GetOutputSize) g_renderer_funcs.GetOutputSize(&w, &h);
g_mmx_custom_view = MmxRendererViewport(g_mmx_custom_aspect, w, h);
g_snes_width = g_mmx_custom_view.width;
g_ws_active = false;
g_ws_extra = 0;
PpuBeginDrawing(g_ppu, g_my_pixels, 256 * 4, g_ppu_render_flags);
PpuSetExtraSpace(g_ppu, 0);
PpuSetWsHudOamShift(g_ppu, 0);
PpuSetWsHudOamShiftRange2(g_ppu, 0, 0);
PpuSetWidescreenBg3Widen(g_ppu, 0);
PpuSetWidescreenLineEnhancer(g_ppu, NULL, NULL);
if (g_snes && g_snes->cart) MmxRendererSetRom(g_rom, g_snes->cart->romSize);
return;
}
int width = MmxDisplay_ComputeFrameWidth(g_config.widescreen);
/* Probe/CI determinism: SNESRECOMP_WS_EXTRA pins the margin so measured
* widescreen geometry never silently follows the window aspect (a probe
Expand Down Expand Up @@ -255,7 +272,7 @@ void MmxDisplay_SetWidescreenEnabled(bool enabled) {
}

bool MmxDisplay_IsWidescreenEnabled(void) { return g_config.widescreen; }
bool MmxDisplay_IsWidescreenActive(void) { return g_ws_active; }
bool MmxDisplay_IsWidescreenActive(void) { return g_ws_active || (g_mmx_custom_renderer && g_mmx_custom_view.extra); }
int MmxDisplay_GetCurrentFrameWidth(void) { return g_snes_width > 0 ? g_snes_width : 256; }

/* Resolve one BG2 8x8 tile directly from MMX's decompressed level map.
Expand Down Expand Up @@ -443,7 +460,15 @@ static int g_script_index; // current entry
static int g_script_phase; // 0=holding, 1=waiting
static int g_script_counter; // frames left in current phase

static uint32 ParseButtonMask(const char *name) {
static uint32 ParseButtonMask(const char *name) {
/* Scripted movement must be able to jump while holding a direction. */
const char *plus = strchr(name, '+');
if (plus) {
char first[64]; size_t length = (size_t)(plus - name);
if (!length || length >= sizeof(first)) return 0;
memcpy(first, name, length); first[length] = 0;
return ParseButtonMask(first) | ParseButtonMask(plus + 1);
}
if (strcmp(name, "start") == 0) return 0x0008;
if (strcmp(name, "select") == 0) return 0x0004;
if (strcmp(name, "up") == 0) return 0x0010;
Expand Down Expand Up @@ -641,7 +666,31 @@ static SDL_HitTestResult HitTestCallback(SDL_Window *win, const SDL_Point *pt, v
return SDL_HITTEST_NORMAL;
}

void RtlDrawPpuFrame(uint8 *pixel_buffer, size_t pitch, uint32 render_flags) {
void RtlDrawPpuFrame(uint8 *pixel_buffer, size_t pitch, uint32 render_flags) {
if (g_mmx_custom_renderer) {
static uint32_t output[MMX_RENDER_MAX_WIDTH * 224];
extern void MmxWsChrRebindSweep(void);
MmxWsChrRebindSweep();
MmxRendererBeginFrame(g_ram);
g_rtl_game_info->draw_ppu_frame();
bool valid = MmxRendererEndFrame((const uint32_t *)g_my_pixels) &&
MmxRendererDraw(output, g_mmx_custom_view, g_mmx_custom_hud);
if (!valid) {
memset(output, 0, g_snes_width * 224 * sizeof(*output));
for (int y = 0; y < 224; ++y)
memcpy(output + y * g_snes_width + g_mmx_custom_view.extra,
g_my_pixels + y * 256 * 4, 256 * 4);
}
/* Shared pitch-aware blit and capture diagnostics; no PPU composition. */
RtlWidescreenPresent(pixel_buffer, pitch, (const uint8_t *)output, g_snes_width, 224);
const char *capture = getenv("MMX_RENDER_CAPTURE");
if (capture) {
static unsigned count;
const char *after = getenv("MMX_RENDER_CAPTURE_FRAME");
if (++count == (unsigned)(after ? atoi(after) : 1)) MmxRendererSaveCapture(capture);
}
return;
}
MmxDisplay_PrepareBg2Shadow();
/* WS-CHRBIND heal sweep: re-run bank_82_827D_M1X1's OAM tile-base/
* palette bind for any object that latched it before its VRAM-CHR slot
Expand Down Expand Up @@ -877,6 +926,7 @@ static void SdlRenderer_BeginDraw(int width, int height, uint8 **pixels, int *pi
MmxDisplay_ComputeViewport(width, height, output_width, output_height,
SnesDisplayAspect_Clamp(g_config.display_aspect),
g_config.ignore_aspect_ratio, false, &viewport);
if (g_mmx_custom_renderer) viewport = MmxRendererDestination(g_mmx_custom_view, output_width, output_height);
g_sdl_present_rect.x = viewport.x;
g_sdl_present_rect.y = viewport.y;
g_sdl_present_rect.w = viewport.width;
Expand Down Expand Up @@ -1148,14 +1198,12 @@ int main(int argc, char** argv) {
argv[0] = (char *)AbsolutizePathArg(argv[0], rom_abs, sizeof(rom_abs));
}

/* The config is config.ini next to the executable — nothing else,
* no directory walking. Anchoring cwd to the exe dir also pins
* keybinds.ini, rom.cfg and saves/ there, however the process was
* launched. (On read-only installs the anchor declines and cwd
* stays authoritative; see launcher.h.) */
/* By default anchor config, keybinds, ROM selection and saves to the exe.
* Explicit --config keeps the caller's cwd for isolated playtest data.
* Read-only installs also keep cwd authoritative; see launcher.h. */
{
extern int snesrecomp_anchor_to_exe_dir(void);
int anchored = snesrecomp_anchor_to_exe_dir();
int anchored = config_file ? 0 : snesrecomp_anchor_to_exe_dir();
host_report_breadcrumb("exe-dir anchor: %s",
anchored ? "ok" : "declined (cwd stays authoritative)");
}
Expand Down Expand Up @@ -1368,9 +1416,8 @@ int main(int argc, char** argv) {
#endif

#if defined(RECOMP_LAUNCHER)
/* cwd is anchored to the exe dir (snesrecomp_anchor_to_exe_dir above),
* and recomp_ui.cmake stages assets to <exe>/assets, so "." resolves
* assets correctly. */
/* Assets are relative to cwd. The default anchor uses the staged
* executable assets; an isolated --config runner supplies its own. */
int act = recomp_launcher_run_window(
MMX_LAUNCHER_TITLE,
&ls, &gi, ".", init_rom, rom_path_buf, sizeof(rom_path_buf));
Expand Down Expand Up @@ -1941,7 +1988,11 @@ error_reading:;
uint32 inputs = g_input_state | g_pad_buttons | g_gamepad[0].axis_buttons | g_gamepad[1].axis_buttons << 12;
inputs |= TickScript();
inputs |= debug_server_get_controller_inputs();
RtlRunFrame(inputs | GetActiveControllers() | debug_server_get_controller_active_mask());
if (g_mmx_custom_renderer) {
MmxDisplay_PreparePpuFrame();
MmxRendererLatchSprites();
}
RtlRunFrame(inputs | GetActiveControllers() | debug_server_get_controller_active_mask());

#ifdef ENABLE_ORACLE_BACKEND
// Step the oracle emulator with the same input. The runner's per-player
Expand Down Expand Up @@ -2188,8 +2239,9 @@ static void HandleCommand(uint32 j, bool pressed) {
g_cursor = !g_cursor;
snesrecomp_sdl_show_cursor(g_cursor != 0);
break;
case kKeys_Reset:
RtlReset(1);
case kKeys_Reset:
MmxRendererReset();
RtlReset(1);
break;
case kKeys_Pause: g_paused = !g_paused; break;
case kKeys_PauseDimmed:
Expand Down
Loading