Earthsiege Research archive

Input and cockpit state

The original keeps device polling, held controls, command dispatch, cockpit controls and player movement separate. A modern input layer should produce the same command/control structure; it need not emulate DOS interrupts or analogue joystick calibration.

Development/repositioning controls now have separate explanations and original-code captures. COMMAND_LINE.md covers program arguments and recording/playback setup.

Held keys

tools/check_input.py independently checks all 4,096 combinations of the twelve direction keys and 3,000 press/release events against DBSIM 0x4c818 and 0x4c6ec. Direction pairs come from the initialized original fixture. Results and all four command tables are in analysis/interface/input.json.

The fourteen held inputs are the eight perimeter keypad keys, M/J/K/I, Space and Enter. The first eight operate the movement pair, while M/J/K/I operate the separate aiming pair. Four diagonal movement entries are exclusive: the first such held entry replaces earlier movement and skips later movement keys. Only the first held dedicated aiming key contributes; these keys do not compose diagonal aiming. The normal step is 256 for walking and 64 for aiming; alternate control mode reduces movement to 64. Alt raises either to 256.

A press sets held and clears released. Releasing before an update has consumed that press marks it for one final update. Releasing after consumption clears it immediately. Consequently a short tap must not disappear between frames. Space and Enter have their own held flags; Enter is also allowed through to command processing.

The keyboard interrupt consumer at 0x7de20 establishes the modifier bits: Alt 0x200, Ctrl 0x400, Shift XOR Caps Lock 0x800. Do not confuse these with SDL modifier values. Application command masking removes unrelated device bits. Native KEYJOY.CFG supports TILT, BACKTURN and MISSILE, each with DEFAULT/REVERSE; the supplied CD defaults explicitly reverse missile pitch.

Commands and precedence

0x59df8 offers a command to the cockpit first (0x2d24c), then the global handler, then the controlled actor's virtual handler. The HERC handler 0x11050 in turn offers it to the weapon list 0xd4b4 before its remaining commands. Thus an Escape or Tab has different effects according to the active panel/view. A single global hotkey switch would lose that precedence.

The exported tables preserve each numeric command and exact branch address. Established groups include:

0x5a0c0 applies controls after command handling. Right-mouse aiming records an origin only when the initial press lies in the cockpit scene rectangle. Horizontal response is signed squared displacement divided by 64; vertical response is signed squared displacement divided by 32. Holding both mouse buttons resets torso yaw and elevation. These are positional, nonlinear controls, not raw mouse-delta steering. External camera and guided missile modes redirect the axes through other consumers.

Live cockpit exchange

tools/check_cockpit_state.py executes the original 0x155a8 and its actual initialized widget methods for 768 supplied states. It compares independently calculated reactor reserve, front/rear shields, shield balance, current/commanded throttle, heading/torso/elevation and target ownership counts. It does not replace weapon-panel exchange or gauge setters with stubs.

0x2cf28 updates the widget collection; 0x2b4a0 draws it. These calls also manage the dedicated instrument surface and the final forward overlay, so their ordering matters.

All eight 1,700-byte GAU files now agree byte-for-byte with the original loader after its selective coordinate rebasing. There are nine, not ten, stored weapon rectangles. Named layout blocks identify weapon controls, reactor, shields, view controls, throttle, heading/targeting, wingmates, and HERC identity/clock. The cockpit browser can overlay these bounds. Its static artwork remains a layout inspector; the reference renderer separately runs the actual live widgets.

Joystick configuration, interactive panels and portable widget drawing are integrated. Their original-code comparisons and host-window checks are described below and in MULTIFUNCTION_DISPLAYS.md and SHELL.md.

The native C++ held-key port now agrees with all 4,096 accumulation and 3,000 transition captures, including complete latch state. check_input.py retains these original states in work/research/interface/native-keyboard.json for fixture import. SDL routing, cockpit command precedence and mouse/joystick consumers are integrated; their comparisons are recorded below.

Camera state

