paavohtl Claude Opus 4.6 (1M context) commited on
Commit
c8f62aa
·
1 Parent(s): 4a4cdc9

feat: port RenderDrawingQueue dispatcher to pure Rust

Browse files

Ports the per-frame render-queue dispatcher (0x542350, ~789k dispatches
per minute of gameplay) and its four leaf helpers — RQ_ClipCoordinates
(0x542BA0), RQ_ClipCoordinatesWithRef (0x542C70), RQ_TranslateCoordinates
(0x542B10, formerly mis-labelled RQ_GetCameraOffset_Maybe), and the
inline FixedDiv16_16 — to native Rust. The dispatcher's 14 cmd-type
cases dispatch through the DisplayGfx vtable to slots 11..22 via raw
vtable pointers, eliminating the last per-frame WA→DisplayGfx bridge.

Lesson #1 from the project memory bit again: the Ghidra decompile of
RQ_TranslateCoordinates lost the ECX register input entirely (only
showed in_EAX and unaff_EDI). Verified ECX = x_in from the prologue
disasm `AND ECX, 0xFFFF_0000` at 0x542B1A and from the dispatcher's
`MOV ECX, [ESI+0x8]` setup at every call site. Recorded as failure
mode #3 in feedback_ghidra_caller_frame_reads.md.

ClipContext, helpers, and all 14 case bodies use the existing Fixed
type (Fixed::floor / Fixed::div / Fixed::mul_raw) — no parallel
fixed_div_16_16 helper. Adds a new 1-reg + 2-stack usercall_trampoline!
macro variant for the EAX-this convention. Verified live across four
headful replays (napalm, drill_prod_rope, shotgun_longbow_ninja,
various_weapons_1) totalling ~789k Rust vtable dispatches with no
crashes or visual issues; also fixed a latent bug in case 0xA where
the previous draft passed `0,0` to rq_translate_coordinates instead
of cmd[2]/cmd[3] (case is dead in tested replays so the bug never
fired).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

crates/openwa-core/src/render/mod.rs CHANGED
@@ -4,6 +4,7 @@ pub mod landscape;
4
  pub mod opengl;
5
  pub mod palette;
6
  pub mod queue;
 
7
  pub mod sprite;
8
  pub mod turn_order;
9
 
 
4
  pub mod opengl;
5
  pub mod palette;
6
  pub mod queue;
7
+ pub mod queue_dispatch;
8
  pub mod sprite;
9
  pub mod turn_order;
10
 
