Skip to content

Native widescreen - #8

Merged
pairomaniac merged 33 commits into
mainfrom
widescreen
Sep 3, 2026
Merged

Native widescreen#8
pairomaniac merged 33 commits into
mainfrom
widescreen

Conversation

@pairomaniac

Copy link
Copy Markdown
Owner

The game at 1920x1080 (1280x720 on F4), rendered rather than scaled: every 640x480 assumption rewritten, the 2D layer through a canvas in an appended section, HUD polygons rescaled at insert, split screen as two real viewports, the ending roll without its bands. Translated onto the JP and OEM builds by generated tables.

Since the branch was opened: the two-player photo screens fill the viewport, the timer keeps to the top-left corner and the health bars are centred as a group on wide viewports, top/bottom split draws the HUD at the side-by-side size, the compositor skips untouched rows, and tools/uiemu.py runs the blob under Unicorn as a check.

Design, the game structures it rests on and the porting record are in docs/HIRES.md. 28 files, +8534 -561.

The prompt was placed by constants for 640x480; it is computed from
the picture globals, which 0x5c88ac already halves in the
low-resolution mode. JP and OEM pinned.
The game renders at 1920x1080 itself (F4 and F5 Screen: 1280x720),
in place of 640x480: about 250 sites over the retail executable and
a blob of new code (asm/ui.asm) in an appended section.

- mode, window, viewport globals, row tables and the coverage mask
  resized; the projection keeps the vertical field of view and shows
  more at the sides; the polygon pool is raised to 8000
- the 2D layer (backdrops, menus, HUD, text) is drawn on a 640x480
  canvas and composited onto the viewport at scale, so it keeps its
  layout; HUD polygons are rescaled at insert to the same frame
- split screen is side by side or top and bottom at full height, the
  timer and bars pinned to the viewport top, the machine select drawn
  once at full size
- the ending credits lose their black bands: the roll enters at the
  bottom and leaves through the top, the strip watermarks floored and
  the tile planes' row-table overruns bounded
- the enemy marker's on-screen window and the hangar's draw window
  follow the wider view; the credits prompt and the pause and loading
  text scale with the picture
- the F5 dialog says 720p/1080p and Ver/Hor; the choice is saved
- tools: uibuild.py builds the blob, vomap/votrans/hiresport.py map
  the sites onto the other builds, selftest.py pins every build's
  output and exercises the F4 table and the port path