check_camera.py captures 960 original 0x4b8 updates and 1,200 original 0xa04 control updates; native C++ matches all returned state. Mode 0 is free movement, 1 is target-relative orbit and 2 is model-attached. Free/orbit pitch is limited to ±0x3000. Orbit distance stays within 2,500–30,000 units. A camera movement that falls below height 100 rolls back position/orientation unless increasing orbit distance, in which case the original inverse-sine lookup raises the camera. Distance and acceleration state are not rolled back along with the pose.

Attached mode uses the actor's real virtual node getter, combines its local matrix with the actor transform, then applies the short-coordinate camera offset. Its Euler extraction uses the original inverse-sine/atan tables, including the pole branch. When the actor has no attachment matrix, the camera instead follows its base placement and ignores the additional look angles.

HERC profile disk word 10 selects the camera part. 0x10ee0 supplies attachment offset (0, word98, word100) and orbit focus (0, word102, word104). The offsets are local coordinates, not additional world heights. 0x13201 finds the part in the full model and returns the corresponding live hierarchical node transform.

Player control routing

capture_player_input.py records 4,096 original 0x5a0c0 routing calls from the initialized cockpit. Native C++ agrees on every movement/aim/camera/fire/target request and retained state. Commands, widget events and the downstream movement, aiming, camera and firing methods are explicit boundaries; those methods have separate original-code comparisons.

Mouse-origin acquisition includes the viewport edges. The first right press records the origin; subsequent updates use signed squared displacement, retaining 32-bit overflow and short axis addition. Both mouse buttons reset torso and pitch angles without clearing the previous mouse response during that update. Releasing right clears the response. Manual aiming cancels centering; otherwise an enabled tracker follows a living selected target. A disabled HERC instead requests centering. The target's virtual offset is added in world axes, rather than rotated again. External camera mode redirects the main axes and suppresses firing; controlled flyers use their separate control consumer. The third joystick button has a rising-edge target-cycle request. Release settings can stop throttle or request centering after this update's aim has already run.

Player integration comparisons

The native mission now has three independent 720-frame comparisons that run original player input, target/weapon exchange, live controllers, impacts, power and perception together. Explicit widget requests select targets, pair weapons and enable tracking; the manual-missile variant changes only the player's missile ammunition selector to 3. All 41,040 actor observations and retained player weapon, target, seeker and RNG states agree. The minimal headless widget adapter omits painting and returns unchanged throttle; the live gauge owners have their own comparisons below.

Camera/manual-missile pointers in the original input structure can alias movement or aiming. A steering missile clears that shared pair and the fire flag before the HERC input consumer runs. Native code represents the chosen pair explicitly. capture_mission_herc_frames.py and player_world_capture.py retain the exact boundary definitions; commands are applied after group dispatch as in 0x59700.

Native cockpit instrument drawing

Portable drawing now includes reactor reserve, forward/reverse speed lamps, requested throttle, and shield balance. check_reactor_gauges.py captures 96 original full-paint rasters through all chassis-specific ENG_CLP masks. check_throttle_gauges.py captures 864 complete native widgets, including the Raptor's horizontal slider, clamping and inclusive LED boundaries. The current speed lamps and requested-speed thumb are separate values; both use Q10 units. THR_LED1/2 provide the original lamp strips, and PBUTTONS supplies the thumb. The horizontal widget reserves twelve pixels even though the authored thumb bitmap is wider. Index zero remains transparent when the button is painted.

check_shield_instruments.py captures 480 original shield widgets and all 1,025 charge levels for the six animated DAC entries, indices 80–85. Those entries colour the existing cockpit shield artwork; the numerical displays instead show the requested front/rear allocation, summing to 200 percent. CPGREEN is the numeral font, confirmed by comparing its complete glyph payload with the live initialized font. PBUTTONS entries 58–63 provide three faces for each button. The original three palette dimming expressions are retained individually, including their distinct thresholds and integer rounding.

NativeRaster runs original constructors, readers and VGA painters. UI bitmap scratch copies require the original 640-pixel backing-page stride even when the visible capture is only 320 pixels wide. Its real native control container owns widget registration. Captures deliberately isolate full paints and supplied widget state; they do not establish pointer-event behavior or full cockpit composition by themselves.

Cockpit pointer controls