crates/openwa-core/src/render/queue_dispatch.rs ADDED
@@ -0,0 +1,1064 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `RenderDrawingQueue` (0x542350) — the per-frame render-queue dispatcher.
2
+ //!
3
+ //! This is the bridge between the producer-side `RenderQueue` (a buffer of
4
+ //! enqueued draw commands) and the consumer-side `DisplayGfx` vtable. Once
5
+ //! per frame, `GameRender_Maybe` (0x533DC0) sets up a [`ClipContext`] and
6
+ //! invokes this function with `(EAX = render_queue, stack[0] = display_gfx,
7
+ //! stack[1] = clip_ctx)`. The dispatcher then:
8
+ //!
9
+ //! 1. Sorts the entry-pointer array by each command's `layer` field
10
+ //! (ascending; using a non-stable sort to mirror MSVC `qsort`).
11
+ //! 2. Walks the sorted list from highest index downward (so high-layer
12
+ //! commands draw first, low-layer commands draw on top).
13
+ //! 3. Switches on `cmd[0]` (command type, 0..0xE) and dispatches to the
14
+ //! appropriate `DisplayGfx` vtable slot, passing through one of the
15
+ //! `RQ_*` clip / translate helpers as needed.
16
+ //!
17
+ //! The 14 command types map to slots 11..22 of [`DisplayGfxVtable`]:
18
+ //!
19
+ //! | type | producer | slot | DisplayGfx method |
20
+ //! |------|----------------|------|------------------------|
21
+ //! | 0 | DRAW_RECT | 18 | `fill_rect` |
22
+ //! | 1 | DRAW_BITMAP_GLOBAL | 20 | `draw_scaled_sprite` |
23
+ //! | 2 | DRAW_TEXTBOX_LOCAL | 20 | `draw_scaled_sprite` |
24
+ //! | 3 | (no Rust producer) | 21 | `draw_via_callback` |
25
+ //! | 4 | DRAW_SPRITE_GLOBAL | 19 | `blit_sprite` |
26
+ //! | 5 | DRAW_SPRITE_LOCAL | 19 | `blit_sprite` |
27
+ //! | 6 | DRAW_SPRITE_OFFSET | 19 | `blit_sprite` |
28
+ //! | 7 | (no Rust producer) | 12 | `draw_polyline` |
29
+ //! | 8 | DRAW_LINE_STRIP | 14 | `draw_line_clipped` |
30
+ //! | 9 | DRAW_POLYGON | 13 | `draw_line` |
31
+ //! | 0xA | (no Rust producer) | 15 | `draw_pixel_strip` |
32
+ //! | 0xB | DRAW_CROSSHAIR | 16 | `draw_crosshair` |
33
+ //! | 0xC | DRAW_OUTLINED_PIXEL | 17 | `draw_outlined_pixel` |
34
+ //! | 0xD | DRAW_TILED_BITMAP | 11 | `draw_tiled_bitmap` |
35
+ //! | 0xE | (no Rust producer) | 22 | `draw_tiled_terrain` |
36
+ //!
37
+ //! ## Calling convention
38
+ //!
39
+ //! `RenderDrawingQueue` is `__usercall(EAX = *mut RenderQueue,
40
+ //! stack[0] = *mut DisplayGfx, stack[1] = *mut ClipContext)`, returning
41
+ //! with `RET 0x8`. Verified from caller `GameRender_Maybe` at 0x5340af.
42
+ //!
43
+ //! ## Helper calling conventions (verified from disassembly)
44
+ //!
45
+ //! - `RQ_ClipCoordinates` (0x542BA0) is `__thiscall(ClipContext*)` taking
46
+ //! `(x_in, y_in, ref_z, *out_x, *out_y, *out_scale)`.
47
+ //! - `RQ_ClipCoordinatesWithRef` (0x542C70) has the same shape but uses
48
+ //! the pivot fields `clip[2]/clip[3]`.
49
+ //! - `RQ_TranslateCoordinates` (0x542B10) is `__usercall(ECX = x_in,
50
+ //! EAX = y_in, EDI = ClipContext*, stack[0] = *out_x, stack[1] = *out_y)`.
51
+ //! The Ghidra decompile lost the ECX input — verified from disasm at
52
+ //! 0x542B1A (`AND ECX, 0xFFFF0000`). Same shape as `RQ_ClipCoordinates`
53
+ //! but without the perspective scale.
54
+
55
+ use crate::fixed::Fixed;
56
+ use crate::render::display::gfx::DisplayGfx;
57
+ use crate::render::queue::RenderQueue;
58
+
59
+ // =============================================================================
60
+ // ClipContext — the 4-i32 anchor/pivot block GameRender_Maybe builds on stack
61
+ // =============================================================================
62
+
63
+ /// Per-frame clip / camera context, built on the stack by
64
+ /// `GameRender_Maybe` (0x533DC0) and passed as the second stack arg to
65
+ /// [`render_drawing_queue`].
66
+ ///
67
+ /// The first two fields are the camera anchor in fixed-16.16 coordinates;
68
+ /// the second pair are pivot points for `RQ_ClipCoordinatesWithRef` (case 6
69
+ /// with `flags & 4`). All four are sourced from `DisplayGfx + 0x8CEC..0x8CFC`
70
+ /// at the start of `GameRender_Maybe` and adjusted in place by the
71
+ /// perspective-zoom math before being handed to the dispatcher.
72
+ #[repr(C)]
73
+ #[derive(Debug, Clone, Copy)]
74
+ pub struct ClipContext {
75
+ /// Camera anchor X in Fixed16. Subtracted from each command's source
76
+ /// X by the clip helpers; only the upper 16 bits matter.
77
+ pub cam_x: Fixed,
78
+ /// Camera anchor Y in Fixed16.
79
+ pub cam_y: Fixed,
80
+ /// Pivot X for the perspective-with-pivot path
81
+ /// (`RQ_ClipCoordinatesWithRef`, case 6 with `cmd.flags & 4`).
82
+ /// Stored as Fixed16 with the fractional bits guaranteed zero by
83
+ /// `GameRender_Maybe`'s setup math.
84
+ pub pivot_x: Fixed,
85
+ /// Pivot Y — same as `pivot_x`.
86
+ pub pivot_y: Fixed,
87
+ }
88
+
89
+ const _: () = assert!(core::mem::size_of::<ClipContext>() == 0x10);
90
+
91
+ // =============================================================================
92
+ // Pixel-aligned subtract with overflow saturation
93
+ // =============================================================================
94
+
95
+ /// Pixel-aligned subtract with overflow saturation (the inner loop body
96
+ /// shared by `RQ_ClipCoordinates` and `RQ_TranslateCoordinates`).
97
+ ///
98
+ /// Both inputs are masked to their upper 16 bits via [`Fixed::floor`]
99
+ /// (truncated to pixel resolution while staying in fixed-16.16
100
+ /// representation). Returns `in_val - cam` clamped to `Fixed(i32::MIN)`
101
+ /// / `Fixed(i32::MAX)` on either-direction overflow.
102
+ ///
103
+ /// `FixedDiv16_16` (0x5B3501), incidentally also used by these helpers,
104
+ /// is the same operation as [`Fixed::div`] (the `Div` impl on `Fixed`):
105
+ /// `(num << 16) / denom` with a 64-bit signed intermediate. We don't
106
+ /// need a standalone `fixed_div_16_16` helper.
107
+ #[inline]
108
+ fn clip_sub_saturate(in_val: Fixed, cam: Fixed) -> Fixed {
109
+ let in_val = in_val.floor();
110
+ let cam = cam.floor();
111
+ let delta = Fixed(in_val.0.wrapping_sub(cam.0));
112
+ if in_val.0 < 0 {
113
+ // in_val is negative; if cam is positive and delta wrapped positive,
114
+ // it's an underflow → saturate to MIN.
115
+ if cam.0 > 0 && delta.0 > 0 {
116
+ return Fixed(i32::MIN);
117
+ }
118
+ } else {
119
+ // in_val is non-negative; if cam is negative and delta wrapped
120
+ // negative, it's an overflow → saturate to MAX.
121
+ if cam.0 < 0 && delta.0 < 0 {
122
+ return Fixed(i32::MAX);
123
+ }
124
+ }
125
+ delta
126
+ }
127
+
128
+ /// Perspective scale `0x100_0000 / projected_z` as a Fixed16 ratio.
129
+ /// Pulled out so the same expression doesn't have to be re-derived in
130
+ /// each clip helper.
131
+ #[inline]
132
+ fn perspective_scale(projected_z: i32) -> Fixed {
133
+ // Equivalent to the WA `FixedDiv16_16` call: `(0x100_0000 << 16) / projected_z`.
134
+ Fixed::from_raw(0x0100_0000) / Fixed::from_raw(projected_z)
135
+ }
136
+
137
+ // =============================================================================
138
+ // RQ_ClipCoordinates (0x542BA0) — perspective-scaled clip
139
+ // =============================================================================
140
+
141
+ /// `RQ_ClipCoordinates` (0x542BA0) — perspective clip with no pivot.
142
+ ///
143
+ /// Returns `false` if `ref_z + 0x100_0000 < 1` (projection plane behind
144
+ /// the eye); otherwise writes `(out_x, out_y, out_scale)` and returns true.
145
+ ///
146
+ /// Calling convention in WA: `__thiscall(ClipContext*)`. The Rust port
147
+ /// takes the context by reference instead.
148
+ pub fn rq_clip_coordinates(
149
+ clip: &ClipContext,
150
+ x_in: Fixed,
151
+ y_in: Fixed,
152
+ ref_z: i32,
153
+ out_x: &mut Fixed,
154
+ out_y: &mut Fixed,
155
+ out_scale: &mut Fixed,
156
+ ) -> bool {
157
+ let delta_x = clip_sub_saturate(x_in, clip.cam_x);
158
+ let delta_y = clip_sub_saturate(y_in, clip.cam_y);
159
+
160
+ let projected_z = ref_z.wrapping_add(0x0100_0000);
161
+ if projected_z < 1 {
162
+ return false;
163
+ }
164
+ let scale = perspective_scale(projected_z);
165
+ *out_scale = scale;
166
+ *out_x = delta_x.mul_raw(scale);
167
+ *out_y = delta_y.mul_raw(scale);
168
+ true
169
+ }
170
+
171
+ // =============================================================================
172
+ // RQ_ClipCoordinatesWithRef (0x542C70) — perspective with pivot
173
+ // =============================================================================
174
+
175
+ /// `RQ_ClipCoordinatesWithRef` (0x542C70) — perspective clip relative to
176
+ /// a pivot point (read from `clip.pivot_x/pivot_y`).
177
+ ///
178
+ /// The `(cam − pivot)` delta is scaled by the perspective factor and added
179
+ /// back to `pivot`, producing the projected camera position. The input
180
+ /// `(x_in, y_in)` is then subtracted from that projected position with
181
+ /// saturation.
182
+ ///
183
+ /// Used by case 6 (`DRAW_SPRITE_OFFSET`) when `flags & 4` is set.
184
+ pub fn rq_clip_coordinates_with_ref(
185
+ clip: &ClipContext,
186
+ x_in: Fixed,
187
+ y_in: Fixed,
188
+ ref_z: i32,
189
+ out_x: &mut Fixed,
190
+ out_y: &mut Fixed,
191
+ out_scale: &mut Fixed,
192
+ ) -> bool {
193
+ let projected_z = ref_z.wrapping_add(0x0100_0000);
194
+ if projected_z < 1 {
195
+ return false;
196
+ }
197
+ let scale = perspective_scale(projected_z);
198
+ *out_scale = scale;
199
+
200
+ // Project camera position relative to pivot, then subtract from input.
201
+ let cam_x_pixel = clip.cam_x.floor();
202
+ let cam_y_pixel = clip.cam_y.floor();
203
+
204
+ let projected_cam_x = (cam_x_pixel - clip.pivot_x).mul_raw(scale) + clip.pivot_x;
205
+ let projected_cam_y = (cam_y_pixel - clip.pivot_y).mul_raw(scale) + clip.pivot_y;
206
+
207
+ *out_x = sub_saturate(x_in, projected_cam_x);
208
+ *out_y = sub_saturate(y_in, projected_cam_y);
209
+ true
210
+ }
211
+
212
+ /// Saturating signed subtract on `Fixed` values. Used by
213
+ /// `rq_clip_coordinates_with_ref`'s outer subtract, which operates on
214
+ /// already-projected positions where the inputs are no longer guaranteed
215
+ /// pixel-aligned, so it does NOT pre-floor like [`clip_sub_saturate`].
216
+ #[inline]
217
+ fn sub_saturate(in_val: Fixed, cam: Fixed) -> Fixed {
218
+ let delta = Fixed(in_val.0.wrapping_sub(cam.0));
219
+ if in_val.0 < 0 {
220
+ if cam.0 > 0 && delta.0 > 0 {
221
+ return Fixed(i32::MIN);
222
+ }
223
+ } else {
224
+ if cam.0 < 0 && delta.0 < 0 {
225
+ return Fixed(i32::MAX);
226
+ }
227
+ }
228
+ delta
229
+ }
230
+
231
+ // =============================================================================
232
+ // RQ_TranslateCoordinates (0x542B10) — clip with no perspective
233
+ // =============================================================================
234
+
235
+ /// `RQ_TranslateCoordinates` (0x542B10) — same shape as
236
+ /// `rq_clip_coordinates` but without the perspective scaling. Always
237
+ /// succeeds.
238
+ ///
239
+ /// In WA this is `__usercall(ECX = x_in, EAX = y_in, EDI = ClipContext*,
240
+ /// stack[0] = *out_x, stack[1] = *out_y)`. The Ghidra decompile lost the
241
+ /// ECX input (annotated `unaff_EDI` for the context and `in_EAX` for `y`,
242
+ /// silently dropping `x`); verified from the prologue disassembly at
243
+ /// 0x542B1A which masks ECX with `0xFFFF_0000`.
244
+ pub fn rq_translate_coordinates(
245
+ clip: &ClipContext,
246
+ x_in: Fixed,
247
+ y_in: Fixed,
248
+ out_x: &mut Fixed,
249
+ out_y: &mut Fixed,
250
+ ) {
251
+ *out_x = clip_sub_saturate(x_in, clip.cam_x);
252
+ *out_y = clip_sub_saturate(y_in, clip.cam_y);
253
+ }
254
+
255
+ // =============================================================================
256
+ // RenderDrawingQueue (0x542350) — the dispatcher
257
+ // =============================================================================
258
+
259
+ /// `RenderDrawingQueue` (0x542350) — the per-frame render-queue dispatcher.
260
+ ///
261
+ /// See module-level docs for the per-case slot mapping and calling
262
+ /// convention. The Rust signature here flattens the WA usercall into a
263
+ /// plain function: a thin trampoline in `openwa-dll` captures the EAX
264
+ /// register input and forwards the two stack args.
265
+ ///
266
+ /// # Safety
267
+ ///
268
+ /// All three pointers must be valid for the duration of the call. The
269
+ /// render queue's `entry_count` must accurately reflect the number of
270
+ /// `entry_ptrs` slots in use, and each entry pointer must point at a
271
+ /// valid command structure whose first `u32` is a command type in `0..=0xE`
272
+ /// (commands with type `> 0xE` are silently ignored, matching WA — the
273
+ /// `JA 0x542a9e` at 0x54239f branches into the loop tail).
274
+ pub unsafe fn render_drawing_queue(
275
+ rq: *mut RenderQueue,
276
+ display: *mut DisplayGfx,
277
+ clip: *mut ClipContext,
278
+ ) {
279
+ let count = (*rq).entry_count as usize;
280
+ if count == 0 {
281
+ return;
282
+ }
283
+ let entry_ptrs_base = core::ptr::addr_of_mut!((*rq).entry_ptrs) as *mut *mut u8;
284
+ let entries: &mut [*mut u8] = core::slice::from_raw_parts_mut(entry_ptrs_base, count);
285
+
286
+ // Sort by cmd[1] = layer field. The original uses MSVC qsort (unstable);
287
+ // we mirror that with sort_unstable_by_key.
288
+ entries.sort_unstable_by_key(|&p| {
289
+ // SAFETY: entries are valid command pointers; cmd[1] is in range.
290
+ unsafe { *(p as *const i32).add(1) }
291
+ });
292
+
293
+ let clip_ref: &ClipContext = &*clip;
294
+ let vtable = (*display).base.vtable;
295
+
296
+ // Walk from the highest index downward.
297
+ for i in (0..count).rev() {
298
+ let cmd = entries[i] as *const u32;
299
+ let cmd_type = *cmd;
300
+ match cmd_type {
301
+ 0 => dispatch_case_0_fill_rect(display, vtable, clip_ref, cmd),
302
+ 1 => dispatch_case_1_bitmap_global(display, vtable, cmd),
303
+ 2 => dispatch_case_2_textbox_local(display, vtable, clip_ref, cmd),
304
+ 3 => dispatch_case_3_via_callback(display, vtable, clip_ref, cmd),
305
+ 4 => dispatch_case_4_sprite_global(display, vtable, cmd),
306
+ 5 => dispatch_case_5_sprite_local(display, vtable, clip_ref, cmd),
307
+ 6 => dispatch_case_6_sprite_offset(display, vtable, clip_ref, cmd),
308
+ 7 => dispatch_case_7_polyline(display, vtable, cmd),
309
+ 8 => dispatch_case_8_line_strip(display, vtable, clip_ref, cmd),
310
+ 9 => dispatch_case_9_polygon(display, vtable, clip_ref, cmd),
311
+ 0xA => dispatch_case_a_pixel_strip(display, vtable, clip_ref, cmd),
312
+ 0xB => dispatch_case_b_crosshair(display, vtable, clip_ref, cmd),
313
+ 0xC => dispatch_case_c_outlined_pixel(display, vtable, clip_ref, cmd),
314
+ 0xD => dispatch_case_d_tiled_bitmap(display, vtable, clip_ref, cmd),
315
+ 0xE => dispatch_case_e_tiled_terrain(display, vtable, clip_ref, cmd),
316
+ _ => { /* unknown command type — silently skipped, matching WA */ }
317
+ }
318
+ }
319
+ }
320
+
321
+ // =============================================================================
322
+ // Per-case dispatchers
323
+ // =============================================================================
324
+ //
325
+ // Each case body mirrors the corresponding `case N:` arm of WA's
326
+ // `RenderDrawingQueue` (0x542350). Field offsets are the `puVar1[N]`
327
+ // indices from the decompile (each step = 4 bytes).
328
+
329
+ use crate::render::display::vtable::DisplayGfxVtable;
330
+
331
+ #[inline]
332
+ unsafe fn read_field(cmd: *const u32, idx: usize) -> i32 {
333
+ *(cmd.add(idx) as *const i32)
334
+ }
335
+
336
+ #[inline]
337
+ unsafe fn read_fixed(cmd: *const u32, idx: usize) -> Fixed {
338
+ Fixed::from_raw(*(cmd.add(idx) as *const i32))
339
+ }
340
+
341
+ // ---- case 0: DRAW_RECT → slot 18 (fill_rect) -------------------------------
342
+ //
343
+ // iVar4 = RQ_ClipCoordinates(this, cmd[3], cmd[4], cmd[7], &local_18, &param_1, ..);
344
+ // if !iVar4 break;
345
+ // iVar4 = RQ_ClipCoordinates(this, cmd[5], cmd[6], cmd[7], &local_10, &local_c, ..);
346
+ // if !iVar4 break;
347
+ // // x/y MIN-corner clamps to keep pixel-perfect rect edges
348
+ // vtable[18](this, x1>>16, y1>>16, x2>>16, y2>>16, cmd[2]);
349
+ unsafe fn dispatch_case_0_fill_rect(
350
+ display: *mut DisplayGfx,
351
+ vtable: *const DisplayGfxVtable,
352
+ clip: &ClipContext,
353
+ cmd: *const u32,
354
+ ) {
355
+ let mut x1 = Fixed::ZERO;
356
+ let mut y1 = Fixed::ZERO;
357
+ let mut x2 = Fixed::ZERO;
358
+ let mut y2 = Fixed::ZERO;
359
+ let mut scale = Fixed::ZERO;
360
+ if !rq_clip_coordinates(
361
+ clip,
362
+ read_fixed(cmd, 3),
363
+ read_fixed(cmd, 4),
364
+ read_field(cmd, 7),
365
+ &mut x1,
366
+ &mut y1,
367
+ &mut scale,
368
+ ) {
369
+ return;
370
+ }
371
+ if !rq_clip_coordinates(
372
+ clip,
373
+ read_fixed(cmd, 5),
374
+ read_fixed(cmd, 6),
375
+ read_field(cmd, 7),
376
+ &mut x2,
377
+ &mut y2,
378
+ &mut scale,
379
+ ) {
380
+ return;
381
+ }
382
+
383
+ // Pixel-perfect MIN/MAX rect-edge clamping when the source is at the
384
+ // sentinel anchor values: cmd[3]/cmd[4] == -0x80000000 → clamp to MIN,
385
+ // cmd[5]/cmd[6] == 0x7FFF0000 → clamp to MAX. WA uses these to draw
386
+ // "infinite" rectangles in screen space.
387
+ if read_field(cmd, 3) == i32::MIN {
388
+ x1 = Fixed(i32::MIN);
389
+ }
390
+ if read_field(cmd, 4) == i32::MIN {
391
+ y1 = Fixed(i32::MIN);
392
+ }
393
+ if read_field(cmd, 5) == 0x7FFF_0000 {
394
+ x2 = Fixed(i32::MAX);
395
+ }
396
+ if read_field(cmd, 6) == 0x7FFF_0000 {
397
+ y2 = Fixed(i32::MAX);
398
+ }
399
+
400
+ let color = *(cmd.add(2));
401
+ ((*vtable).fill_rect)(
402
+ display,
403
+ x1.to_int(),
404
+ y1.to_int(),
405
+ x2.to_int(),
406
+ y2.to_int(),
407
+ color,
408
+ );
409
+ }
410
+
411
+ // ---- case 1: DRAW_BITMAP_GLOBAL → slot 20 (draw_scaled_sprite) -------------
412
+ //
413
+ // vtable[20](this, cmd[2], cmd[3], cmd[4], cmd[5], cmd[6], cmd[7], cmd[8], cmd[9]);
414
+ //
415
+ // Pure passthrough — global bitmaps draw at the supplied world coords.
416
+ unsafe fn dispatch_case_1_bitmap_global(
417
+ display: *mut DisplayGfx,
418
+ vtable: *const DisplayGfxVtable,
419
+ cmd: *const u32,
420
+ ) {
421
+ use crate::bitgrid::DisplayBitGrid;
422
+ let x = read_fixed(cmd, 2);
423
+ let y = read_fixed(cmd, 3);
424
+ let sprite = read_field(cmd, 4) as *mut DisplayBitGrid;
425
+ let src_x = read_field(cmd, 5);
426
+ let src_y = read_field(cmd, 6);
427
+ let src_w = read_field(cmd, 7);
428
+ let src_h = read_field(cmd, 8);
429
+ let flags = *(cmd.add(9));
430
+ ((*vtable).draw_scaled_sprite)(display, x, y, sprite, src_x, src_y, src_w, src_h, flags);
431
+ }
432
+
433
+ // ---- case 2: DRAW_TEXTBOX_LOCAL → slot 20 (draw_scaled_sprite) -------------
434
+ //
435
+ // `cmd[2] == 0` selects the simple variant (one clip call); otherwise the
436
+ // box has separate top-edge / bottom-edge clip targets and a flag byte at
437
+ // `cmd+8` controlling whether the result clamps to the second clip pair.
438
+ unsafe fn dispatch_case_2_textbox_local(
439
+ display: *mut DisplayGfx,
440
+ vtable: *const DisplayGfxVtable,
441
+ clip: &ClipContext,
442
+ cmd: *const u32,
443
+ ) {
444
+ use crate::bitgrid::DisplayBitGrid;
445
+ let mode = *(cmd.add(2));
446
+ let mut x1 = Fixed::ZERO;
447
+ let mut y1 = Fixed::ZERO;
448
+ let mut scale = Fixed::ZERO;
449
+ if mode == 0 {
450
+ if !rq_clip_coordinates(
451
+ clip,
452
+ read_fixed(cmd, 3),
453
+ read_fixed(cmd, 4),
454
+ read_field(cmd, 5),
455
+ &mut x1,
456
+ &mut y1,
457
+ &mut scale,
458
+ ) {
459
+ return;
460
+ }
461
+ } else {
462
+ if !rq_clip_coordinates(
463
+ clip,
464
+ read_fixed(cmd, 3),
465
+ read_fixed(cmd, 4),
466
+ read_field(cmd, 5),
467
+ &mut x1,
468
+ &mut y1,
469
+ &mut scale,
470
+ ) {
471
+ return;
472
+ }
473
+ let mut x2 = Fixed::ZERO;
474
+ let mut y2 = Fixed::ZERO;
475
+ if !rq_clip_coordinates(
476
+ clip,
477
+ read_fixed(cmd, 3),
478
+ read_fixed(cmd, 4),
479
+ read_field(cmd, 6),
480
+ &mut x2,
481
+ &mut y2,
482
+ &mut scale,
483
+ ) {
484
+ return;
485
+ }
486
+ let flag_byte = *(cmd.add(2)) as u8;
487
+ if flag_byte & 1 != 0 {
488
+ x1 = x2;
489
+ }
490
+ if flag_byte & 2 != 0 {
491
+ y1 = y2;
492
+ }
493
+ }
494
+ let sprite = read_field(cmd, 7) as *mut DisplayBitGrid;
495
+ let src_x = read_field(cmd, 8);
496
+ let src_y = read_field(cmd, 9);
497
+ let src_w = read_field(cmd, 10);
498
+ let src_h = read_field(cmd, 11);
499
+ let flags = *(cmd.add(12));
500
+ ((*vtable).draw_scaled_sprite)(display, x1, y1, sprite, src_x, src_y, src_w, src_h, flags);
501
+ }
502
+
503
+ // ---- case 3 → slot 21 (draw_via_callback) ----------------------------------
504
+ //
505
+ // iVar4 = RQ_ClipCoordinates(this, cmd[2], cmd[3], cmd[4], &x, &y, ..);
506
+ // if !iVar4 break;
507
+ // vtable[21](this, x, y, cmd[6], cmd[7], cmd[8]);
508
+ unsafe fn dispatch_case_3_via_callback(
509
+ display: *mut DisplayGfx,
510
+ vtable: *const DisplayGfxVtable,
511
+ clip: &ClipContext,
512
+ cmd: *const u32,
513
+ ) {
514
+ let mut x = Fixed::ZERO;
515
+ let mut y = Fixed::ZERO;
516
+ let mut scale = Fixed::ZERO;
517
+ if !rq_clip_coordinates(
518
+ clip,
519
+ read_fixed(cmd, 2),
520
+ read_fixed(cmd, 3),
521
+ read_field(cmd, 4),
522
+ &mut x,
523
+ &mut y,
524
+ &mut scale,
525
+ ) {
526
+ return;
527
+ }
528
+ let obj = read_field(cmd, 6) as *mut u8;
529
+ let p5 = *(cmd.add(7));
530
+ let p6 = *(cmd.add(8));
531
+ ((*vtable).draw_via_callback)(display, x, y, obj, p5, p6);
532
+ }
533
+
534
+ // ---- case 4: DRAW_SPRITE_GLOBAL → slot 19 (blit_sprite) --------------------
535
+ //
536
+ // vtable[19](this, cmd[2], cmd[3], cmd[4], cmd[5]);
537
+ unsafe fn dispatch_case_4_sprite_global(
538
+ display: *mut DisplayGfx,
539
+ vtable: *const DisplayGfxVtable,
540
+ cmd: *const u32,
541
+ ) {
542
+ let x = read_fixed(cmd, 2);
543
+ let y = read_fixed(cmd, 3);
544
+ let sprite_flags = *(cmd.add(4));
545
+ let palette = *(cmd.add(5));
546
+ ((*vtable).blit_sprite)(display, x, y, sprite_flags, palette);
547
+ }
548
+
549
+ // ---- case 5: DRAW_SPRITE_LOCAL → slot 19 (blit_sprite) ---------------------
550
+ //
551
+ // RQ_TranslateCoordinates(clip, cmd[2], cmd[3], &x, &y);
552
+ // vtable[19](this, x, y, cmd[4], cmd[5]);
553
+ unsafe fn dispatch_case_5_sprite_local(
554
+ display: *mut DisplayGfx,
555
+ vtable: *const DisplayGfxVtable,
556
+ clip: &ClipContext,
557
+ cmd: *const u32,
558
+ ) {
559
+ let mut x = Fixed::ZERO;
560
+ let mut y = Fixed::ZERO;
561
+ rq_translate_coordinates(clip, read_fixed(cmd, 2), read_fixed(cmd, 3), &mut x, &mut y);
562
+ let sprite_flags = *(cmd.add(4));
563
+ let palette = *(cmd.add(5));
564
+ ((*vtable).blit_sprite)(display, x, y, sprite_flags, palette);
565
+ }
566
+
567
+ // ---- case 6: DRAW_SPRITE_OFFSET → slot 19 (blit_sprite) --------------------
568
+ //
569
+ // `cmd[2]` is a flag byte: bit 0/1 are top/bottom-edge clamps, bit 2 selects
570
+ // the perspective-with-pivot helper instead of the plain perspective clip.
571
+ // `cmd[2] == 0` selects the simple translate-only path used for purely
572
+ // screen-space offset sprites.
573
+ unsafe fn dispatch_case_6_sprite_offset(
574
+ display: *mut DisplayGfx,
575
+ vtable: *const DisplayGfxVtable,
576
+ clip: &ClipContext,
577
+ cmd: *const u32,
578
+ ) {
579
+ let flags = *(cmd.add(2));
580
+
581
+ let mut x = Fixed::ZERO;
582
+ let mut y = Fixed::ZERO;
583
+ let mut scale = Fixed::ZERO;
584
+
585
+ if flags == 0 {
586
+ // Simple translate path (no perspective).
587
+ rq_translate_coordinates(clip, read_fixed(cmd, 3), read_fixed(cmd, 4), &mut x, &mut y);
588
+ } else {
589
+ // Two clip calls (top + bottom Y) with optional MIN/MAX clamps.
590
+ let use_pivot = (flags & 4) != 0;
591
+ let clip_fn: fn(
592
+ &ClipContext,
593
+ Fixed,
594
+ Fixed,
595
+ i32,
596
+ &mut Fixed,
597
+ &mut Fixed,
598
+ &mut Fixed,
599
+ ) -> bool = if use_pivot {
600
+ rq_clip_coordinates_with_ref
601
+ } else {
602
+ rq_clip_coordinates
603
+ };
604
+ if !clip_fn(
605
+ clip,
606
+ read_fixed(cmd, 3),
607
+ read_fixed(cmd, 4),
608
+ read_field(cmd, 5),
609
+ &mut x,
610
+ &mut y,
611
+ &mut scale,
612
+ ) {
613
+ return;
614
+ }
615
+ let mut x2 = Fixed::ZERO;
616
+ let mut y2 = Fixed::ZERO;
617
+ if !clip_fn(
618
+ clip,
619
+ read_fixed(cmd, 3),
620
+ read_fixed(cmd, 4),
621
+ read_field(cmd, 6),
622
+ &mut x2,
623
+ &mut y2,
624
+ &mut scale,
625
+ ) {
626
+ return;
627
+ }
628
+ let flag_byte = *(cmd.add(2)) as u8;
629
+ if flag_byte & 1 != 0 {
630
+ x = x2;
631
+ }
632
+ if flag_byte & 2 != 0 {
633
+ y = y2;
634
+ }
635
+ }
636
+
637
+ let sprite_flags = *(cmd.add(7));
638
+ let palette = *(cmd.add(8));
639
+ ((*vtable).blit_sprite)(display, x, y, sprite_flags, palette);
640
+ }
641
+
642
+ // ---- case 7 → slot 12 (draw_polyline) --------------------------------------
643
+ //
644
+ // vtable[12](this, &cmd[4], cmd[2], cmd[3]);
645
+ //
646
+ // `cmd[2]` is the vertex count, `cmd[3]` is the color, vertex data
647
+ // follows starting at `cmd[4]`.
648
+ unsafe fn dispatch_case_7_polyline(
649
+ display: *mut DisplayGfx,
650
+ vtable: *const DisplayGfxVtable,
651
+ cmd: *const u32,
652
+ ) {
653
+ let points = cmd.add(4) as *mut i32;
654
+ let count = read_field(cmd, 2);
655
+ let color = *(cmd.add(3));
656
+ ((*vtable).draw_polyline)(display, points, count, color);
657
+ }
658
+
659
+ // ---- case 8: DRAW_LINE_STRIP → slot 14 (draw_line_clipped) -----------------
660
+ //
661
+ // First vertex at `cmd[4..6]` plus `cmd[6]` (z), then iterate
662
+ // `(count - 1)` more vertices and call slot 14 for each segment.
663
+ // `cmd[3]` is the color shared by all segments.
664
+ unsafe fn dispatch_case_8_line_strip(
665
+ display: *mut DisplayGfx,
666
+ vtable: *const DisplayGfxVtable,
667
+ clip: &ClipContext,
668
+ cmd: *const u32,
669
+ ) {
670
+ let mut x1 = Fixed::ZERO;
671
+ let mut y1 = Fixed::ZERO;
672
+ let mut scale = Fixed::ZERO;
673
+ if !rq_clip_coordinates(
674
+ clip,
675
+ read_fixed(cmd, 4),
676
+ read_fixed(cmd, 5),
677
+ read_field(cmd, 6),
678
+ &mut x1,
679
+ &mut y1,
680
+ &mut scale,
681
+ ) {
682
+ return;
683
+ }
684
+ let count = read_field(cmd, 2);
685
+ let color = *(cmd.add(3));
686
+ if count <= 1 {
687
+ return;
688
+ }
689
+ // Vertex N is at cmd[8 + (N-1)*3 .. 8 + (N-1)*3 + 3], a 3-tuple of (x, y, z).
690
+ // Walk N = 1..count.
691
+ let mut vert = cmd.add(8) as *const u32;
692
+ for _ in 1..count {
693
+ let mut x2 = Fixed::ZERO;
694
+ let mut y2 = Fixed::ZERO;
695
+ if !rq_clip_coordinates(
696
+ clip,
697
+ Fixed::from_raw(*(vert.sub(1)) as i32),
698
+ Fixed::from_raw(*vert as i32),
699
+ *(vert.add(1)) as i32,
700
+ &mut x2,
701
+ &mut y2,
702
+ &mut scale,
703
+ ) {
704
+ break;
705
+ }
706
+ ((*vtable).draw_line_clipped)(display, x1, y1, x2, y2, color);
707
+ x1 = x2;
708
+ y1 = y2;
709
+ vert = vert.add(3);
710
+ }
711
+ }
712
+
713
+ // ---- case 9: DRAW_POLYGON → slot 13 (draw_line) ----------------------------
714
+ //
715
+ // Same shape as case 8 but with two color params (cmd[3], cmd[4]) and the
716
+ // first vertex starts at `cmd[5..8]`. Per-segment vertex stride is 3 u32s.
717
+ unsafe fn dispatch_case_9_polygon(
718
+ display: *mut DisplayGfx,
719
+ vtable: *const DisplayGfxVtable,
720
+ clip: &ClipContext,
721
+ cmd: *const u32,
722
+ ) {
723
+ let mut x1 = Fixed::ZERO;
724
+ let mut y1 = Fixed::ZERO;
725
+ let mut scale = Fixed::ZERO;
726
+ if !rq_clip_coordinates(
727
+ clip,
728
+ read_fixed(cmd, 5),
729
+ read_fixed(cmd, 6),
730
+ read_field(cmd, 7),
731
+ &mut x1,
732
+ &mut y1,
733
+ &mut scale,
734
+ ) {
735
+ return;
736
+ }
737
+ let count = read_field(cmd, 2);
738
+ let color1 = *(cmd.add(3));
739
+ let color2 = *(cmd.add(4));
740
+ if count <= 1 {
741
+ return;
742
+ }
743
+ let mut vert = cmd.add(9) as *const u32;
744
+ for _ in 1..count {
745
+ let mut x2 = Fixed::ZERO;
746
+ let mut y2 = Fixed::ZERO;
747
+ if !rq_clip_coordinates(
748
+ clip,
749
+ Fixed::from_raw(*(vert.sub(1)) as i32),
750
+ Fixed::from_raw(*vert as i32),
751
+ *(vert.add(1)) as i32,
752
+ &mut x2,
753
+ &mut y2,
754
+ &mut scale,
755
+ ) {
756
+ break;
757
+ }
758
+ ((*vtable).draw_line)(display, x1, y1, x2, y2, color1, color2);
759
+ x1 = x2;
760
+ y1 = y2;
761
+ vert = vert.add(3);
762
+ }
763
+ }
764
+
765
+ // ---- case 0xA → slot 15 (draw_pixel_strip) ---------------------------------
766
+ //
767
+ // RQ_TranslateCoordinates(clip, cmd[2], cmd[3], &x, &y);
768
+ // // The translate's `clip_sub_saturate` floor()s its inputs, so its
769
+ // // output drops the fractional 16 bits of cmd[2]/cmd[3]. The dispatcher
770
+ // // re-adds those fractional bits back to recover pixel-fractional precision:
771
+ // x += cmd[2] & 0xFFFF;
772
+ // y += cmd[3] & 0xFFFF;
773
+ // vtable[15](this, x, y, cmd[4], cmd[5], cmd[6], cmd[7]);
774
+ unsafe fn dispatch_case_a_pixel_strip(
775
+ display: *mut DisplayGfx,
776
+ vtable: *const DisplayGfxVtable,
777
+ clip: &ClipContext,
778
+ cmd: *const u32,
779
+ ) {
780
+ let mut x = Fixed::ZERO;
781
+ let mut y = Fixed::ZERO;
782
+ rq_translate_coordinates(clip, read_fixed(cmd, 2), read_fixed(cmd, 3), &mut x, &mut y);
783
+ let cmd_x = *(cmd.add(2));
784
+ let cmd_y = *(cmd.add(3));
785
+ x = x + Fixed::from_raw((cmd_x & 0xFFFF) as i32);
786
+ y = y + Fixed::from_raw((cmd_y & 0xFFFF) as i32);
787
+ let dx = read_fixed(cmd, 4);
788
+ let dy = read_fixed(cmd, 5);
789
+ let count = read_field(cmd, 6);
790
+ let color = *(cmd.add(7));
791
+ ((*vtable).draw_pixel_strip)(display, x, y, dx, dy, count, color);
792
+ }
793
+
794
+ // ---- case 0xB: DRAW_CROSSHAIR → slot 16 (draw_crosshair) -------------------
795
+ //
796
+ // iVar4 = RQ_ClipCoordinates(this, cmd[4], cmd[5], cmd[6], &x, &y, ..);
797
+ // if !iVar4 break;
798
+ // vtable[16](this, x>>16, y>>16, cmd[2], cmd[3]);
799
+ unsafe fn dispatch_case_b_crosshair(
800
+ display: *mut DisplayGfx,
801
+ vtable: *const DisplayGfxVtable,
802
+ clip: &ClipContext,
803
+ cmd: *const u32,
804
+ ) {
805
+ let mut x = Fixed::ZERO;
806
+ let mut y = Fixed::ZERO;
807
+ let mut scale = Fixed::ZERO;
808
+ if !rq_clip_coordinates(
809
+ clip,
810
+ read_fixed(cmd, 4),
811
+ read_fixed(cmd, 5),
812
+ read_field(cmd, 6),
813
+ &mut x,
814
+ &mut y,
815
+ &mut scale,
816
+ ) {
817
+ return;
818
+ }
819
+ let color_fg = *(cmd.add(2));
820
+ let color_bg = *(cmd.add(3));
821
+ ((*vtable).draw_crosshair)(display, x.to_int(), y.to_int(), color_fg, color_bg);
822
+ }
823
+
824
+ // ---- case 0xC → slot 17 (draw_outlined_pixel) ------------------------------
825
+ //
826
+ // iVar4 = RQ_ClipCoordinates(this, cmd[4], cmd[5], cmd[6], &x, &y, ..);
827
+ // if !iVar4 break;
828
+ // vtable[17](this, x>>16, y>>16, cmd[2], cmd[3]);
829
+ unsafe fn dispatch_case_c_outlined_pixel(
830
+ display: *mut DisplayGfx,
831
+ vtable: *const DisplayGfxVtable,
832
+ clip: &ClipContext,
833
+ cmd: *const u32,
834
+ ) {
835
+ let mut x = Fixed::ZERO;
836
+ let mut y = Fixed::ZERO;
837
+ let mut scale = Fixed::ZERO;
838
+ if !rq_clip_coordinates(
839
+ clip,
840
+ read_fixed(cmd, 4),
841
+ read_fixed(cmd, 5),
842
+ read_field(cmd, 6),
843
+ &mut x,
844
+ &mut y,
845
+ &mut scale,
846
+ ) {
847
+ return;
848
+ }
849
+ let color_fg = *(cmd.add(2));
850
+ let color_bg = read_field(cmd, 3);
851
+ ((*vtable).draw_outlined_pixel)(display, x.to_int(), y.to_int(), color_fg, color_bg);
852
+ }
853
+
854
+ // ---- case 0xD: DRAW_TILED_BITMAP → slot 11 (draw_tiled_bitmap) -------------
855
+ //
856
+ // iVar4 = RQ_ClipCoordinates(this, 0, cmd[2], cmd[3], &x, &y, ..);
857
+ // if !iVar4 break;
858
+ // // Bit 0 of the flag byte at cmd[5] forces dest_x to 0 instead of x>>16.
859
+ // vtable[11](this, (flag ? 0 : x>>16), y>>16, cmd[4]);
860
+ unsafe fn dispatch_case_d_tiled_bitmap(
861
+ display: *mut DisplayGfx,
862
+ vtable: *const DisplayGfxVtable,
863
+ clip: &ClipContext,
864
+ cmd: *const u32,
865
+ ) {
866
+ use crate::render::display::vtable::TiledBitmapSource;
867
+ let mut x = Fixed::ZERO;
868
+ let mut y = Fixed::ZERO;
869
+ let mut scale = Fixed::ZERO;
870
+ if !rq_clip_coordinates(
871
+ clip,
872
+ Fixed::ZERO,
873
+ read_fixed(cmd, 2),
874
+ read_field(cmd, 3),
875
+ &mut x,
876
+ &mut y,
877
+ &mut scale,
878
+ ) {
879
+ return;
880
+ }
881
+ let flag_byte = *(cmd.add(5)) as u8;
882
+ let dest_x = if flag_byte != 0 { 0 } else { x.to_int() };
883
+ let source = read_field(cmd, 4) as *const TiledBitmapSource;
884
+ ((*vtable).draw_tiled_bitmap)(display, dest_x, y.to_int(), source);
885
+ }
886
+
887
+ // ---- case 0xE → slot 22 (draw_tiled_terrain) -------------------------------
888
+ //
889
+ // Same shape as case 2: `cmd[2] == 0` selects the one-clip variant; non-zero
890
+ // runs the two-clip path with the same flag-byte clamps.
891
+ unsafe fn dispatch_case_e_tiled_terrain(
892
+ display: *mut DisplayGfx,
893
+ vtable: *const DisplayGfxVtable,
894
+ clip: &ClipContext,
895
+ cmd: *const u32,
896
+ ) {
897
+ let mode = *(cmd.add(2));
898
+ let mut x = Fixed::ZERO;
899
+ let mut y = Fixed::ZERO;
900
+ let mut scale = Fixed::ZERO;
901
+ if mode == 0 {
902
+ if !rq_clip_coordinates(
903
+ clip,
904
+ read_fixed(cmd, 3),
905
+ read_fixed(cmd, 4),
906
+ read_field(cmd, 5),
907
+ &mut x,
908
+ &mut y,
909
+ &mut scale,
910
+ ) {
911
+ return;
912
+ }
913
+ } else {
914
+ if !rq_clip_coordinates(
915
+ clip,
916
+ read_fixed(cmd, 3),
917
+ read_fixed(cmd, 4),
918
+ read_field(cmd, 5),
919
+ &mut x,
920
+ &mut y,
921
+ &mut scale,
922
+ ) {
923
+ return;
924
+ }
925
+ let mut x2 = Fixed::ZERO;
926
+ let mut y2 = Fixed::ZERO;
927
+ if !rq_clip_coordinates(
928
+ clip,
929
+ read_fixed(cmd, 3),
930
+ read_fixed(cmd, 4),
931
+ read_field(cmd, 6),
932
+ &mut x2,
933
+ &mut y2,
934
+ &mut scale,
935
+ ) {
936
+ return;
937
+ }
938
+ let flag_byte = *(cmd.add(2)) as u8;
939
+ if flag_byte & 1 != 0 {
940
+ x = x2;
941
+ }
942
+ if flag_byte & 2 != 0 {
943
+ y = y2;
944
+ }
945
+ }
946
+ let count = read_field(cmd, 7);
947
+ let flags = *(cmd.add(8));
948
+ ((*vtable).draw_tiled_terrain)(display, x, y, count, flags);
949
+ }
950
+
951
+ // =============================================================================
952
+ // Unit tests for the helpers
953
+ // =============================================================================
954
+
955
+ #[cfg(test)]
956
+ mod tests {
957
+ use super::*;
958
+
959
+ fn zero_clip() -> ClipContext {
960
+ ClipContext {
961
+ cam_x: Fixed::ZERO,
962
+ cam_y: Fixed::ZERO,
963
+ pivot_x: Fixed::ZERO,
964
+ pivot_y: Fixed::ZERO,
965
+ }
966
+ }
967
+
968
+ #[test]
969
+ fn perspective_scale_at_zero_z() {
970
+ // 0x100_0000 / 0x100_0000 = 0x10000 (Fixed16 1.0)
971
+ assert_eq!(perspective_scale(0x0100_0000), Fixed::ONE);
972
+ }
973
+
974
+ #[test]
975
+ fn clip_sub_saturate_no_overflow() {
976
+ // 5.0 - 2.0 = 3.0 in fixed16
977
+ assert_eq!(
978
+ clip_sub_saturate(Fixed::from_int(5), Fixed::from_int(2)),
979
+ Fixed::from_int(3)
980
+ );
981
+ }
982
+
983
+ #[test]
984
+ fn clip_sub_saturate_underflow_clamps_to_min() {
985
+ // x_in negative, cam positive, delta wraps positive → MIN
986
+ assert_eq!(
987
+ clip_sub_saturate(Fixed(i32::MIN), Fixed::from_raw(0x4000_0000)),
988
+ Fixed(i32::MIN)
989
+ );
990
+ }
991
+
992
+ #[test]
993
+ fn clip_sub_saturate_overflow_clamps_to_max() {
994
+ // x_in positive, cam negative, delta wraps negative → MAX
995
+ assert_eq!(
996
+ clip_sub_saturate(
997
+ Fixed::from_raw(0x4000_0000),
998
+ Fixed::from_raw(i32::MIN + 0x10000)
999
+ ),
1000
+ Fixed(i32::MAX)
1001
+ );
1002
+ }
1003
+
1004
+ #[test]
1005
+ fn clip_coordinates_at_z_zero_is_identity() {
1006
+ let clip = zero_clip();
1007
+ let mut ox = Fixed::ZERO;
1008
+ let mut oy = Fixed::ZERO;
1009
+ let mut scale = Fixed::ZERO;
1010
+ let ok = rq_clip_coordinates(
1011
+ &clip,
1012
+ Fixed::from_int(5),
1013
+ Fixed::from_int(7),
1014
+ 0,
1015
+ &mut ox,
1016
+ &mut oy,
1017
+ &mut scale,
1018
+ );
1019
+ assert!(ok);
1020
+ // scale = 0x100_0000 / 0x100_0000 = 0x10000 (1.0 in fixed16)
1021
+ assert_eq!(scale, Fixed::ONE);
1022
+ assert_eq!(ox, Fixed::from_int(5));
1023
+ assert_eq!(oy, Fixed::from_int(7));
1024
+ }
1025
+
1026
+ #[test]
1027
+ fn clip_coordinates_negative_z_returns_false() {
1028
+ let clip = zero_clip();
1029
+ let mut ox = Fixed::ZERO;
1030
+ let mut oy = Fixed::ZERO;
1031
+ let mut scale = Fixed::ZERO;
1032
+ let ok = rq_clip_coordinates(
1033
+ &clip,
1034
+ Fixed::from_int(5),
1035
+ Fixed::from_int(7),
1036
+ -0x100_0001,
1037
+ &mut ox,
1038
+ &mut oy,
1039
+ &mut scale,
1040
+ );
1041
+ assert!(!ok);
1042
+ }
1043
+
1044
+ #[test]
1045
+ fn translate_coordinates_camera_offset() {
1046
+ let clip = ClipContext {
1047
+ cam_x: Fixed::from_int(10),
1048
+ cam_y: Fixed::from_int(20),
1049
+ pivot_x: Fixed::ZERO,
1050
+ pivot_y: Fixed::ZERO,
1051
+ };
1052
+ let mut ox = Fixed::ZERO;
1053
+ let mut oy = Fixed::ZERO;
1054
+ rq_translate_coordinates(
1055
+ &clip,
1056
+ Fixed::from_int(30),
1057
+ Fixed::from_int(50),
1058
+ &mut ox,
1059
+ &mut oy,
1060
+ );
1061
+ assert_eq!(ox, Fixed::from_int(20));
1062
+ assert_eq!(oy, Fixed::from_int(30));
1063
+ }
1064
+ }
crates/openwa-dll/src/hook.rs CHANGED
@@ -80,6 +80,25 @@ macro_rules! usercall_trampoline {
80
  }
81
  };
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  // 1 register arg, 3 stack params, ret N
84
  (fn $name:ident; impl_fn = $impl:path; reg = $reg:ident;
85
  stack_params = 3; ret_bytes = $ret:literal) => {
 
80
  }
81
  };
82
 
83
+ // 1 register arg, 2 stack params, ret N
84
+ (fn $name:ident; impl_fn = $impl:path; reg = $reg:ident;
85
+ stack_params = 2; ret_bytes = $ret:literal) => {
86
+ #[unsafe(naked)]
87
+ unsafe extern "C" fn $name() {
88
+ core::arch::naked_asm!(
89
+ "push edx",
90
+ "push [esp+12]",
91
+ "push [esp+12]",
92
+ concat!("push ", stringify!($reg)),
93
+ "call {impl_fn}",
94
+ "add esp, 12",
95
+ "pop edx",
96
+ concat!("ret ", $ret),
97
+ impl_fn = sym $impl,
98
+ );
99
+ }
100
+ };
101
+
102
  // 1 register arg, 3 stack params, ret N
103
  (fn $name:ident; impl_fn = $impl:path; reg = $reg:ident;
104
  stack_params = 3; ret_bytes = $ret:literal) => {
crates/openwa-dll/src/replacements/render.rs CHANGED
@@ -676,10 +676,39 @@ fn install_render_queue() -> Result<(), String> {
676
  va::DRAW_CROSSHAIR_LINE,
677
  trampoline_draw_crosshair_line as *const (),
678
  )?;
 
 
 
 
 
 
679
  }
