Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c354aeb
Enable optional instruction-debug audit builds and record widescreen …
mstan Oct 26, 2026
f4a6f77
Normalize audit record line endings
mstan Oct 26, 2026
5a44b58
Preserve native parallax controller lifetime and repair widescreen HUD
mstan Oct 26, 2026
cd8ae92
Correct before-room QA handoff and preserve second-round feedback
mstan Oct 26, 2026
3e5cdfb
Anchor hunter landing collision to room records and hide duplicate doors
mstan Oct 26, 2026
f697107
qa: restore resident door graphics before hunter approaches
mstan Oct 26, 2026
a8ae506
docs: record owner acceptance of Byte and door animation
mstan Oct 26, 2026
5ed96c1
Pin shared rocket margin fix and record unchanged X3 regression
mstan Oct 27, 2026
bcce3e1
Pin validated shared room margin update
mstan Oct 27, 2026
6947965
Pin validated sparse canvas margin fixes
mstan Oct 27, 2026
ac75e8f
Contain Kaiser canvas and extend fixed arena walls
mstan Oct 27, 2026
4974423
Frame isolated X3 rematches and qualify native Boss Rush
mstan Oct 27, 2026
eb62e9d
Record final-build native Boss Rush replay and focused handoff
mstan Oct 27, 2026
fbff2b9
Extend locked Lab 1 walls into widescreen margins
mstan Oct 27, 2026
2e3a376
Record Lab 1 margin replay and remaining audit gaps
mstan Oct 27, 2026
8a71e90
Contain Godkarmachine and Doppler arena margins
mstan Oct 27, 2026
f30b8a6
Record assisted fortress defeats and bounded margin checks
mstan Oct 27, 2026
895c44d
docs: qualify native fortress progression and combined replay
mstan Oct 27, 2026
e7e6c6c
docs: record matched final-title runtime and save checks
mstan Oct 27, 2026
a0142c8
fix(widescreen): contain Lab2 room and boss canvas
mstan Oct 27, 2026
929a88d
build: pin direct bounded-canvas renderer
mstan Oct 27, 2026
49f2e69
docs: record final Lab2 renderer and runtime qualification
mstan Oct 27, 2026
e0fb303
build: qualify current engine base for X3 widescreen audit
mstan Oct 27, 2026
ac916f3
docs: record current X3 review build publication
mstan Oct 27, 2026
750e429
docs: qualify native final-room and Sigma transition
mstan Oct 27, 2026
87f198e
fix: preserve original X3 epilogue composition
mstan Oct 27, 2026
8ae5266
docs: record assisted X3 finale and epilogue validation
mstan Oct 27, 2026
935983c
docs: qualify assisted alternate X3 rescue ending
mstan Oct 27, 2026
e5e5f15
Preserve Maoh boss art and continue the authored roof in widescreen
mstan Oct 27, 2026
a897c86
Record Maoh trigger and shared presentation qualification
mstan Oct 27, 2026
e6e55db
Qualify actual Zero loss and native retreat at three widths
mstan Oct 27, 2026
3a0e9bd
Anchor the X3 final-stage entrance view inside its room
mstan Oct 27, 2026
3b74058
Record owner confirmation of the actual Kaiser finishing hit
mstan Oct 27, 2026
af8912e
Qualify Mac capture and X rescue at native and wide sizes
mstan Oct 27, 2026
c33ed97
Qualify earned Zero-loss finale and current Bit exit
mstan Oct 28, 2026
47f4d91
Accept owner Bit floor validation
mstan Oct 28, 2026
ffd0637
Anchor very-wide entrance views using original CX4 sprite positions
mstan Oct 28, 2026
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: 3 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ endif()
set(SNESRECOMP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/snesrecomp" CACHE PATH "snesrecomp checkout")
set(RECOMP_UI_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/recomp-ui" CACHE PATH "recomp-ui checkout")
set(SNESRECOMP_ENABLE_MODS ON CACHE BOOL "Shared mod package runtime" FORCE)
option(SNESRECOMP_REVERSE_DEBUG "Instruction-level debugger instrumentation" OFF)
include(${SNESRECOMP_ROOT}/runner/runner.cmake)

add_executable(MegaManX3SNESRecomp
Expand All @@ -22,7 +23,8 @@ target_include_directories(MegaManX3SNESRecomp PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/recomp ${SNESRECOMP_RUNNER_INCLUDE_DIRS})
target_compile_definitions(MegaManX3SNESRecomp PRIVATE
SMW_DISABLE_LM=1 SYSTEM_VOLUME_MIXER_AVAILABLE=0
SNESRECOMP_TRACE=$<BOOL:${SNESRECOMP_ENABLE_TRACE}> SNESRECOMP_REVERSE_DEBUG=0)
SNESRECOMP_TRACE=$<BOOL:${SNESRECOMP_ENABLE_TRACE}>
SNESRECOMP_REVERSE_DEBUG=$<BOOL:${SNESRECOMP_REVERSE_DEBUG}>)
set_property(SOURCE src/main.c APPEND PROPERTY COMPILE_DEFINITIONS
SNESRECOMP_BUILD_VERSION="${SNESRECOMP_BUILD_VERSION}")
if(NOT MSVC)
Expand Down
431 changes: 431 additions & 0 deletions docs/WIDESCREEN.md

Large diffs are not rendered by default.

2,634 changes: 2,634 additions & 0 deletions docs/widescreen-trigger-audit.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion framework_pins.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
snesrecomp=f68aa0fe8edae75cbc7d4b9473c71687b45ce702
snesrecomp=9a0c96eba07493e4e128cdf349332e6d094b5393
recomp-ui=0546fbcb0126bb14bf88d2128b4fe8a4229570a3
recomp-net=8c76fb643339144a3a84d5ba8f5defe6250dcd26
retcomm-rbengine=2a03e73693acee0fb78076ea058642c931bef12e
2 changes: 1 addition & 1 deletion snesrecomp
Submodule snesrecomp updated 234 files
3 changes: 2 additions & 1 deletion src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"# windows are adapted for 16:9. See docs/WIDESCREEN.md.\n"

#define MmxDisplayViewport X3DisplayViewport
#define MmxDisplay_ComputeFrameWidth X3Display_ComputeFrameWidth
#define MmxDisplay_ComputeFrameWidth(dw, dh, wide) \
X3Display_ComputeFrameWidth(dw, dh, (wide) && !X3UseNativeFrame())
#define MmxDisplay_ComputeViewport X3Display_ComputeViewport
#define MmxDisplay_GetWindowBaseWidth X3Display_GetWindowBaseWidth
#define MmxDisplay_GetWindowBaseHeight X3Display_GetWindowBaseHeight
Expand Down
572 changes: 335 additions & 237 deletions src/x3_rtl.c

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/x3_rtl.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ void X3ConfigureWsHud(void);

/* Per-frame exact BG1/BG2 gutter fill, self-gated by native VRAM parity. */
void X3ConfigureWsBgMargins(void);
bool X3UseNativeFrame(void);

/* LLE host execution cursor (resume PC + CpuState) — not covered by
* snes_saveload, which snapshots the unused snes->cpu. */
Expand Down
12 changes: 10 additions & 2 deletions tools/apply_overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

* pair-confirmed X windows: camera-X read -> add constant -> limit constant;
* camera triggers: camera-X read -> add constant -> compare with dp$05.
* Bit/Byte's landing thresholds, anchored to their original world records
so a vertical approach cannot delay collision until below the floor.
* $00:DE3A's four dynamic record-stream frontiers, routed through guarded
helpers that retain the authentic bounds by default.

Expand Down Expand Up @@ -57,14 +59,15 @@
ADD_BUDGET = 60
LIMIT_BUDGET = 40
CENTERED_LIMIT_BUDGET = 180
EXPECTED_SITES = 30
EXPECTED_SITES = 32 # 30 horizontal windows + two world-anchored hunter landings
EXPECTED_PAIRS = 12
EXPECTED_STREAM_SITES = 4
EXPECTED_DISPATCH_SITES = 0


def win_snippet(indent, var, kind, const):
fn = "X3WsObjWinAdd" if kind == "add" else "X3WsObjWinLimit"
fn = ("X3HunterLandingCameraY" if kind == "landing" else
"X3WsObjWinAdd" if kind == "add" else "X3WsObjWinLimit")
return (
f"{indent}{OBJ_MARKER} {{ extern uint16 {fn}(uint16); "
f"{var} = {fn}({const}); }}\n"
Expand Down Expand Up @@ -106,6 +109,11 @@ def apply_obj_windows(lines, verbose, fname):
state = ("add", ADD_BUDGET)
continue
if RE_ANCHOR_Y.search(line):
if cur_fn in {"bank_07_9077", "bank_3C_C582"}:
match = re.match(r"^(\s*)uint16 (_v\d+) =", line)
if match:
indent, var = match.groups()
inject[idx] = (indent,var,"landing",var,cur_fn)
state = None
continue
if state is None:
Expand Down
52 changes: 52 additions & 0 deletions tools/hunter_approach.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Graphics prerequisites for disposable stage-8 Bit/Byte approach snapshots.

Checkpoint 1 loads graphics group 4, but retail progression retains door asset
$56 from group 1. A direct checkpoint warp omits it. Let the original guest
graphics job load both groups before the door actor caches its tile/attributes.
This is a preparation assist, not validation of the skipped stage progression.
"""


def _room_doors(driver):
pool = driver.read(0x1518, 0x300)
return [(0x1518 + i, pool[i:i + 0x60])
for i in range(0, len(pool), 0x60)
if pool[i] and pool[i + 10] == 0x11
and 0x1400 <= int.from_bytes(pool[i + 5:i + 7], 'little') < 0x1700]


def validate_door_graphics(driver):
"""Reject a missing asset or an actor that cached the old missing asset."""
base = driver.byte(0x18256)
attributes = driver.byte(0x18356)
assert (base, attributes) == (0xf0, 0x2f), (
'Door asset $56 was not loaded', base, attributes)
doors = _room_doors(driver)
for address, actor in doors:
assert (actor[0x18], actor[0x11]) == (base, attributes), (
'Door cached an invalid graphics asset', hex(address),
actor[0x18], actor[0x11])
return dict(asset=0x56, tile_base=base, attributes=attributes,
doors=[hex(address) for address, _ in doors])


def prepare_door_graphics(driver):
"""Run original $00:B1FC via its idle cooperative scheduler slot $10.

Call after checkpoint 1 loads and before moving toward the room. The writes
to $40/$42 reproduce $00:81A3's registration of this graphics-only job.
No actors, camera state, collision state or encounter results are written.
"""
assert driver.byte(0x1fae) == 8 and driver.byte(0x1fb5) == 1
assert driver.byte(0x1f18) == 4
assert not _room_doors(driver), 'Warm graphics before the room door initializes'
before = driver.byte(0x1fd8)
for group in (1, 4):
assert driver.byte(0x40) == 0, 'Native graphics job slot is busy'
driver.byte(0x1f18, group)
driver.word(0x42, 0xb1fc)
driver.word(0x40, 0x0101)
driver.until(0x40, b'\0', 1800, 1)
driver.step(3)
assert driver.byte(0x1fd8) == before, 'Encounter results changed during preparation'
return validate_door_graphics(driver)
54 changes: 54 additions & 0 deletions tools/test_bossrush_framing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Check isolated rematch framing and restored adaptive hub presentation.

Supply current-build live-room and hub snapshots to an already running muted
debug host with widescreen enabled. No game RAM or encounter results are edited.
Native teleports, defeats and cumulative flags are separate gameplay checks.
"""
import argparse
import json
from pathlib import Path
import shutil
import struct
import sys


def run_case(driver, profile, state, expected_width, label):
profile=Path(profile)
(profile/'saves').mkdir(exist_ok=True)
shutil.copy2(state,profile/'saves/save0.sav')
driver.command('clear_controller');driver.command('loadstate 0')
for _ in range(3):driver.command('step 1')
driver.command('screenshot '+label+'.bmp')
width,height=struct.unpack_from('<ii',(profile/(label+'.bmp')).read_bytes(),18)
result=dict(stage=driver.byte(0x1fae),checkpoint=driver.byte(0x1fb5),
size=[width,abs(height)],expected_width=expected_width,
rematch_flags=driver.byte(0x1fda))
result['passed']=result['stage']==12 and result['size']==[expected_width,224]
(profile/(label+'.json')).write_bytes((json.dumps(result,indent=2)+'\n').encode())
assert result['passed'],result
return result


def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--port',required=True,type=int)
parser.add_argument('--profile',required=True,type=Path)
parser.add_argument('--room-state',required=True,type=Path)
parser.add_argument('--hub-state',required=True,type=Path)
parser.add_argument('--corridor-state',required=True,type=Path)
parser.add_argument('--wide-width',required=True,type=int)
parser.add_argument('--framework',type=Path,
default=Path(__file__).resolve().parents[1]/'snesrecomp')
args=parser.parse_args();sys.path.insert(0,str(args.framework/'tools'))
from sneslib.client import DebugClient
from sneslib.route import FrameDriver
driver=FrameDriver(DebugClient(args.port,timeout=20),args.profile)
try:
print(json.dumps([
run_case(driver,args.profile,args.room_state,256,'rematch-native-frame'),
run_case(driver,args.profile,args.hub_state,args.wide_width,'hub-wide-frame'),
run_case(driver,args.profile,args.corridor_state,args.wide_width,'corridor-wide-frame')]))
finally:driver.close()


if __name__=='__main__':main()
49 changes: 49 additions & 0 deletions tools/test_ending_framing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Verify fixed epilogue framing and restored widescreen cast on a debug host.

Use disposable current-build snapshots and a muted private profile with the
widescreen feature enabled. This checks presented dimensions and current modes;
the real final clear and ending progression require separate gameplay evidence.
"""
import argparse
import json
from pathlib import Path
import shutil
import struct
import sys


def check(driver, profile, state, phase, width, label):
shutil.copy2(state, profile/'saves/save0.sav')
driver.command('clear_controller');driver.command('loadstate 0')
for _ in range(3):driver.command('step 1')
driver.command('screenshot '+label+'.bmp')
actual_width,height=struct.unpack_from('<ii',(profile/(label+'.bmp')).read_bytes(),18)
mode=[driver.byte(0xd1),driver.byte(0xd2)]
result=dict(case=label,mode=mode,size=[actual_width,abs(height)],expected_width=width)
result['passed']=mode==[6,phase] and result['size']==[width,224]
assert result['passed'],result
return result


def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--port',required=True,type=int)
parser.add_argument('--profile',required=True,type=Path)
parser.add_argument('--epilogue-state',required=True,type=Path)
parser.add_argument('--cast-state',required=True,type=Path)
parser.add_argument('--wide-width',required=True,type=int)
parser.add_argument('--framework',type=Path,
default=Path(__file__).resolve().parents[1]/'snesrecomp')
args=parser.parse_args();sys.path.insert(0,str(args.framework/'tools'))
from sneslib.client import DebugClient
from sneslib.route import FrameDriver
(args.profile/'saves').mkdir(exist_ok=True)
driver=FrameDriver(DebugClient(args.port,timeout=20),args.profile)
try:
print(json.dumps([
check(driver,args.profile,args.epilogue_state,0,256,'epilogue-native-frame'),
check(driver,args.profile,args.cast_state,2,args.wide_width,'cast-wide-frame')]))
finally:driver.close()


if __name__=='__main__':main()
77 changes: 77 additions & 0 deletions tools/test_hunter_landings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Replay Bit/Byte from prepared approaches in a disposable debug profile.

The caller starts a muted test host. No enemy coordinates, collision state,
camera limits, spawn flags or encounter results are written by this check.
Prerequisite assists are recorded in the supplied approach snapshots.
"""
import argparse
import json
from pathlib import Path
import shutil
import sys
from hunter_approach import validate_door_graphics


def run_case(driver, profile, state, name, actor_id):
profile, state = Path(profile), Path(state)
(profile/'saves').mkdir(exist_ok=True)
shutil.copy2(state, profile/'saves/save0.sav')
driver.command('clear_controller')
driver.command('loadstate 0')
for _ in range(3): driver.command('step 1')
def hunters():
r=driver.read(0xd18,0x800)
return [dict(dp=0xd18+i, state=r[i:i+4].hex(), id=r[i+10],
y=int.from_bytes(r[i+8:i+10],'little'), hp=r[i+0x27])
for i in range(0,0x800,64) if r[i] and r[i+10] in (0x46,0x47)]
assert not hunters(), 'Approach already contains a hunter'
assert driver.byte(0x1fd8)&15 == 0, 'Approach has completed encounter flags'
start=dict(x=driver.word(0x9dd),y=driver.word(0x9e0),
camera=[driver.word(0x1e5d),driver.word(0x1e60)])
assert 0x1400 <= start['x'] < 0x1500, start
graphics = validate_door_graphics(driver)
samples=[]
for n in range(0,660,10):
driver.byte(0x9ff,32)
driver.step(10,'right')
samples.append(dict(frames=n+10,hunters=hunters()))
live=hunters()
assert len(live)==1 and live[0]['id']==actor_id, live
actor=live[0]
# Measured retail sprite feet relative to each object's world origin.
foot_offset=24 if actor_id==0x46 else 40
floor=driver.word(0x9e0)+17
foot=actor['y']+foot_offset
assert abs(foot-floor)<=1, (name,actor,foot,floor)
assert driver.byte(0x1fd8)&15 == 0
driver.snapshot(name+'-landing')
for _ in range(50): driver.step(1,'b'); driver.step(10)
live=hunters()
assert len(live)==1 and live[0]['state'][2:4]=='02' and live[0]['hp']>0, live
driver.snapshot(name+'-active')
result=dict(name=name, start=start, graphics=graphics, landing=actor, feet=foot, floor=floor,
active=live, outcome_flags=driver.byte(0x1fd8),samples=samples,
passed=True)
(profile/(name+'-landing-result.json')).write_text(json.dumps(result,indent=2))
return result


def main():
ap=argparse.ArgumentParser(description=__doc__)
ap.add_argument('--port',required=True,type=int)
ap.add_argument('--profile',required=True,type=Path)
ap.add_argument('--states',required=True,type=Path)
ap.add_argument('--framework',type=Path,default=Path(__file__).resolve().parents[1]/'snesrecomp')
args=ap.parse_args()
sys.path.insert(0,str(args.framework/'tools'))
from sneslib.client import DebugClient
from sneslib.route import FrameDriver
client=DebugClient(args.port,timeout=20)
driver=FrameDriver(client,args.profile)
try:
result=[run_case(driver,args.profile,args.states/(name+'.sav'),name,actor)
for name,actor in [('x3-byte',0x47),('x3-bit',0x46)]]
print(json.dumps(result,indent=2))
finally: driver.close()

if __name__=='__main__': main()
Loading