check_instrument_pointer.py runs the original mouse event queue, hit tests, press/drag/release methods and instrument callbacks for 1,248 sequential updates across all eight cockpits. Native C++ matches the selected control, drag state, button faces, pending and painted thumb positions, requested throttle and shield balance after every update. Shield actions use their original simulation exchange. The fixture replaces host mouse packets only and paints between packets; queued packets within a single paint interval retain the previous painted geometry.

The throttle release getter reads that last painted thumb, not its pending mouse coordinate. The vertical callback negates the axis value. Right releases still pass through the shared event controller, but shield button callbacks require the left-button bit. A simultaneous two-button release keeps the right-release word, as in the original queue. Moving out of the root window skips ordinary button hover processing, whereas an active slider continues clamping beyond its bounds.

The live mission now exchanges slider requests and shield allocations after the simulation update. A nonzero keyboard throttle adjustment (even at the limit), or an explicit stop, wins the exchange and clears its ownership flag. These controls share the native painters' slider geometry and fixed-point rounding.

Weapon readouts and group controls

The native implementation now reproduces all three original weapon rows: energy charge and requested-charge slider, ammunition count, and ECM on/off. The check_weapon_instruments.py captures compare 1,152 complete paints and input rectangles across all eight cockpits. Each capture constructs and initially paints the widget before changing state, so the original slider has valid backing pixels. The capture then isolates the full paint on a known background. Charge lamps paint only the charged interval; the remaining pixels belong to the cockpit artwork.

check_weapon_display_state.py compares 768 original row exchanges through 0xb5b8, 0xb67c and 0xb588. Energy settings use the original Q10/native-capacitor rounding and keyboard ownership flag. Displayed ammunition counts descend toward the actual count at the original frame-scaled rate. The ECM switch feeds both native device flags. Weapon names combine the executable's weapon and seeker name tables; UI initialization is separate from controller construction, as in the original, and does not assign invented labels to unused seeker selectors.

check_weapon_group_instruments.py compares 72 original full group paints and 144 exchanges through 0xcf78. I/II/III selection uses the authored fonts; LINK and TRACK use their original button faces. Mouse group and tracking requests are exchanged after automatic weapon selection. A keyboard group-change latch takes precedence over the mouse selection for that update and is then consumed. The live cockpit preserves these phases and connects number keys, selection, right-click membership, linked firing, tracking, charge sliders and ECM controls.

HUD navigation and target rendering

check_navigation_instruments.py compares 1,024 sequential original paints of the heading tape, torso indicator, elevation indicator, aiming cross and ATT tracking label. Disabled HERCs retain heading and torso display state while continuing to update elevation. The indicators consume angle differences, retain clamping history and use different neutral thresholds. Heading strips retain the original separate fixed-point compensation term and inclusive clipping boundary.

check_target_instruments.py covers 512 original target projections and complete rasters, including behind-camera/near-plane rejection, both sides and missile lock. The target box is derived from two vertical world points using the current model radius; its apparent size is limited to 25–75 pixels. An acquired lock replaces open corner lines with filled corner triangles. This capture isolates the painter; target eligibility and acquisition have separate original tests.

check_waypoint_instruments.py covers 512 original route lookups and complete arrow/text rasters. The HUD uses the point after the group's current route cursor, omits absent or finished routes, and uses off-screen arrows outside its narrow forward sector. The distance label uses the original approximate planar distance, rounded down in world units before conversion to metres. These are not the three-dimensional weapon range or the radar display's range calculations.

capture_mission_instruments.py retains every target's actual model radius, position and side plus the player's next waypoint from all four original recorded mission starts. These close the data connection between the isolated painters and the native mission objects. Damage-induced widget flicker and interactive MFDs have their own completed comparisons in MULTIFUNCTION_DISPLAYS.md.

Cockpit clock and presentation programs

check_clock_instruments.py executes the original constructor, full painter and update scheduler for 4,250 cases. The clock increments seconds on every other colon phase, uses a strict now > deadline check, and reschedules from the current tick without catching up missed intervals. A full repaint also toggles the phase. The original wraps at 25:00, not 100 minutes. Portable redraws retain the current phase rather than advancing the clock on every host frame.

check_cockpit_presentations.py runs 2,560 original TTM updates across all ten simulation scripts, including all eight cockpits, ambience and window cracks. Both bytecode execution state and complete brush rasters match the native C++ player. This includes alternate named sequences, random waits, mirrored brushes, waiting-frame redraws and label jumps. The Colossus program stops at its first ENDLABEL under the ordinary automatic entry path. Its later animation remains in the resource; the player does not silently skip that stop.

The TTM loader translates authoring .BMP names to ThreeSpace .DBA banks; these are not an additional DGDS bitmap format. The portable parser also reads all 123 original TTM programs, retaining every opcode/operand and named label; its decompressed streams match the original-code-checked analysis. The runtime implements the cockpit operations. The shell has a separate interpreter for its own presentation operations; see SHELL.md for the integrated comparisons.

The separate presentation random generator is the original ANSI-style LCG at 0x7cd28, seeded from DOS time by the manager. It does not consume the gameplay random stream. DBSIM creates the underlying timer with PIT divisor 0x26d3 (0x5a9dc), approximately 120 Hz; the clock and TTM callback counts advance every second interrupt. The native inspection loop accumulates nominal 60 Hz UI ticks separately from its 40 Hz physics updates.

Joystick calibration and preferences

check_joystick_pipeline.py checks 3,840 original outputs against independent arithmetic, plus all twelve device/pedal selections. check_joystick_routing.py checks 2,304 subsequent application routes and six Insert-mode switches. The machine-readable fixtures are analysis/interface/joystick-pipeline.json and joystick-routing.json. Electrical gameport discovery is supplied; original calibration, response curves, hat decoding, registration and application routing execute unchanged.

Preference byte 7 selects none, standard, two standard joysticks, Thrustmaster FCS, CH Flightstick or CH Flightstick Pro. Byte 8 selects the initial movement or turret control arrangement; Insert toggles arrangements only when that preference is enabled. Byte 9 enables separate rudder pedals. Two-stick mode forcibly disables the separate pedal preference and consumes all four axes. Byte 10 enables WCS throttle input. The four device responses are not universally four flight axes: FCS uses its fourth analogue channel for the hat, CH uses it for the slider, and the Pro decodes button combinations into hat directions.

The standard dead zone is 25 in signed 7-bit calibrated units, followed by a fixed-point rescale. The usual response mode squares that rescaled magnitude, retaining its sign, and applies another small-response dead zone. The pedal specialization reverses the third axis. Calibration's lower clamp has an original implementation error: its result is overwritten by the upper clamp, so values below the recorded minimum can exceed the nominal response range. This is recorded rather than silently repaired in the reference calculation.

Application routing selects a nonzero device axis in preference to the matching keyboard axis. In movement mode the primary pair controls locomotion and pedals control turret yaw; turret mode exchanges these roles. With two joysticks the whole pairs exchange. WCS supplies commanded throttle after this selection. The selected steering pointers alias the corresponding movement/aim pair; this also matters to the already documented missile-control handoff. Device-axis collection combines simultaneous sources with bitwise OR, not arithmetic sum. The route fixture isolates these choices without held keys, external camera or missile steering; those have separate original captures.

Preference transactions

check_preference_transactions.py independently checks 800 original transactions, recorded in analysis/interface/preference-transactions.json. The 17-byte file has separate current, saved and previous-value arrays at 0xbd562, 0xbd574 and 0xbd586, plus per-setting callback pointers at 0xbd598. Reading a missing file produces zeros; a successful read seeds all three arrays and invokes every registered callback. Setting a value can invoke its callback even when unchanged. Commit invokes changed callbacks and copies current values to both saved and previous. Cancel restores saved values and invokes changed callbacks, but does not overwrite the previous-value array.

Saving selected indices first reads the existing file and patches only those bytes; if it does not exist, the current complete array is written. All writes in this fixture are in memory. Callback invocations are recorded boundaries: this check establishes transactions, not the downstream sound/display hardware side effects of each preference.

Original cockpit view transitions

capture_cockpit_views.py independently checks all 105 combinations of view/request/pending state in 0x22634, then captures 130 original VGA presentations during F9/F10 side-view changes. F9 looks left from forward and returns from the right; F10 looks right and returns from the left. A request already pending prevents another from replacing it. The transition is completed by 0x226f0/0x22904, with page-dependent preparation and several hardware-pan steps inside one presentation call. Treating the view as an instantaneous bitmap swap loses this sequence.

native_vga.py now retains the attribute-controller address/data flip-flop and fine horizontal panning. The original display driver also records its fine-pan byte divided by two at 0x9e614. NativeScene.hardware_page() combines the programmed CRTC display start, fine pan and four planes; stable-page pixels agree with the earlier surface-origin capture. Transition GIF cadence is a convenient inspection interval, not a measured host-speed-independent animation duration.

Original scrolling surface

capture_cockpit_pans.py retains the prepared 640×400 VGA surface and all 213 scrolling frames for forward/lower, both side panels and their returns. The mouse cursor is disabled with original 66948 immediately before each presentation; reticle and pointer drawing are separate consumers. No pan or raster instruction is replaced. The exporter checks every captured indexed pixel before emitting the native comparison fixture.

Downward scrolling uses five-pixel increments and briefly reaches 200 before settling at the authored 197-pixel lower-panel origin. The upward path reaches 2 then 0 without overshooting. Horizontal paths use ten-pixel increments and write their final origin twice. See analysis/cockpits/pan-frames/index.json. All six paths and all 213 surface frames match the native implementation.

Side panels retain the forward image plane

The original camera does not turn through 90 degrees for F9/F10. In 226f0, the three additions to camera +44/+46/+48 all add literal zero; this is also what the machine instructions do, not a decompiler omission. capture_cockpit_view_camera.py executes the original command, camera-owner, camera-update, drawing and presentation routines for both side panels and both returns. All 24 frames retain the same camera position, angles and zero look offset. The VUE projection origin instead moves the image centre to x=480 for the left panel and x=-160 for the right (forward x=160). This continues the forward image plane into the adjacent cockpit panels.

The native cockpit resource reader already follows that projection rule. Adding a quarter-turn camera rotation would be a departure from this original behaviour. The observation is retained in analysis/cockpits/view-camera.json; it does not assert pixel equality of the complete intermediate pans.

Original modal dialogs

analysis/cockpits/runtime.html includes original pause, mission-status, exit and preferences captures. tools/capture_cockpit_dialogs.py runs the actual command dispatch at 59df8, construction, painting, modal keyboard loop and destruction. P, Q, Ctrl-Escape, F12 and Alt-P were exercised with cancellation and acceptance (ten cases). Preferences initially focus Music: eight Tabs reach Done, while Enter on Music changes that preference instead of dismissing the dialog.

The transport supplies keys only after a modal opens and retains preference writes in memory. Every case returns modal depth 94d78 to zero and restores bdbda to the cockpit focus b5b6e. The exit response is captured at the modal boundary; the subsequent simulator-to-shell handoff is a separate check. See analysis/cockpits/modal-transitions.json for keys, return values, resource reads and before/after preferences. No installation files were changed.

The pointer-queue fixture now also compares the original double-click latch, with 1,248 timed packets spanning the strict 60-tick boundary on the 120 Hz mouse clock. Left and right releases keep separate previous timestamps, but either can set the shared latch. Position is not part of that test. Simultaneous releases still retain the right-release button word. The native lower map uses this latch to transmit an already-selected, ready command on double-click.

The portable preference manager also matches all 800 original transactions, including each current/saved/previous array, callback, and partial disk update. The fixture now retains the selected-index inputs and previous values directly. Saving settings remains separate from accepting edits.

Portable mission alert rendering

capture_alert_rasters.py retains all nineteen original general alerts, their bounds and the complete before/after indexed frames. Native cursor hide/show operations isolate the dialog from the pointer. The portable renderer matches all nineteen frames, including transparent artwork edges, original font metrics and button borders. The original response table at 0x97158 distinguishes a warning's Continue response from a terminal notification's Return response.

Original controls and calibration windows

The controls capture browser contains original GO screenshots and reproducible policy/raster comparisons. CTL_ALRT is the code-managed options screen 1320. Its seven buttons select the joystick model, joystick role, WCS, RCS, mouse, calibration and Done. Right-click reverses the two enumerated choices. Alt+Tab moves focus backward; Escape accepts Done. Changing to two sticks temporarily hides RCS; applying that selection also clears the saved pedal preference. Only preference bytes 7–11 are written by the controls apply operation.

Calibration uses CAL_ALRT, with original per-device instructions and a sixteen-long calibration block (minimum, maximum, low centre, high centre for four axes), written to DATA/JOY1.CAL and DATA/JOY2.CAL. The original selection table enables the FCS hat, but neither CH choice enables its authored slider calibration entry. With missing physical hardware the original reports that no joystick is installed. A cancellation after sampling does not save changes, but can leave the in-memory calibration partly changed: 0x7956 resets and clears the device pointer before 0x80b8 attempts to restore the saved block.

The controls C++ port matches 1,944 changes and 72 complete original rasters. Calibration matches 22 full original sequences, including missing devices and cancellation after an endpoint, and 64 original rasters. Physical polling and mature input-debounce counters are explicit capture boundaries.

Live-input integration findings

The complete mixed routing capture now covers 4,096 combinations of actual held keys, competing joystick axes/buttons, external-camera and missile modes, pitch/backturn reversals, WCS and the button-2 mode-switch latch. The keyboard alternate-speed choice is made before that button changes mode. These records are in work/research/interface/joystick-mixed-routing.json; the low-level hardware poll supplies a packet while the original routing executes unchanged.

The third joystick button (packet +0x17) triggers target cycling; keyboard Enter is packet button 1 and also passes through the command chain. Copying its held state directly into the third-button signal was a native integration mistake: it bypassed tactical-panel handling. The fourth button reads the current movement/turret mode to stop throttle or request centering.

capture_input_options.py retains 30 KEYJOY inputs and 80 composed hat-view requests in work/research/interface/input-options.json. KEYJOY's original 0x4d918 parser is a whitespace-token reader, not an INI reader. Spaces around equals matter, # skips the rest of the line, comparison is case-insensitive, and REVERSE sets a flag that later DEFAULT does not clear. DOS text EOF ends the supplied file normally. An unrecognized token exposes a bug: the reader uses the table's byte size (28) as an entry count, walking beyond seven token pointers. These cases fail at the original compare's invalid memory access. Native C++ safely reports malformed tokens.

The flag at 0x95814 is MISSILE = REVERSE, not DEFAULT; earlier mixed-fixture field labeling was reversed even though the captured numeric behavior was correct. All three raw initializer flags are zero. The shipped file sets only MISSILE. Hat bytes request actions [1, 0, 4, 5] in order through 0x22634; the first request valid for the current view wins, and a pending transition blocks further requests. Existing view-transition captures retain the actual hardware-pan presentation.

SDL platform mapping and its intentional distinction from DOS electrical counts are documented in the native repository. The analysis captures continue to operate on original gameport-style counts and original 64-byte calibration blocks. No analysis files have been committed.

In-flight preference controls and artwork

The policy in DBSIM 0x47a88 is now compared for 1,512 actions / 24 sequences. Music and Sounds toggle; Radio cycles text only, speech only and text+speech. Horizon toggles; Sky, Ground and Shapes change by 32 with the original byte-end wrap behavior. Right-click reverses those detail changes. Done writes preference slots 0–6 only, and does not commit the preference manager's saved/previous arrays. The sound/display callbacks and the separately verified Controls child are explicit boundaries for these policy fixtures.

The complete original PRF_ALRT painter also has 96 full raster comparisons, including all value types, button faces and the vertically striped detail bars. See capture_mission_preferences.py and the original captures. The cursor is excluded because it is drawn independently of the dialog.

Original DCI pointers and live native presentation

The small green diamond visible in the forward view is PCURSOR.DCI, selected by 346a4 when the pointer is inside the authored HUD input rectangle. It is not a world-space target marker. CURSOR.DCI is the ordinary white cross; MCURSOR.DCI is the lower-map destination cursor. Original 345bc constructs the input rectangle from GAU 48c plus its 450 parent. 2df04/2df6c manage primary and secondary cursors, and the lower-cockpit transition switches ownership. The shell uses res32/arrow.dci; simulator alerts have their own dci/ALERT.DCI. The four directional DCI sprites and shell ICONS.DCI remain retained resources.

capture_software_cursors.py executes the original 66948 loader and VGA show routine for all eleven DCI resources at ten pointer positions. All 110 native rasters match, including hotspots, transparent pixels and clipped edges. check_native_cursors.py, under a private X11 display, also checks actual SDL pixels at scale two for aiming, normal controls, a clipped corner and a modal. It complements the isolated raster fixture; it does not establish every possible cursor-ownership transition. Raw native scene/replay images deliberately keep the separately presented pointer out of retained cockpit pages.

Integrated cockpit edges

Original 2dcc8/2dc2c and 2db1c establish the three inclusive screen-edge controls, hover cursor selection and click requests. The capture in work/research/cockpits/edge-controls.json covers seven sequential view states; edge-controls.bin records 1,365 boundary cases. Native integration now routes them after cockpit instruments, including corner precedence and directional DCI pointers. This fixes the missing lower-panel transition in DEMO1.

Retained external cameras and world draw ownership

All four complete demo comparisons now include 24 camera components on every observed initial/frame/exit state: 7,032 states in total. ESRPLY09 stores these expected results separately from the TAP inputs. They are checked after native execution, never supplied to the camera. The captures are demo1-camera-playback-v3 through demo4-camera-playback-v3, made with the unchanged game and passive DOSBox observer, sound enabled and startup/radio clocks retained.

Original 5894c changes camera ownership after player controls and before the camera transform. 58a44 retains an existing camera for its target. The first orbit starts at distance zero and clamps to 2,500 on update; later cockpit/orbit switches retain distance, orbit angles and rates. The previous integration recreated it at distance 10,000 and switched one presentation too early. Player control routing also finishes after a Q dialog accepts exit; the outer frame then leaves before perception/power. This includes the final camera-rate update even though no further camera transform or world draw occurs.

The external viewport is (0,30)-(319,170), with projection centre (160,105). Its rectangular clip region selects row-based texture traversal, just like the cockpit masks. Using column traversal caused visible texture and cloud differences. Original 58b68 places GREEN6X8 labels at x=50/215, baseline 189, using STRINGS0 groups 14 and 32. Both control-owner label rasters match.

Original 59c8f skips environment and scene rendering while lower-panel view 1 is stable, and resumes it during the prepared transition. Advancing hidden clouds on those frames had displaced the entire cloud field by the time the player returned to the forward view. Native camera and cloud diagnostics are available through EARTHSIEGE_REPLAY_DIAGNOSTICS.

A projected folded quad exposed an additional original column-mapper boundary: with a 40x40 texture it reads offsets 2,215 and 2,805, outside 1,600 bitmap bytes. The actual mission uses the row mapper, which stays in range for this quad. capture_folded_texture.py isolates both original modes with sentinel guard bytes. The native safety rule leaves undefined samples untouched; all defined pixels remain exact. Four captures test this boundary, in addition to the 600 ordinary texture rasters. This does not emulate arbitrary adjacent heap bytes.

The full sampled-frame investigation is complete: 114 of 120 images match exactly after controlling presentation timing. The other six differ only at the intentionally repaired malformed radar model, and match when the same repair is applied to the original renderer. See PIXEL_FIDELITY.md for all 230 affected pixels, original instructions and reference boundaries.

Retained cockpit instrument painting

Original 59c08 paints the widget container (2cf28) before 155a8 exchanges reactor, shield and speed values for the next paint. The native renderer now retains those values separately from simulation state. A second draw without a simulation update uses the already exchanged values. Throttle hit testing also retains the painted handle while a programmatic adjustment changes its pending position.

The unchanged DEMO2 snapshot after frame 1020 shows shield values 374/374/512 with the old six DAC colours. Calling only the original widget painter changes those colours without advancing simulation. DEMO1 frame 60 similarly retains painted speed 921 while its newly exchanged value is 1024.

Speed lamps additionally retain their bitmap contents. The original incremental bar setters paint the interval between old and new boundaries inclusively; an increasing and decreasing speed can therefore colour the shared boundary pixel differently. Raptor's horizontal full painter 3bda8 resets the forward bar cache twice instead of resetting the reverse bar. Its resulting retained pixels and next-update behaviour are reproduced, rather than recomputing a fresh bar.

The independent ESTHUP01 sequence fixture covers 2,720 original incremental and full repaint cases across all eight cockpit layouts. Four replay fixtures add 579 original reactor/throttle regions and shield-colour samples, excluding 13 regions near the original software cursor. Six shield points deliberately avoid the separately clocked red lamps. These expected pixels never feed native simulation or rendering.

Analysis evidence: tools/check_throttle_updates.py, tools/export_replay_instrument_frames.py, tools/inspect_panel_paint.py, work/research/tapes/replays/demo1-speed-indicator/throttle-state.json and work/research/tapes/replays/demo2-instrument-paint/instrument-paint.json.

Charge sliders retain their backing

capture_weapon_charge_retained.py executes the original energy-row constructor 3700c, setter 37194, full painter 37144 and incremental painter 3716c. The child slider painters 389ac/38b30 save and restore the two-pixel-wide thumb rectangle, including its two rows outside the LED bar. PBUTTONS frame 26 has a padded eight-pixel width, which is not the saved rectangle width.

On a full paint, a changed thumb position restores its old backing before the LED boundary is reset. Reset 317a4 does not clear pixels. LED setter 31e5c includes the old endpoint when charging and preserves the new endpoint when discharging. Incremental slider updates always restore the saved rectangle, then repaint only changed LEDs. The newly allocated saved backing in the captured DOS startup is zero; moving a thumb before its first save therefore leaves black corners on the cockpit artwork. Warmed full-painter captures had previously hidden this behaviour.

work/research/cockpits/weapon-charge-retained.json and the native fixture weapon_charge_retained.bin retain 1,280 sequential original cases, with cold/full/incremental paints and changes of charge, requested position and background. All match the C++ retained widget. The sixteen initial replay corner differences are thereby eliminated without modifying weapon simulation.

External-view return and physical VGA pages

The copy at 22b4d uses inclusive x=0..320. It is a 321-pixel copy between physical pages in a 640-pixel-wide VGA backing surface. Driver 51cxx runs three passes: partial left byte, all complete four-plane groups, partial right byte. Copying page 0 to page 320 therefore wraps the extra column into the first column of the source page's next row. The reverse copy has a different overlap order. This is not a projection or clipping error.

The native port retains the same bounded storage writes. The actual driver draw selector targets the other page of the +88/+8c pair before presentation; vertical and horizontal pans accordingly use different starting pages. tools/capture_cockpit_page_copy.py and work/research/cockpits/page-copy.json preserve 42 complete original VGA surfaces. This accounts for the ten alternate-frame left-border pixels in DEMO1 after frame 1134.

Expansion input differences

March expansion DBSIM 0x53218 adds PEDALS to the KEYJOY token table. Its REVERSE flag negates the rudder-pedal axis before control routing. The other three switches retain their meaning, and DEFAULT still does not clear an earlier REVERSE. Unknown tokens still run beyond the original comparison table; the C++ reader rejects them safely. capture_expansion_input_options.py checks 34 original inputs, including all switches, case handling and repeated assignments.

Expansion TAP frames add two held-button bits (weapon cycling and radar toggle) and a separate queued-command list. Queued commands run before frame preparation; the primary command keeps its later dispatch position. All 11,421 packets in four recordings are decoded and original-parser checked. C++ runs each recording to completion, including dialogs, audio and periodic drawing.

Expansion GO 0x09af4 saves 18 bytes of PREFS.CFG; DBSIM 0x4f5bc saves 22. The latter's loader 0x4f504 zeroes its 22-byte buffer before reading, so a shell file is valid simulator input. Slot 17 carries the battleset selection. It affects combat music selection and projectile falloff. The native preference manager accepts both lengths and preserves the extra simulator settings.