680
  Ok(())
681
  }
682
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
683
  // ==========================================================================
684
  // DisplayGfx vtable hooks
685
  // ==========================================================================
 
676
  va::DRAW_CROSSHAIR_LINE,
677
  trampoline_draw_crosshair_line as *const (),
678
  )?;
679
+
680
+ let _ = hook::install(
681
+ "RenderDrawingQueue",
682
+ va::RQ_RENDER_DRAWING_QUEUE,
683
+ trampoline_render_drawing_queue as *const (),
684
+ )?;
685
  }
686
  Ok(())
687
  }
688
 
689
+ // ---------------------------------------------------------------------------
690
+ // RenderDrawingQueue (0x542350) — usercall(EAX=RenderQueue*) + 2 stack
691
+ // (DisplayGfx*, ClipContext*), RET 0x8.
692
+ //
693
+ // The per-frame render-queue dispatcher. Pure Rust port lives in
694
+ // `openwa_core::render::queue_dispatch::render_drawing_queue`. The
695
+ // trampoline captures EAX and forwards the two stack args.
696
+ // ---------------------------------------------------------------------------
697
+
698
+ use openwa_core::render::queue_dispatch::{render_drawing_queue, ClipContext};
699
+
700
+ usercall_trampoline!(fn trampoline_render_drawing_queue;
701
+ impl_fn = render_drawing_queue_impl;
702
+ reg = eax; stack_params = 2; ret_bytes = "0x8");
703
+
704
+ unsafe extern "cdecl" fn render_drawing_queue_impl(
705
+ rq: *mut RenderQueue,
706
+ display: *mut DisplayGfx,
707
+ clip: *mut ClipContext,
708
+ ) {
709
+ render_drawing_queue(rq, display, clip);
710
+ }
711
+
712
  // ==========================================================================
713
  // DisplayGfx vtable hooks
714
  // ==========================================================================