docs/HIRES.md is the design and the record of what the game does at
each site, read off the disassembly. Retail only at this commit.
Three faults, all past the byte checks. Four MMX mask spans wrote
retail's interleaved mov ecx,[FB_PITCH] verbatim, so the ported
rasterizer loaded its row pointer from a retail address - the movq
fault on both builds. JP's FOV block is 54 bytes to retail's 82, and
the replacement ran into the viewport function's pointer stores - the
null surface and blank screens. The credits jne-to-jmp sites kept
retail's displacement and were rebased from the retail site, landing
mid-instruction in JP's 2D engine. port_sites rebuilds mask spans
from the build's bytes, routes a too-short FOV block through UI_FOV
in the section, takes a jcc's displacement from the build and only
rebases jumps into the sections; the F4 table and the activate
recreate size are ported; the strip-loader stores pair by idiom and
JP's engine-B roll site is mapped by hand. tools/portaudit.py and
portdump.py check placements by instruction shape. Retail output
unchanged; JP and OEM pinned.
The ending driver cuts the roll at frame 0x10e2, timed for the last
line to be under the top band, which the patch no longer paints. Both
thresholds move 80 frames: the last line's rows are off the top at
0x112e and back at the foot from 0x1136, and the blank feed clears
only columns 9..59, so their right-hand tiles would show. All three
builds re-pinned.
hiresport.py splits into resolve() and main(); portaudit.py reads the
maps from it and portdump.py goes. port_sites takes the section
address instead of a magic bound, reads the mask spans' interleave
length from the table, and returns the FOV block once. Builds carry
their PE stamp. The marker window reuses the hangar's margin.
Retail-only wording out of the tip, README, NOTES and the docs index;
the section header and HIRES.md name the roll cut, the marker window
and what port_sites redoes per build. No byte changes.
The size the game passes for the intro movie is its 16:10 window;
von.avi is 4:3 with the picture letterboxed inside it (rows 30..209,
16:9), and mciavi stretches to the window. The stub asks mciavi for
the frame size - WHERE_SOURCE with WHERE_MAX, since the placement
runs more than once and a plain WHERE returns the rect set last time
- fits the picture band and sends it as the PUT source rect, with
the game's numbers and no crop as the fallback. README's widescreen
caveat trimmed to what a player needs. All three builds re-pinned.
On 2D-only screens a top row of one colour fills the margins with it,
so the splash and title reach the screen edges in white; anything
patterned gets black. All three builds re-pinned.
The encounter grid and the noise transition are 8-px tiles on plane B
(the grid a 2x4-tile pattern, the static a 40x8-tile block under random
whole-tile scrolls), not generated. In the pre-3D phase the canvas is
the viewport's aspect with the picture centred (D_XO); the blob then
walks the engine's ring through the game's own destination and blit
helpers for the columns either side, each showing what the picture
shows at its column mod 80, so seams are the walker's own. Only rows
with no empty tile are continued; others take the flat colour as
before. The scan for painting past 4:3 and the viewport fill go. The
port tool reads each engine's ring, scroll, watermarks and helpers off
the build's own walker. Verified under Unicorn on all three patched
builds. All three re-pinned.
0x480410 halves its coordinates whenever 0x6bc948 is set; pre hides
the flag from the game's walker but post had restored it before the
margins were drawn, so their tiles landed at half scale, over the
picture on the right. All three re-pinned.
fit built 1/s as (2^24 / s) << 8, dropping the low byte: 0.68% short
at 2.25, so the 2D layer was stretched, its last columns and rows
never shown, text drifted right and down against the HUD polygons, and
the encounter grid measured 48.33 px. Now 2^32 / s rounded up; the
3 px polygon shift that compensated is zero. All three re-pinned.
Nine sites, the same shape as renderer A's: pool, side array, flush
list and the three caps (2000 in B's inserts, 2500 in its flush).
The port tool pairs both renderers' cap and lea sites by order. All
three re-pinned.
Zeroed with the exact composite reciprocal; the data words, the two
adds in pre and UI_SHIFT are now removed. All three re-pinned.
Native widescreen and Gamepad now come before Internet play; the quick
start no longer says widescreen starts unticked; shorter paragraphs and
subheadings; every picture with a descriptive alt text.
The star field starts further left by the extra width in columns, and
the moon card's commit goes through credits_moon, which scales it to
the full height.
The flash is the unit quad drawn as a grid of tiles through attribute
block 0x791ad0, sized a fifth over the 4:3 frame, so it stopped 80 px
short of each edge at 1080p. fill, run by both insert hooks after the
pass rescale, pushes the edge tiles' outer vertices to the viewport's
edges in a round (D_ROUND, from the patcher's round list). D_CMOON and
D_ROUND, which had been placed on D_F4WANT and D_XO, get slots of their
own.
Attaches to v_on.exe under Wine by pid. Modes: who draws a pixel, the
paint order at a spot, writers of an address, a break with backtrace,
a hex dump, and the ones that found the lock-on line - wedge (every
write to a pixel with its record, to a log), submit (records of one
attribute block at the insert, with the submitter), clip (polygons of
one attribute block at the clipper) and proj2d (2D quads projected at
a z other than 1.0).
So it clears the roll's window at the full height.
The object drawer picks an object's solid or meshed model by the camera
yaw against the object's authored direction code, meshed outside a
window of 0x4800 (0x3800 on the eight-way stages). The codes are
90-degree quantised, so an object beside the machine meshed once the
yaw was 11 degrees past square to it: at 4:3 that is at the picture's
edge, at 16:9 while it is whole on screen. Both windows get 22.5
degrees more; an object behind the machine is 135 degrees or more off
and still meshes.
The damage flash's attribute block 0x791ad0 is the game's flat red; the
off-screen arrow is drawn with it too, and the last step of its slide
put its tip on the frame inset, where fill threw it to the viewport
edge. fill now skips polygons under 120 HUD units wide.
The flat grey band that flashed across the picture as the enemy came on
from the side is the marker's leader line to the distance readout, in
pieces, 600 times its size: the 2D quad submits project their z = 1.0
vertices with the aspect slot of the projection, and the clipper
re-projects what it clips or subdivides with the 3D slot, focal length
included. Stock does it at 640x480 too, from 362 px of line.

asm/lockline.asm, applied to every build with or without Native
widescreen: the 2D quad submits' and mesh walkers' prologues call it to
flag the renderer, and the clippers' eleven fdivr sites per renderer
divide by the aspect slot while the flag is up. Site maps for JP and
OEM by byte signature. The resolution blob's layout is as it was.
The machine select and network waiting cards draw a 64x48-tile photo
at plane B's low-resolution window, 512x384 of the picture, framed in
black. margins recognises the three blocks by two cell values and
rescales the photo over the whole canvas from a staged copy, keeping
its aspect (full width, top and bottom cropped at 16:9; the full
picture at 4:3). Plane A and the sprites are drawn after, so text and
cursors are untouched.

The blob had 21 bytes free, so the pass stubs, data block and F4 table
move down 0x400 (D_BASE 0x1c00, UI_F4TAB_OFF 0x1f00). Port tables
regenerated; MD5s re-pinned for all three builds.
On a viewport wider than 4:3 the centred HUD frame carried the timer
and the bars towards the middle. In a round the 2D layer's band rows
and the in-game HUD pass's polygons in them move outward by the
frame's inset - the timer left, PLAYER/ENEMY and the bars right - so
each keeps its 4:3 distance from the nearest edge; the TOTAL time
moves right the same way. 1P and top/bottom split; side by side has
no inset. hud_enter records the outermost pass's stub so rescale can
tell the bars' pass from the marker's. Port tables regenerated; MD5s
re-pinned.
ui.asm header rewritten for what the blob does now (aspect canvas,
photo backdrops, HUD spread); D_RETD filed with D_RETS; the patcher-
written data listed. HIRES.md: sprites claim on the photo screens
dropped, the spread's pass gating stated correctly (the marker is
outside the passes in a round), stale cross-reference, two long lines,
the site count, and Rebuilding now covers the port tables and the
pins. UI_PASS_FUNCS comments name the reticle and the demo drivers.
uibuild's data-overlap bound follows the moved block. Patcher tip and
README mention the HUD anchoring and the photo screens. DEVELOPING's
daily loop points at the port-table step.
post compares each canvas row against the copy once (repe cmpsd) and
skips every viewport row that samples an unchanged one, which is most
of the HUD phase. rescale's y side keeps its temporaries in ebp
instead of push/pop; extents' labels are its own; the pass gate and
stub size are named. Insert comment corrected: both writers file four
vertices. Port tables regenerated; MD5s re-pinned.
…rgins in the debug readout

The spread now moves only the timer (and the TOTAL time); PLAYER/ENEMY
and the bars keep the centred frame. Top/bottom split draws the HUD at
the side-by-side scale, capped so frame rows 48..432 stay on screen
(HIRES_HUD_TB_ROWS), instead of the 4:3 that fits the half-height
viewport. HIRES_DEBUG_STATES also prints margins' scroll and the two
photo cells, to diagnose a photo backdrop that is not rescaled. Port
tables regenerated; MD5s re-pinned.
In stock the labels and bar frames span 233..523 of the 640 frame,
centre 378. In a round they move by 320 - HIRES_HUD_BARS units (-58),
2D layer and polygons alike, on every layout; the timer's inset shift
and the TOTAL time are unchanged. Port tables regenerated; MD5s
re-pinned.
…e stock layout

spread_row applied D_BARDX as viewport px, so the labels moved 58 px
where the bar polygons moved 58 units (130 px at 1080p) and the two
overlapped. The shift is now scaled by the 2D scale. The bar centring
is gated on the inset again, so side by side, which has no room for
it, is stock. Port tables regenerated; MD5s re-pinned.
The ring reads back with every tile index of a block lowered by one
constant (0x2380 as 0x154e on the select): the loader rebases the
block to its bank slot after the memcpy, so the two-cell signature
never matched. margins now compares the differences of cells (32, 24)
and (0, 47) against (0, 0), which the rebase preserves. Port tables
regenerated; MD5s re-pinned.
margins now runs for side by side too, for the photo alone: each
engine's canvas is 4:3, so a card's 512x384 becomes the 640x480 and
fills its half's width. Top/bottom is left as is; the rows its
viewport keeps are the photo's own. Port tables regenerated; MD5s
re-pinned.
The last two tile columns of all three blocks are the blank tile, so
the rescale took 16 px of black along with the picture and left a
strip on the right (58 px at 1080p). The source is 496x384 now. Port
tables regenerated; MD5s re-pinned.
The checks this branch's changes were verified with, committed as a
check: the game's plane B walker plus the blob with a rebased photo
block (1P, single viewport for either engine, side by side), the HUD
spread's 2D and polygon positions, the pre-fill against the viewport,
the no-op composite, the layouts' insets, top/bottom's row cut and
side by side's band pin. Runs from check.py on a retail exe; passes
with a note without nasm or unicorn. uibuild exports the three labels
it calls. rescale computes the band row once for both offsets. Port
tables regenerated; MD5s re-pinned.
@pairomaniac
pairomaniac merged commit 87b99dc into main Sep 3, 2026
2 checks passed
@pairomaniac
pairomaniac deleted the widescreen branch September 3, 2026 16:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant