paavohtl Claude Opus 4.6 (1M context) commited on
Commit
c75775a
·
1 Parent(s): 382f027

refactor: split render.rs, move game logic to core, type command structs

Browse files

Major restructuring of the render subsystem:

Module split — DLL render.rs (1463 lines) split into:
- render/render_queue.rs: enqueue hook trampolines + install
- render/display_vtable.rs: vtable patch stubs + install
- render/mod.rs: thin re-export

Game logic moved to openwa-core (~1140 lines):
- bitgrid/blit.rs: blit routing (8bpp, 1-bit, generic fallback)
- render/display/blit_sprite.rs: BlitSprite + DrawScaledSprite
- render/bungee_trail.rs: bungee trail rendering
- render/crosshair_line.rs: crosshair line rendering
- render/display/destructor.rs: headless_destructor
- trig.rs: interpolated trig table lookup (sin_lookup, cos_lookup)

Render command struct type improvements:
- Position fields: u32 → Fixed (repr-transparent, layout-compatible)
- Sprite fields: u32 → SpriteOp in DrawSpriteOffsetCmd
- Pointer fields: u32 → *mut DisplayBitGrid / *const TiledBitmapSource
- Named all param_N/_reserved fields from dispatcher semantics
(ref_z, src_x/y/w/h, color, mode, flags, etc.)

Also deduplicates fixed_mul in weapon_fire.rs via Fixed::mul_raw.

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

crates/openwa-core/src/bitgrid/blit.rs ADDED
@@ -0,0 +1,385 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! BitGrid blit operations — sprite blitting on raw BitGrid pointers.
2
+ //!
3
+ //! These functions bridge raw `*mut DisplayBitGrid` pointers to the safe
4
+ //! `PixelGrid`/`BlitSource` abstractions in `render::display::sprite_blit`.
5
+
6
+ use crate::bitgrid::DisplayBitGrid;
7
+ use crate::render::display::line_draw::PixelGridMut;
8
+ use crate::render::display::sprite_blit::{
9
+ blit_sprite_rect, blit_stippled, blit_tiled, BlitBlend, BlitOrientation, BlitSource,
10
+ };
11
+
12
+ /// Rust implementation of the core sprite blit (BitGrid__BlitSpriteRect, 0x4F6910).
13
+ ///
14
+ /// Handles all blend modes for both 8bpp and 1-bit surfaces:
15
+ /// - 8bpp modes 0-3: via `blit_sprite_rect` (orientation-aware, PixelGrid wrapper)
16
+ /// - 1-bit byte-aligned fast path: memcpy/OR/nop for modes 0-3 with Normal orientation
17
+ /// - Generic per-pixel fallback: handles all remaining cases (unaligned 1-bit, modes 4-5,
18
+ /// mixed cpp, etc.) via `blit_generic_perpixel`
19
+ pub unsafe fn blit_impl(
20
+ dst: *mut DisplayBitGrid,
21
+ dst_x: i32,
22
+ dst_y: i32,
23
+ width: i32,
24
+ height: i32,
25
+ src: *mut DisplayBitGrid,
26
+ src_x: i32,
27
+ src_y: i32,
28
+ color_table: *const u8,
29
+ flags: u32,
30
+ ) -> u32 {
31
+ if width == 0 || height == 0 {
32
+ return 0;
33
+ }
34
+
35
+ let blend_mode = flags & 0xFFFF;
36
+ let src_cpp = (*src).cells_per_unit;
37
+ let dst_cpp = (*dst).cells_per_unit;
38
+
39
+ // Fast path: 8bpp surfaces with blend modes 0-3
40
+ if dst_cpp == 8 && src_cpp == 8 && blend_mode <= 3 {
41
+ return blit_8bpp(
42
+ dst,
43
+ dst_x,
44
+ dst_y,
45
+ width,
46
+ height,
47
+ src,
48
+ src_x,
49
+ src_y,
50
+ color_table,
51
+ flags,
52
+ );
53
+ }
54
+
55
+ // --- Clipping (shared by 1-bit fast path and generic fallback) ---
56
+ // This mirrors the clipping logic at the top of BitGrid__BlitSpriteRect.
57
+ let dst_right = dst_x + width;
58
+ let dst_bottom = dst_y + height;
59
+
60
+ let clip_left = (*dst).clip_left as i32;
61
+ let clip_top = (*dst).clip_top as i32;
62
+ let clip_right = (*dst).clip_right as i32;
63
+ let clip_bottom = (*dst).clip_bottom as i32;
64
+
65
+ // Early-out: completely outside clip rect
66
+ if dst_x >= clip_right
67
+ || dst_right <= clip_left
68
+ || dst_y >= clip_bottom
69
+ || dst_bottom <= clip_top
70
+ {
71
+ return 0;
72
+ }
73
+
74
+ // Clamp visible region to clip rect
75
+ let vis_left = dst_x.max(clip_left);
76
+ let vis_right = dst_right.min(clip_right);
77
+ let vis_top = dst_y.max(clip_top);
78
+ let vis_bottom = dst_bottom.min(clip_bottom);
79
+
80
+ if vis_left >= vis_right || vis_top >= vis_bottom {
81
+ return 0;
82
+ }
83
+
84
+ // Adjust source coordinates for clipping and orientation.
85
+ // The orientation switch in the original adjusts src_x/src_y before the cpp check.
86
+ let orientation_code = (flags >> 16) & 0xFFFF;
87
+ let (adj_src_x, adj_src_y) = adjust_src_for_orientation(
88
+ src_x,
89
+ src_y,
90
+ dst_x,
91
+ dst_y,
92
+ width,
93
+ height,
94
+ vis_left,
95
+ vis_top,
96
+ vis_right,
97
+ vis_bottom,
98
+ orientation_code,
99
+ );
100
+
101
+ // Build slices from raw BitGrid data pointers
102
+ let dst_data =
103
+ core::slice::from_raw_parts_mut((*dst).data, ((*dst).row_stride * (*dst).height) as usize);
104
+ let src_data =
105
+ core::slice::from_raw_parts((*src).data, ((*src).row_stride * (*src).height) as usize);
106
+
107
+ // 1-bit byte-aligned fast path
108
+ if dst_cpp == 1 && src_cpp == 1 {
109
+ let left_aligned = (vis_left & 7) == 0;
110
+ let right_aligned = (vis_right & 7) == 0;
111
+ let src_aligned = (adj_src_x & 7) == 0;
112
+
113
+ if left_aligned && right_aligned && src_aligned && color_table.is_null() && flags < 4 {
114
+ crate::render::display::sprite_blit::blit_1bit_aligned(
115
+ dst_data,
116
+ (*dst).row_stride,
117
+ src_data,
118
+ (*src).row_stride,
119
+ vis_left,
120
+ vis_top,
121
+ vis_right,
122
+ vis_bottom,
123
+ adj_src_x,
124
+ adj_src_y,
125
+ blend_mode,
126
+ );
127
+ return 1;
128
+ }
129
+ }
130
+
131
+ // Convert color_table pointer to typed reference for the generic path
132
+ let color_table_ref: Option<&[u8; 256]> = if !color_table.is_null() {
133
+ Some(&*(color_table as *const [u8; 256]))
134
+ } else {
135
+ None
136
+ };
137
+
138
+ // Generic per-pixel fallback — handles all remaining cases
139
+ crate::render::display::sprite_blit::blit_generic_perpixel(
140
+ dst_data,
141
+ (*dst).row_stride,
142
+ dst_cpp,
143
+ src_data,
144
+ (*src).row_stride,
145
+ src_cpp,
146
+ vis_left,
147
+ vis_top,
148
+ vis_right,
149
+ vis_bottom,
150
+ adj_src_x,
151
+ adj_src_y,
152
+ color_table_ref,
153
+ blend_mode,
154
+ )
155
+ }
156
+
157
+ /// Adjust source coordinates for clipping and orientation.
158
+ ///
159
+ /// Port of the orientation switch in BitGrid__BlitSpriteRect (0x4F69D3..0x4F6A9D).
160
+ /// Maps the 16 orientation codes (0-15) to 8 unique source adjustments.
161
+ pub fn adjust_src_for_orientation(
162
+ src_x: i32,
163
+ src_y: i32,
164
+ dst_x: i32,
165
+ dst_y: i32,
166
+ width: i32,
167
+ height: i32,
168
+ vis_left: i32,
169
+ vis_top: i32,
170
+ vis_right: i32,
171
+ vis_bottom: i32,
172
+ orientation: u32,
173
+ ) -> (i32, i32) {
174
+ match orientation {
175
+ 1 | 10 => {
176
+ // MirrorX: src_x from right side, src_y from top
177
+ let sx = src_x + (dst_x - vis_right) + width;
178
+ let sy = src_y + (vis_top - dst_y);
179
+ (sx, sy)
180
+ }
181
+ 2 | 9 => {
182
+ // MirrorY: src_x from left, src_y from bottom
183
+ let sx = src_x + (vis_left - dst_x);
184
+ let sy = src_y + (dst_y - vis_bottom) + height;
185
+ (sx, sy)
186
+ }
187
+ 3 | 8 => {
188
+ // MirrorXY: src_x from right, src_y from bottom
189
+ let sx = src_x + (dst_x - vis_right) + width;
190
+ let sy = src_y + (dst_y - vis_bottom) + height;
191
+ (sx, sy)
192
+ }
193
+ 4 | 15 => {
194
+ // Rotate90: src_x tracks dst_y from bottom, src_y tracks dst_x from left
195
+ let sx = src_x + (dst_y - vis_bottom) + height;
196
+ let sy = src_y + (vis_left - dst_x);
197
+ (sx, sy)
198
+ }
199
+ 5 | 14 => {
200
+ // Rotate90+MirrorX
201
+ let sx = src_x + (dst_y - vis_bottom) + height;
202
+ let sy = src_y + (dst_x - vis_right) + width;
203
+ (sx, sy)
204
+ }
205
+ 6 | 13 => {
206
+ // Rotate90+MirrorY
207
+ let sx = src_x + (vis_top - dst_y);
208
+ let sy = src_y + (vis_left - dst_x);
209
+ (sx, sy)
210
+ }
211
+ 7 | 12 => {
212
+ // Rotate90+MirrorXY
213
+ let sx = src_x + (vis_top - dst_y);
214
+ let sy = src_y + (dst_x - vis_right) + width;
215
+ (sx, sy)
216
+ }
217
+ _ => {
218
+ // Normal (0, 11, or any default)
219
+ let sx = src_x + (vis_left - dst_x);
220
+ let sy = src_y + (vis_top - dst_y);
221
+ (sx, sy)
222
+ }
223
+ }
224
+ }
225
+
226
+ /// 8bpp blit path — extracted from the original blit_impl for clarity.
227
+ ///
228
+ /// Handles blend modes 0-3 on 8bpp surfaces via PixelGrid wrapper.
229
+ pub unsafe fn blit_8bpp(
230
+ dst: *mut DisplayBitGrid,
231
+ dst_x: i32,
232
+ dst_y: i32,
233
+ width: i32,
234
+ height: i32,
235
+ src: *mut DisplayBitGrid,
236
+ src_x: i32,
237
+ src_y: i32,
238
+ color_table: *const u8,
239
+ flags: u32,
240
+ ) -> u32 {
241
+ let color_table_ref: Option<&[u8; 256]> = if !color_table.is_null() {
242
+ Some(&*(color_table as *const [u8; 256]))
243
+ } else {
244
+ None
245
+ };
246
+
247
+ let orientation = BlitOrientation::from_flags(flags);
248
+ let blend = BlitBlend::from_flags(flags);
249
+
250
+ let src_data =
251
+ core::slice::from_raw_parts((*src).data, ((*src).row_stride * (*src).height) as usize);
252
+ let blit_src = BlitSource {
253
+ data: src_data,
254
+ width: (*src).width,
255
+ height: (*src).height,
256
+ row_stride: (*src).row_stride,
257
+ };
258
+
259
+ let dst_data_len = ((*dst).row_stride * (*dst).height) as usize;
260
+ let dst_grid = PixelGridMut {
261
+ data: core::slice::from_raw_parts_mut((*dst).data, dst_data_len),
262
+ width: (*dst).width,
263
+ height: (*dst).height,
264
+ row_stride: (*dst).row_stride,
265
+ clip_left: (*dst).clip_left,
266
+ clip_top: (*dst).clip_top,
267
+ clip_right: (*dst).clip_right,
268
+ clip_bottom: (*dst).clip_bottom,
269
+ };
270
+
271
+ blit_sprite_rect(
272
+ dst_grid,
273
+ &blit_src,
274
+ dst_x,
275
+ dst_y,
276
+ width,
277
+ height,
278
+ src_x,
279
+ src_y,
280
+ color_table_ref,
281
+ orientation,
282
+ blend,
283
+ ) as u32
284
+ }
285
+
286
+ /// Stippled blit on raw BitGrid pointers.
287
+ ///
288
+ /// Wraps the raw pointers into PixelGrid/BlitSource and calls
289
+ /// `sprite_blit::blit_stippled`.
290
+ pub unsafe fn blit_stippled_raw(
291
+ dst: *mut DisplayBitGrid,
292
+ src: *mut DisplayBitGrid,
293
+ dst_x: i32,
294
+ dst_y: i32,
295
+ width: i32,
296
+ height: i32,
297
+ src_x: i32,
298
+ src_y: i32,
299
+ stipple_mode: u32,
300
+ parity: u32,
301
+ ) {
302
+ let src_data =
303
+ core::slice::from_raw_parts((*src).data, ((*src).row_stride * (*src).height) as usize);
304
+ let blit_src = BlitSource {
305
+ data: src_data,
306
+ width: (*src).width,
307
+ height: (*src).height,
308
+ row_stride: (*src).row_stride,
309
+ };
310
+
311
+ let dst_data_len = ((*dst).row_stride * (*dst).height) as usize;
312
+ let mut dst_grid = PixelGridMut {
313
+ data: core::slice::from_raw_parts_mut((*dst).data, dst_data_len),
314
+ width: (*dst).width,
315
+ height: (*dst).height,
316
+ row_stride: (*dst).row_stride,
317
+ clip_left: (*dst).clip_left,
318
+ clip_top: (*dst).clip_top,
319
+ clip_right: (*dst).clip_right,
320
+ clip_bottom: (*dst).clip_bottom,
321
+ };
322
+
323
+ blit_stippled(
324
+ &mut dst_grid,
325
+ &blit_src,
326
+ dst_x,
327
+ dst_y,
328
+ width,
329
+ height,
330
+ src_x,
331
+ src_y,
332
+ stipple_mode,
333
+ parity,
334
+ );
335
+ }
336
+
337
+ /// Tiled blit on raw BitGrid pointers.
338
+ ///
339
+ /// Wraps the raw pointers into PixelGrid/BlitSource and calls
340
+ /// `sprite_blit::blit_tiled`.
341
+ pub unsafe fn blit_tiled_raw(
342
+ dst: *mut DisplayBitGrid,
343
+ src: *mut DisplayBitGrid,
344
+ initial_x: i32,
345
+ dst_y: i32,
346
+ tile_width: i32,
347
+ tile_height: i32,
348
+ clip_left: i32,
349
+ clip_right: i32,
350
+ flags: u32,
351
+ ) {
352
+ let src_data =
353
+ core::slice::from_raw_parts((*src).data, ((*src).row_stride * (*src).height) as usize);
354
+ let blit_src = BlitSource {
355
+ data: src_data,
356
+ width: (*src).width,
357
+ height: (*src).height,
358
+ row_stride: (*src).row_stride,
359
+ };
360
+
361
+ let dst_data_len = ((*dst).row_stride * (*dst).height) as usize;
362
+ let mut dst_grid = PixelGridMut {
363
+ data: core::slice::from_raw_parts_mut((*dst).data, dst_data_len),
364
+ width: (*dst).width,
365
+ height: (*dst).height,
366
+ row_stride: (*dst).row_stride,
367
+ clip_left: (*dst).clip_left,
368
+ clip_top: (*dst).clip_top,
369
+ clip_right: (*dst).clip_right,
370
+ clip_bottom: (*dst).clip_bottom,
371
+ };
372
+
373
+ blit_tiled(
374
+ &mut dst_grid,
375
+ &blit_src,
376
+ initial_x,
377
+ dst_y,
378
+ tile_width,
379
+ tile_height,
380
+ clip_left,
381
+ clip_right,
382
+ None, // color_table — tiled mode doesn't use one
383
+ flags,
384
+ );
385
+ }
crates/openwa-core/src/{bitgrid.rs → bitgrid/mod.rs} RENAMED
@@ -35,6 +35,8 @@
35
  //! (*display_grid).vtable = rb_ptr(BIT_GRID_DISPLAY_VTABLE);
36
  //! ```
37
 
 
 
38
  use crate::rebase::rb;
39
  use crate::task::base::Vtable;
40
  use crate::wa_alloc::{wa_malloc, wa_malloc_struct_zeroed};
 
35
  //! (*display_grid).vtable = rb_ptr(BIT_GRID_DISPLAY_VTABLE);
36
  //! ```
37
 
38
+ pub mod blit;
39
+
40
  use crate::rebase::rb;
41
  use crate::task::base::Vtable;
42
  use crate::wa_alloc::{wa_malloc, wa_malloc_struct_zeroed};
crates/openwa-core/src/game/weapon_fire.rs CHANGED
@@ -1363,7 +1363,7 @@ pub unsafe fn projectile_fire(
1363
  /// Fixed-point 16.16 multiply: (a * b) >> 16, using full 64-bit intermediate.
1364
  #[inline(always)]
1365
  fn fixed_mul(a: i32, b: i32) -> i32 {
1366
- ((a as i64 * b as i64) >> 16) as i32
1367
  }
1368
 
1369
  /// Rust implementation of CreateArrow (0x51ED90).
 
1363
  /// Fixed-point 16.16 multiply: (a * b) >> 16, using full 64-bit intermediate.
1364
  #[inline(always)]
1365
  fn fixed_mul(a: i32, b: i32) -> i32 {
1366
+ crate::fixed::Fixed(a).mul_raw(crate::fixed::Fixed(b)).0
1367
  }
1368
 
1369
  /// Rust implementation of CreateArrow (0x51ED90).
crates/openwa-core/src/lib.rs CHANGED
@@ -46,6 +46,7 @@ pub mod registry;
46
  pub mod rng;
47
  pub mod snapshot;
48
  pub mod task;
 
49
  pub mod vtable;
50
  pub mod wa;
51
  pub mod wa_alloc;
 
46
  pub mod rng;
47
  pub mod snapshot;
48
  pub mod task;
49
+ pub mod trig;
50
  pub mod vtable;
51
  pub mod wa;
52
  pub mod wa_alloc;
crates/openwa-core/src/render/bungee_trail.rs ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Bungee trail rendering — draws the bungee drop trajectory path.
2
+ //!
3
+ //! Ported from WA.exe `DrawBungeeTrail` (0x500720).
4
+ //! Draws:
5
+ //! 1. Sprite at trail start position
6
+ //! 2. Series of vertices computed by accumulating angle + trig interpolation
7
+ //! 3. Final vertex at task position (0x84/0x88)
8
+ //! 4. DrawPolygon (if fill != 0) or DrawLineStrip
9
+
10
+ use crate::address::va;
11
+ use crate::fixed::Fixed;
12
+ use crate::rebase::rb;
13
+ use crate::render::queue::*;
14
+ use crate::render::SpriteOp;
15
+ use crate::task::BungeeTrailTask;
16
+ use crate::trig::trig_lookup;
17
+
18
+ /// Draw bungee trail for the given task.
19
+ ///
20
+ /// # Safety
21
+ ///
22
+ /// `task_ptr` must point to a valid `BungeeTrailTask`. ASLR rebase must be
23
+ /// initialized.
24
+ pub unsafe fn draw_bungee_trail(task_ptr: u32, style: u32, fill: u32) {
25
+ let task = &*(task_ptr as *const BungeeTrailTask);
26
+
27
+ if task.trail_visible == 0 {
28
+ return;
29
+ }
30
+
31
+ let ddgame = &*task.base.ddgame;
32
+ let rq = &mut *ddgame.render_queue;
33
+
34
+ let seg_data = task.segment_data;
35
+ if seg_data.is_null() {
36
+ return;
37
+ }
38
+
39
+ let segment_count = task.segment_count;
40
+ if segment_count <= 0 {
41
+ return;
42
+ }
43
+
44
+ let mut x = task.trail_start_x;
45
+ let mut y = task.trail_start_y;
46
+
47
+ let first_angle = *(seg_data.add(4) as *const i32);
48
+
49
+ // Enqueue start sprite (command type 5 = local)
50
+ if let Some(entry) = rq.alloc::<DrawSpriteCmd>() {
51
+ *entry = DrawSpriteCmd {
52
+ command_type: command_type::DRAW_SPRITE_LOCAL,
53
+ layer: 0xDFFFF,
54
+ x: Fixed(x).floor(),
55
+ y: Fixed(y).floor(),
56
+ sprite: SpriteOp(0x45),
57
+ palette: (first_angle + 0x8100) as u32,
58
+ };
59
+ }
60
+
61
+ // Build vertex array from trail segments
62
+ const MAX_VERTICES: usize = 256;
63
+ let mut verts = [[0i32; 3]; MAX_VERTICES];
64
+ let mut vert_count: usize = 0;
65
+ let mut accumulated_angle: u32 = 0;
66
+
67
+ let sin_table = rb(va::G_SIN_TABLE) as *const i32;
68
+ let cos_table = rb(va::G_COS_TABLE) as *const i32;
69
+
70
+ for i in 0..segment_count {
71
+ let seg_angle = *(seg_data.add(4 + i as usize * 8) as *const i32);
72
+
73
+ // Include vertex if: first segment, or segment has nonzero angle, or fill mode
74
+ if (i == 0 || seg_angle != 0 || fill != 0) && vert_count < MAX_VERTICES {
75
+ verts[vert_count] = [x, y, 0];
76
+ vert_count += 1;
77
+ }
78
+
79
+ accumulated_angle = accumulated_angle.wrapping_add(seg_angle as u32);
80
+
81
+ let sin_interp = trig_lookup(sin_table, accumulated_angle);
82
+ let cos_interp = trig_lookup(cos_table, accumulated_angle);
83
+
84
+ x = x.wrapping_add(sin_interp.0.wrapping_mul(8));
85
+ y = y.wrapping_sub(cos_interp.0.wrapping_mul(8));
86
+ }
87
+
88
+ // Final vertex = task position (target)
89
+ if vert_count < MAX_VERTICES {
90
+ verts[vert_count] = [task.pos_x.0, task.pos_y.0, 0];
91
+ vert_count += 1;
92
+ }
93
+
94
+ // Enqueue as polygon or line strip
95
+ if fill != 0 {
96
+ let total_size = vert_count * 0xC + 0x20;
97
+ if let Some(ptr) = rq.alloc_raw(total_size) {
98
+ let header = &mut *(ptr as *mut DrawPolygonHeader);
99
+ *header = DrawPolygonHeader {
100
+ command_type: command_type::DRAW_POLYGON,
101
+ layer: 0xE_0000,
102
+ count: vert_count as u32,
103
+ color1: style,
104
+ color2: fill,
105
+ };
106
+ core::ptr::copy_nonoverlapping(
107
+ verts.as_ptr() as *const u8,
108
+ ptr.add(core::mem::size_of::<DrawPolygonHeader>()),
109
+ vert_count * 0xC,
110
+ );
111
+ }
112
+ } else {
113
+ let total_size = vert_count * 0xC + 0x1C;
114
+ if let Some(ptr) = rq.alloc_raw(total_size) {
115
+ let header = &mut *(ptr as *mut DrawLineStripHeader);
116
+ *header = DrawLineStripHeader {
117
+ command_type: command_type::DRAW_LINE_STRIP,
118
+ layer: 0xE_0000,
119
+ count: vert_count as u32,
120
+ color: style,
121
+ };
122
+ core::ptr::copy_nonoverlapping(
123
+ verts.as_ptr() as *const u8,
124
+ ptr.add(core::mem::size_of::<DrawLineStripHeader>()),
125
+ vert_count * 0xC,
126
+ );
127
+ }
128
+ }
129
+ }
crates/openwa-core/src/render/crosshair_line.rs ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Crosshair line rendering — draws the weapon aiming crosshair line.
2
+ //!
3
+ //! Ported from WA.exe `DrawCrosshairLine` (0x5197D0).
4
+ //! Draws:
5
+ //! 1. Compute direction from angle at task+0x264
6
+ //! 2. Compute line length from DDGame scale + task offset
7
+ //! 3. Endpoint = start + direction * length (with overflow clamping)
8
+ //! 4. DrawPolygon (2 vertices) for the line
9
+ //! 5. Conditionally DrawSpriteLocal at endpoint (crosshair sprite)
10
+
11
+ use crate::address::va;
12
+ use crate::fixed::Fixed;
13
+ use crate::rebase::rb;
14
+ use crate::render::queue::*;
15
+ use crate::render::SpriteOp;
16
+ use crate::task::WeaponAimTask;
17
+ use crate::trig::trig_lookup;
18
+
19
+ /// Draw the weapon aiming crosshair line for the given task.
20
+ ///
21
+ /// # Safety
22
+ ///
23
+ /// `task_ptr` must point to a valid `WeaponAimTask`. ASLR rebase must be
24
+ /// initialized.
25
+ pub unsafe fn draw_crosshair_line(task_ptr: u32) {
26
+ let task = &*(task_ptr as *const WeaponAimTask);
27
+ let gt = &task.game_task;
28
+
29
+ if task.aim_active == 0 {
30
+ return;
31
+ }
32
+
33
+ let ddgame = &*gt.base.ddgame;
34
+ let rq = &mut *ddgame.render_queue;
35
+
36
+ let start_x = gt.pos_x.0;
37
+ let start_y = gt.pos_y.0;
38
+
39
+ let angle = task.aim_angle;
40
+
41
+ // Trig interpolation
42
+ let sin_table = rb(va::G_SIN_TABLE) as *const i32;
43
+ let cos_table = rb(va::G_COS_TABLE) as *const i32;
44
+ let sin_interp = trig_lookup(sin_table, angle);
45
+ let cos_interp = trig_lookup(cos_table, angle);
46
+
47
+ // Scale = fixed_mul(DDGame.parallax_scale, 0x140000) + task.aim_range_offset
48
+ let scale = Fixed(ddgame.parallax_scale).mul_raw(Fixed(0x14_0000)).0 + task.aim_range_offset;
49
+
50
+ // Endpoint = start + direction * scale
51
+ let mut endpoint_x = Fixed(sin_interp.0)
52
+ .mul_raw(Fixed(scale))
53
+ .0
54
+ .wrapping_add(start_x);
55
+ let mut endpoint_y = Fixed(cos_interp.0)
56
+ .mul_raw(Fixed(scale))
57
+ .0
58
+ .wrapping_add(start_y);
59
+
60
+ // Overflow clamping — when endpoint overflows i32 due to large scale
61
+ let mut overflowed = false;
62
+ let mut clamp_factor = 0i32;
63
+
64
+ let threshold = (*ddgame.game_info).game_version;
65
+
66
+ if threshold > 0x11E {
67
+ // Check X overflow: sin > 0 but endpoint wrapped below start
68
+ if sin_interp.0 > 0 && endpoint_x < start_x {
69
+ overflowed = true;
70
+ clamp_factor = (0x7FFFFFFFi32 - start_x) / sin_interp.0;
71
+ }
72
+ // Check Y overflow: cos > 0 but endpoint wrapped below start
73
+ if cos_interp.0 > 0 && endpoint_y < start_y {
74
+ let y_clamp = (0x7FFFFFFFi32 - start_y) / cos_interp.0;
75
+ if !overflowed || y_clamp < clamp_factor {
76
+ clamp_factor = y_clamp;
77
+ }
78
+ overflowed = true;
79
+ }
80
+ if overflowed {
81
+ endpoint_x = start_x + clamp_factor * sin_interp.0;
82
+ endpoint_y = start_y + clamp_factor * cos_interp.0;
83
+ }
84
+ }
85
+
86
+ // Enqueue polygon line (2 vertices)
87
+ let poly_param_1 = ddgame.gfx_color_table[8]; // crosshair line style
88
+ let poly_param_2 = ddgame.gfx_color_table[6]; // crosshair line color
89
+ let verts: [[i32; 3]; 2] = [[start_x, start_y, 0], [endpoint_x, endpoint_y, 0]];
90
+ let total_size = 2 * 0xC + 0x20;
91
+ if let Some(ptr) = rq.alloc_raw(total_size) {
92
+ let header = &mut *(ptr as *mut DrawPolygonHeader);
93
+ *header = DrawPolygonHeader {
94
+ command_type: command_type::DRAW_POLYGON,
95
+ layer: 0xE_0000,
96
+ count: 2,
97
+ color1: poly_param_1,
98
+ color2: poly_param_2,
99
+ };
100
+ core::ptr::copy_nonoverlapping(
101
+ verts.as_ptr() as *const u8,
102
+ ptr.add(core::mem::size_of::<DrawPolygonHeader>()),
103
+ 2 * 0xC,
104
+ );
105
+ }
106
+
107
+ // Draw crosshair sprite at endpoint (only if no overflow clamping)
108
+ if !overflowed {
109
+ if let Some(entry) = rq.alloc::<DrawSpriteCmd>() {
110
+ *entry = DrawSpriteCmd {
111
+ command_type: command_type::DRAW_SPRITE_LOCAL,
112
+ layer: 0x4_0000,
113
+ x: Fixed(endpoint_x).floor(),
114
+ y: Fixed(endpoint_y).floor(),
115
+ sprite: SpriteOp(0x44),
116
+ palette: (0x8000u32).wrapping_sub(angle),
117
+ };
118
+ }
119
+ }
120
+ }
crates/openwa-core/src/render/display/blit_sprite.rs ADDED
@@ -0,0 +1,455 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Rust port of DisplayGfx::BlitSprite (slot 19) and DrawScaledSprite (slot 20).
2
+ //!
3
+ //! These are the high-level sprite blit entry points that resolve sprite IDs,
4
+ //! apply palette transforms, compute destination coordinates with orientation,
5
+ //! and dispatch to the low-level BitGrid blit routines.
6
+
7
+ use crate::address::va;
8
+ use crate::bitgrid::blit::{blit_impl, blit_stippled_raw, blit_tiled_raw};
9
+ use crate::bitgrid::DisplayBitGrid;
10
+ use crate::fixed::Fixed;
11
+ use crate::rebase::rb;
12
+ use crate::render::display::gfx::DisplayGfx;
13
+ use crate::render::display::vtable as display_vtable;
14
+ use crate::render::display::vtable::DrawScaledSpriteResult;
15
+ use crate::render::{SpriteFlags, SpriteOp};
16
+
17
+ /// Rust port of DisplayGfx::BlitSprite (slot 19, 0x56B080).
18
+ ///
19
+ /// Resolves sprite ID + animation, applies palette transforms, computes
20
+ /// destination coordinates with orientation, and dispatches to the appropriate
21
+ /// blit routine (normal, stippled, or tiled).
22
+ pub unsafe fn blit_sprite(
23
+ this: *mut DisplayGfx,
24
+ x: Fixed,
25
+ y: Fixed,
26
+ sprite_flags: u32,
27
+ palette: u32,
28
+ ) {
29
+ let gfx = this;
30
+ let base = this as *const u8;
31
+
32
+ // ---------------------------------------------------------------
33
+ // Extract sprite index and flags
34
+ // ---------------------------------------------------------------
35
+ let sprite = SpriteOp(sprite_flags);
36
+ let flags = sprite.flags();
37
+ let sprite_id = sprite.index() as u32;
38
+
39
+ if sprite_id == 0 {
40
+ return;
41
+ }
42
+
43
+ // ---------------------------------------------------------------
44
+ // Palette manipulation
45
+ // ---------------------------------------------------------------
46
+ let mut pal: u32 = palette;
47
+ if flags.contains(SpriteFlags::INVERT_PALETTE) {
48
+ pal = 0x10000u32.wrapping_sub(palette);
49
+ if sprite_id.wrapping_sub(0x1D5) < 3 {
50
+ // Special sprite IDs: scale by 8/18
51
+ pal = (0x10000u32.wrapping_sub(palette).wrapping_mul(8)) / 0x12;
52
+ }
53
+ }
54
+ if flags.contains(SpriteFlags::PALETTE_XFORM) {
55
+ // Bit 25: palette transform (modular arithmetic for color cycling)
56
+ let tmp = ((pal.wrapping_mul(0x1F) as i32)
57
+ .wrapping_add(((pal.wrapping_mul(0x1F) as i32) >> 31) & 0x1F)
58
+ >> 5) as u32;
59
+ let tmp = tmp.wrapping_add(0x400) & 0xFFFF;
60
+ pal = (tmp.wrapping_rem(0xF800)) / 2;
61
+ if (pal & 0x400) != 0 {
62
+ pal = (pal & !0x400) | 0x8000;
63
+ }
64
+ }
65
+
66
+ // ---------------------------------------------------------------
67
+ // Check sprite arrays — bitmap path if not in primary arrays
68
+ // ---------------------------------------------------------------
69
+ let arr1 = *(base.add(sprite_id as usize * 4 + 0x1008) as *const u32);
70
+ let arr2 = *(base.add(sprite_id as usize * 4 + 0x2008) as *const u32);
71
+
72
+ if arr1 == 0 && arr2 == 0 {
73
+ // Bitmap sprite path — sprite is in the bitmap table at 0x3DD4.
74
+ let bitmap_obj = (*gfx).sprite_table[sprite_id as usize];
75
+ if bitmap_obj.is_null() {
76
+ return;
77
+ }
78
+
79
+ // Get frame data and dimensions from bitmap sprite object.
80
+ let mut sprite_w: i32 = 0;
81
+ let mut sprite_h: i32 = 0;
82
+ let mut rect_left: i32 = 0;
83
+ let mut rect_top: i32 = 0;
84
+ let mut rect_right: i32 = 0;
85
+ let mut rect_bottom: i32 = 0;
86
+ let frame_data = display_vtable::get_bitmap_sprite_info(
87
+ bitmap_obj,
88
+ pal,
89
+ &mut sprite_w,
90
+ &mut sprite_h,
91
+ &mut rect_left,
92
+ &mut rect_top,
93
+ &mut rect_right,
94
+ &mut rect_bottom,
95
+ );
96
+ if frame_data.is_null() {
97
+ return;
98
+ }
99
+
100
+ let camera_x = (*gfx).camera_x;
101
+ let camera_y = (*gfx).camera_y;
102
+ let half_w = sprite_w / 2;
103
+ let half_h = sprite_h / 2;
104
+ let blit_h = rect_bottom - rect_top;
105
+ let dst_x = (x.0 >> 16) + (camera_x - half_w) + rect_left;
106
+ let dst_y = (y.0 >> 16) + (camera_y - half_h) + rect_top;
107
+
108
+ if !flags.contains(SpriteFlags::TILED) {
109
+ // Non-tiled: clipped blit at (dst_x, dst_y).
110
+ display_vtable::blit_bitmap_clipped_native(
111
+ gfx, dst_x, dst_y, sprite_w, blit_h, frame_data, 2,
112
+ );
113
+ } else {
114
+ // Tiled: horizontal tile across the clip rect.
115
+ display_vtable::blit_bitmap_tiled_native(
116
+ gfx, dst_x, sprite_w, dst_y, blit_h, frame_data,
117
+ );
118
+ }
119
+ return;
120
+ }
121
+
122
+ // ---------------------------------------------------------------
123
+ // Bit 24: palette ×4 adjust with orientation-dependent high bits
124
+ // ---------------------------------------------------------------
125
+ // The original ASM at 0x56B145 scales pal by 4, adds 0x8000, then
126
+ // uses the overflow quadrant (bits 16-17 of the scaled value) to
127
+ // select an orientation+blend combo. The low 16 bits become the
128
+ // new palette value.
129
+ let mut orient_local: u32 = 0x0000_0001; // blend=1 (ColorTable/transparency), orientation=0 (Normal)
130
+ if flags.contains(SpriteFlags::PALETTE_X4) {
131
+ let scaled = pal.wrapping_mul(4).wrapping_add(0x8000);
132
+ pal = scaled & 0xFFFF;
133
+ let quad = ((scaled as i32) >> 16) & 3;
134
+ orient_local = match quad {
135
+ 0 => 0x0008_0001, // MirrorXY + ColorTable
136
+ 1 => 0x000C_0001, // Rotate90MirrorXY + ColorTable
137
+ 2 => 0x0004_0001, // Rotate90 + ColorTable
138
+ _ => 0x0000_0001, // Normal + ColorTable
139
+ };
140
+ }
141
+
142
+ // ---------------------------------------------------------------
143
+ // Sprite frame lookup via DisplayGfx::GetSpriteFrameForBlit (slot 33).
144
+ // ---------------------------------------------------------------
145
+ // Resolves the sprite ID + animation value into the actual decompressed
146
+ // frame surface plus its bounding box and full sprite-cell dimensions
147
+ // (used below for centering and clipped blits).
148
+ let mut out_sprite_w: i32 = 0;
149
+ let mut out_sprite_h: i32 = 0;
150
+ let mut out_rect_left: i32 = 0;
151
+ let mut out_rect_top: i32 = 0;
152
+ let mut out_rect_right: i32 = 0;
153
+ let mut out_rect_bottom: i32 = 0;
154
+ let mut out_anim_frac: u32 = 0;
155
+
156
+ let mut sprite_surface = DisplayGfx::get_sprite_frame_for_blit_raw(
157
+ gfx,
158
+ sprite_id,
159
+ pal,
160
+ &mut out_sprite_w,
161
+ &mut out_sprite_h,
162
+ &mut out_rect_left,
163
+ &mut out_rect_top,
164
+ &mut out_rect_right,
165
+ &mut out_rect_bottom,
166
+ &mut out_anim_frac,
167
+ );
168
+
169
+ if sprite_surface.is_null() {
170
+ return;
171
+ }
172
+
173
+ let sprite_w = out_sprite_w;
174
+ let sprite_h = out_sprite_h;
175
+ let rect_left = out_rect_left;
176
+ let rect_top = out_rect_top;
177
+ let rect_right = out_rect_right;
178
+ let rect_bottom = out_rect_bottom;
179
+
180
+ // Size checks
181
+ if rect_left >= rect_right || rect_top >= rect_bottom {
182
+ return;
183
+ }
184
+
185
+ let mut blit_w = rect_right - rect_left;
186
+ let mut blit_h = rect_bottom - rect_top;
187
+
188
+ // ---------------------------------------------------------------
189
+ // Shadow clear (high_flags bit 22)
190
+ // ---------------------------------------------------------------
191
+ if flags.contains(SpriteFlags::SHADOW_CLEAR) {
192
+ // Blit sprite to layer_2 as shadow base
193
+ let layer2 = (*gfx).layer_2;
194
+ blit_impl(
195
+ layer2,
196
+ 0,
197
+ 0,
198
+ blit_w,
199
+ blit_h,
200
+ sprite_surface,
201
+ 0,
202
+ 0,
203
+ core::ptr::null(),
204
+ 0, // mode 0 = copy
205
+ );
206
+ // Manipulate color_add_table entry for shadow
207
+ let color_idx = ((*gfx)._unknown_356c as usize) * 0x100;
208
+ let table_byte = &mut (*gfx).color_add_table[color_idx];
209
+ let saved = *table_byte;
210
+ *table_byte = 0;
211
+
212
+ // Call BitGrid__ClearColumn_Maybe (0x4F6590) — clears shadow channel
213
+ let clear_fn: unsafe extern "cdecl" fn(*mut u8) =
214
+ core::mem::transmute(rb(0x004F6590) as usize);
215
+ clear_fn(table_byte as *mut u8);
216
+
217
+ *table_byte = saved;
218
+
219
+ // Replace sprite surface with layer_2 (shadow-processed)
220
+ sprite_surface = layer2;
221
+ }
222
+
223
+ // ---------------------------------------------------------------
224
+ // Extra orientation flags from high_flags
225
+ // ---------------------------------------------------------------
226
+ if flags.contains(SpriteFlags::MIRROR_X) {
227
+ orient_local |= 0x0001_0000;
228
+ }
229
+ if flags.contains(SpriteFlags::MIRROR_Y) {
230
+ orient_local |= 0x0002_0000;
231
+ }
232
+
233
+ // ---------------------------------------------------------------
234
+ // 16-case orientation switch for camera coordinate mapping
235
+ // ---------------------------------------------------------------
236
+ let camera_x = (*gfx).camera_x;
237
+ let camera_y = (*gfx).camera_y;
238
+
239
+ // Signed divide toward zero (matches MSVC CDQ+SUB+SAR pattern)
240
+ let half_w = if sprite_w < 0 {
241
+ (sprite_w + 1) / 2
242
+ } else {
243
+ sprite_w / 2
244
+ };
245
+ let half_h = if sprite_h < 0 {
246
+ (sprite_h + 1) / 2
247
+ } else {
248
+ sprite_h / 2
249
+ };
250
+
251
+ let x_px = x.0 >> 16;
252
+ let y_px = y.0 >> 16;
253
+
254
+ let (dst_x, dst_y);
255
+ let orientation_key = (orient_local >> 16) as i32;
256
+
257
+ match orientation_key {
258
+ 1 | 10 => {
259
+ // MirrorX
260
+ dst_x = camera_x + half_w + x_px - rect_right;
261
+ dst_y = camera_y - half_h + rect_top + y_px;
262
+ }
263
+ 2 | 9 => {
264
+ // MirrorY — X same as Normal, Y mirrored
265
+ dst_x = camera_x - half_w + rect_left + x_px;
266
+ dst_y = camera_y + half_h + y_px - rect_bottom;
267
+ }
268
+ 3 | 8 => {
269
+ // MirrorXY
270
+ dst_x = camera_x + half_w + x_px - rect_right;
271
+ dst_y = camera_y + half_h + y_px - rect_bottom;
272
+ }
273
+ 4 | 15 => {
274
+ // Rotate90 — swap axes
275
+ dst_x = camera_x - half_h + rect_top + x_px;
276
+ dst_y = camera_y + half_w + y_px - rect_right;
277
+ blit_w = rect_bottom - rect_top;
278
+ blit_h = rect_right - rect_left;
279
+ }
280
+ 5 | 14 => {
281
+ // Rotate90MirrorX
282
+ dst_x = camera_x + half_h + x_px - rect_bottom;
283
+ dst_y = camera_y + half_w + y_px - rect_right;
284
+ blit_w = rect_bottom - rect_top;
285
+ blit_h = rect_right - rect_left;
286
+ }
287
+ 6 | 13 => {
288
+ // Rotate90MirrorY
289
+ dst_x = camera_x - half_h + rect_top + x_px;
290
+ dst_y = camera_y - half_w + rect_left + y_px;
291
+ blit_w = rect_bottom - rect_top;
292
+ blit_h = rect_right - rect_left;
293
+ }
294
+ 7 | 12 => {
295
+ // Rotate90MirrorXY
296
+ dst_x = camera_x + half_h + x_px - rect_bottom;
297
+ dst_y = camera_y - half_w + rect_left + y_px;
298
+ blit_w = rect_bottom - rect_top;
299
+ blit_h = rect_right - rect_left;
300
+ }
301
+ _ => {
302
+ // Normal (0, 11, and any other value)
303
+ dst_x = camera_x - half_w + rect_left + x_px;
304
+ dst_y = camera_y - half_h + rect_top + y_px;
305
+ }
306
+ }
307
+
308
+ // ---------------------------------------------------------------
309
+ // Blit dispatch based on high_flags
310
+ // ---------------------------------------------------------------
311
+
312
+ if blit_w <= 0 || blit_h <= 0 {
313
+ return;
314
+ }
315
+
316
+ // Stippled mode (checkerboard per-pixel blit)
317
+ if flags.intersects(SpriteFlags::STIPPLED_0 | SpriteFlags::STIPPLED_1) {
318
+ let stipple_mode: u32 = if flags.contains(SpriteFlags::STIPPLED_1) {
319
+ 1
320
+ } else {
321
+ 0
322
+ };
323
+ let parity = *(rb(va::G_STIPPLE_PARITY) as *const u32);
324
+
325
+ display_vtable::acquire_render_lock(gfx);
326
+
327
+ blit_stippled_raw(
328
+ (*gfx).layer_0,
329
+ sprite_surface,
330
+ dst_x,
331
+ dst_y,
332
+ blit_w,
333
+ blit_h,
334
+ 0,
335
+ 0,
336
+ stipple_mode,
337
+ parity,
338
+ );
339
+ return;
340
+ }
341
+
342
+ // Tiled mode (horizontal sprite tiling)
343
+ if flags.contains(SpriteFlags::TILED) {
344
+ display_vtable::acquire_render_lock(gfx);
345
+
346
+ blit_tiled_raw(
347
+ (*gfx).layer_0,
348
+ sprite_surface,
349
+ dst_x,
350
+ dst_y,
351
+ blit_w,
352
+ blit_h,
353
+ (*gfx).base.clip_x1,
354
+ (*gfx).base.clip_x2,
355
+ orient_local,
356
+ );
357
+ return;
358
+ }
359
+
360
+ // Determine color table pointer
361
+ let color_table: *const u8 = if flags.contains(SpriteFlags::ADDITIVE) {
362
+ (*gfx).color_add_table.as_ptr()
363
+ } else if flags.contains(SpriteFlags::COLOR_BLEND) {
364
+ (*gfx).color_blend_table.as_ptr()
365
+ } else {
366
+ core::ptr::null()
367
+ };
368
+
369
+ display_vtable::acquire_render_lock(gfx);
370
+
371
+ // src_x=0, src_y=0 always — vtable[33] already set up the sprite surface
372
+ blit_impl(
373
+ (*gfx).layer_0,
374
+ dst_x,
375
+ dst_y,
376
+ blit_w,
377
+ blit_h,
378
+ sprite_surface,
379
+ 0,
380
+ 0,
381
+ color_table,
382
+ orient_local,
383
+ );
384
+ }
385
+
386
+ /// Rust port of DisplayGfx::DrawScaledSprite (slot 20, 0x56B5F0).
387
+ ///
388
+ /// Computes coordinates in the vtable helper, then dispatches the blit
389
+ /// via blit_impl or blit_stippled_raw depending on the result.
390
+ pub unsafe fn draw_scaled_sprite(
391
+ this: *mut DisplayGfx,
392
+ x: Fixed,
393
+ y: Fixed,
394
+ sprite: *mut DisplayBitGrid,
395
+ src_x: i32,
396
+ src_y: i32,
397
+ src_w: i32,
398
+ src_h: i32,
399
+ flags: u32,
400
+ ) {
401
+ match display_vtable::draw_scaled_sprite(this, x, y, sprite, src_x, src_y, src_w, src_h, flags)
402
+ {
403
+ DrawScaledSpriteResult::Blit {
404
+ layer,
405
+ dst_x,
406
+ dst_y,
407
+ width,
408
+ height,
409
+ sprite,
410
+ src_x,
411
+ src_y,
412
+ color_table,
413
+ blit_flags,
414
+ } => {
415
+ blit_impl(
416
+ layer,
417
+ dst_x,
418
+ dst_y,
419
+ width,
420
+ height,
421
+ sprite,
422
+ src_x,
423
+ src_y,
424
+ color_table,
425
+ blit_flags,
426
+ );
427
+ }
428
+ DrawScaledSpriteResult::Stippled {
429
+ layer,
430
+ dst_x,
431
+ dst_y,
432
+ width,
433
+ height,
434
+ sprite,
435
+ src_x,
436
+ src_y,
437
+ stipple_mode,
438
+ } => {
439
+ let parity = *(rb(va::G_STIPPLE_PARITY) as *const u32);
440
+ blit_stippled_raw(
441
+ layer,
442
+ sprite,
443
+ dst_x,
444
+ dst_y,
445
+ width,
446
+ height,
447
+ src_x,
448
+ src_y,
449
+ stipple_mode,
450
+ parity,
451
+ );
452
+ }
453
+ DrawScaledSpriteResult::Handled => {}
454
+ }
455
+ }
crates/openwa-core/src/render/display/destructor.rs CHANGED
@@ -43,6 +43,7 @@ use core::ptr;
43
  use crate::address::va;
44
  use crate::bitgrid::{BitGridBaseVtable, BIT_GRID_BASE_VTABLE};
45
  use crate::rebase::rb;
 
46
  use crate::render::display::context::{FastcallResult, RenderContext};
47
  use crate::render::display::frame_hook::FramePostProcessHook;
48
  use crate::render::display::gfx::{DisplayGfx, TileBitmapSet, DISPLAY_BASE_DESTRUCTOR_IMPL};
@@ -293,3 +294,38 @@ unsafe fn cbitmap_vtable0(bitmap: *mut CBitmap, flags: u32) {
293
  core::mem::transmute(*vtable);
294
  dtor(bitmap, flags);
295
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  use crate::address::va;
44
  use crate::bitgrid::{BitGridBaseVtable, BIT_GRID_BASE_VTABLE};
45
  use crate::rebase::rb;
46
+ use crate::render::display::base::DisplayBase;
47
  use crate::render::display::context::{FastcallResult, RenderContext};
48
  use crate::render::display::frame_hook::FramePostProcessHook;
49
  use crate::render::display::gfx::{DisplayGfx, TileBitmapSet, DISPLAY_BASE_DESTRUCTOR_IMPL};
 
294
  core::mem::transmute(*vtable);
295
  dtor(bitmap, flags);
296
  }
297
+
298
+ // =============================================================================
299
+ // Headless destructor — used when DisplayBase is a headless stub
300
+ // =============================================================================
301
+
302
+ /// Destructor for the headless `DisplayBase` — frees the sprite cache chain
303
+ /// (SpriteCache → FrameCache → buffer) that was allocated by `new_headless()`.
304
+ ///
305
+ /// Installed as vtable slot 0 on the headless `DisplayBase` vtable (0x66A0F8)
306
+ /// by the DLL's display hook installer.
307
+ ///
308
+ /// # Safety
309
+ ///
310
+ /// `this` must be a valid `DisplayBase` pointer with a headless-allocated
311
+ /// sprite cache chain.
312
+ pub unsafe fn headless_destructor(this: *mut DisplayBase, flags: u8) -> *mut DisplayBase {
313
+ use crate::wa_alloc::wa_free;
314
+
315
+ let sprite_cache = (*this).sprite_cache;
316
+ if !sprite_cache.is_null() {
317
+ let frame_cache = (*sprite_cache).frame_cache;
318
+ if !frame_cache.is_null() {
319
+ let buf = (*frame_cache).buffer;
320
+ if !buf.is_null() {
321
+ wa_free(buf);
322
+ }
323
+ wa_free(frame_cache);
324
+ }
325
+ wa_free(sprite_cache);
326
+ }
327
+ if flags & 1 != 0 {
328
+ wa_free(this);
329
+ }
330
+ this
331
+ }
crates/openwa-core/src/render/display/mod.rs CHANGED
@@ -1,4 +1,5 @@
1
  pub mod base;
 
2
  pub mod context;
3
  pub mod destructor;
4
  pub mod font;
 
1
  pub mod base;
2
+ pub mod blit_sprite;
3
  pub mod context;
4
  pub mod destructor;
5
  pub mod font;
crates/openwa-core/src/render/mod.rs CHANGED
@@ -1,3 +1,5 @@
 
 
1
  pub mod ddraw;
2
  pub mod display;
3
  pub mod landscape;
 
1
+ pub mod bungee_trail;
2
+ pub mod crosshair_line;
3
  pub mod ddraw;
4
  pub mod display;
5
  pub mod landscape;
crates/openwa-core/src/render/queue.rs CHANGED
@@ -1,3 +1,7 @@
 
 
 
 
1
  use super::message::{RenderMessage, TypedRenderCmd, COMMAND_TYPE_TYPED};
2
  use super::sprite::sprite_op::SpriteOp;
3
 
@@ -10,10 +14,10 @@ pub struct DrawSpriteCmd {
10
  /// Command type: 4 = sprite global (world-space), 5 = sprite local (screen-space)
11
  pub command_type: u32,
12
  pub layer: u32,
13
- /// X position, upper 16 bits used (Fixed16 format)
14
- pub x_pos: u32,
15
- /// Y position, upper 16 bits used (Fixed16 format)
16
- pub y_pos: u32,
17
  pub sprite: SpriteOp,
18
  /// Palette context — passed to `blit_sprite` as the last arg.
19
  /// Semantics vary by producer (palette pointer, animation index, etc.).
@@ -116,14 +120,14 @@ pub struct DrawTiledBitmapCmd {
116
  /// 0x08: Source X coordinate (Fixed16). Hardcoded 0xFF00_0000 (= -256.0)
117
  /// — the tile sheet always renders from off-screen-left, with the
118
  /// dispatcher's clipping pass producing the on-screen X.
119
- pub x_fixed16: u32,
120
  /// 0x0C: Source Y coordinate (Fixed16). Caller-supplied; goes through
121
  /// `RQ_ClipCoordinates` and the high 16 bits become the destination Y.
122
- pub y_fixed16: u32,
123
- /// 0x10: Pointer to a sprite source descriptor — read by the slot 11
124
- /// method as `p4` with fields at `+0x08` (bpp, 8 or 0x40), `+0x10`
125
  /// (source row stride), `+0x14`, and `+0x18` (total source height).
126
- pub source_descriptor: u32,
127
  /// 0x14: Flag byte. Bit 0 forces the destination X to 0 (ignoring the
128
  /// clipped result), used for "always start at left edge" rendering.
129
  pub flags: u8,
@@ -143,9 +147,9 @@ pub struct DrawCrosshairCmd {
143
  pub layer: u32,
144
  pub color_fg: u32,
145
  pub color_bg: u32,
146
- pub x_pos: u32,
147
- pub y_pos: u32,
148
- pub _reserved: u32, // hardcoded 0 (clip ref)
149
  }
150
 
151
  const _: () = assert!(core::mem::size_of::<DrawCrosshairCmd>() == 0x1C);
@@ -156,11 +160,11 @@ pub struct DrawRectCmd {
156
  pub command_type: u32, // 0
157
  pub layer: u32,
158
  pub color: u32,
159
- pub x1: u32, // & 0xFFFF0000
160
- pub y1: u32, // & 0xFFFF0000
161
- pub x2: u32, // & 0xFFFF0000
162
- pub y2: u32, // & 0xFFFF0000
163
- pub y_clip: u32, // EDX & 0xFFFF0000
164
  }
165
 
166
  const _: () = assert!(core::mem::size_of::<DrawRectCmd>() == 0x20);
@@ -170,13 +174,13 @@ const _: () = assert!(core::mem::size_of::<DrawRectCmd>() == 0x20);
170
  pub struct DrawSpriteOffsetCmd {
171
  pub command_type: u32, // 6
172
  pub layer: u32,
173
- pub sprite_id: u32,
174
- pub x_pos: u32, // & 0xFFFF0000
175
- pub y_pos: u32, // & 0xFFFF0000
176
- pub _reserved: u32, // hardcoded 0
177
- pub y_clip: u32, // EDX & 0xFFFF0000
178
- pub param_7: u32,
179
- pub param_8: u32,
180
  }
181
 
182
  const _: () = assert!(core::mem::size_of::<DrawSpriteOffsetCmd>() == 0x24);
@@ -186,14 +190,14 @@ const _: () = assert!(core::mem::size_of::<DrawSpriteOffsetCmd>() == 0x24);
186
  pub struct DrawBitmapGlobalCmd {
187
  pub command_type: u32, // 1
188
  pub layer: u32,
189
- pub x_pos: u32, // & 0xFFFF0000
190
- pub y_pos: u32, // EDX & 0xFFFF0000
191
- pub bitmap_ptr: u32,
192
- pub _reserved: u32, // hardcoded 0
193
- pub param_6: u32,
194
- pub param_7: u32,
195
- pub param_8: u32,
196
- pub param_9: u32,
197
  }
198
 
199
  const _: () = assert!(core::mem::size_of::<DrawBitmapGlobalCmd>() == 0x28);
@@ -203,17 +207,17 @@ const _: () = assert!(core::mem::size_of::<DrawBitmapGlobalCmd>() == 0x28);
203
  pub struct DrawTextboxLocalCmd {
204
  pub command_type: u32, // 2
205
  pub layer: u32,
206
- pub _reserved_2: u32, // hardcoded 0
207
- pub x_pos: u32, // & 0xFFFF0000
208
- pub y_pos: u32, // EDX & 0xFFFF0000
209
- pub _reserved_5: u32, // hardcoded 0
210
- pub _reserved_6: u32, // hardcoded 0
211
- pub text_ptr: u32,
212
- pub _reserved_8: u32, // hardcoded 0
213
- pub _reserved_9: u32, // hardcoded 0
214
- pub param_6: u32,
215
- pub param_7: u32,
216
- pub param_8: u32,
217
  }
218
 
219
  const _: () = assert!(core::mem::size_of::<DrawTextboxLocalCmd>() == 0x34);
@@ -226,7 +230,7 @@ pub struct DrawLineStripHeader {
226
  pub command_type: u32, // 8
227
  pub layer: u32, // hardcoded 0xE_0000
228
  pub count: u32, // vertex count (from EDI)
229
- pub param_1: u32, // stack param
230
  }
231
 
232
  const _: () = assert!(core::mem::size_of::<DrawLineStripHeader>() == 0x10);
@@ -239,8 +243,8 @@ pub struct DrawPolygonHeader {
239
  pub command_type: u32, // 9
240
  pub layer: u32, // hardcoded 0xE_0000
241
  pub count: u32, // vertex count (from ESI)
242
- pub param_1: u32, // stack param 1
243
- pub param_2: u32, // stack param 2
244
  }
245
 
246
  const _: () = assert!(core::mem::size_of::<DrawPolygonHeader>() == 0x14);
 
1
+ use crate::bitgrid::DisplayBitGrid;
2
+ use crate::fixed::Fixed;
3
+ use crate::render::display::vtable::TiledBitmapSource;
4
+
5
  use super::message::{RenderMessage, TypedRenderCmd, COMMAND_TYPE_TYPED};
6
  use super::sprite::sprite_op::SpriteOp;
7
 
 
14
  /// Command type: 4 = sprite global (world-space), 5 = sprite local (screen-space)
15
  pub command_type: u32,
16
  pub layer: u32,
17
+ /// X position (Fixed16, fractional bits always zero — producers floor before storing).
18
+ pub x: Fixed,
19
+ /// Y position (Fixed16, fractional bits always zero — producers floor before storing).
20
+ pub y: Fixed,
21
  pub sprite: SpriteOp,
22
  /// Palette context — passed to `blit_sprite` as the last arg.
23
  /// Semantics vary by producer (palette pointer, animation index, etc.).
 
120
  /// 0x08: Source X coordinate (Fixed16). Hardcoded 0xFF00_0000 (= -256.0)
121
  /// — the tile sheet always renders from off-screen-left, with the
122
  /// dispatcher's clipping pass producing the on-screen X.
123
+ pub x: Fixed,
124
  /// 0x0C: Source Y coordinate (Fixed16). Caller-supplied; goes through
125
  /// `RQ_ClipCoordinates` and the high 16 bits become the destination Y.
126
+ pub y: Fixed,
127
+ /// 0x10: Pointer to a [`TiledBitmapSource`] descriptor — read by the
128
+ /// slot 11 method with fields at `+0x08` (bpp, 8 or 0x40), `+0x10`
129
  /// (source row stride), `+0x14`, and `+0x18` (total source height).
130
+ pub source: *const TiledBitmapSource,
131
  /// 0x14: Flag byte. Bit 0 forces the destination X to 0 (ignoring the
132
  /// clipped result), used for "always start at left edge" rendering.
133
  pub flags: u8,
 
147
  pub layer: u32,
148
  pub color_fg: u32,
149
  pub color_bg: u32,
150
+ pub x: Fixed,
151
+ pub y: Fixed,
152
+ pub ref_z: i32, // hardcoded 0 (perspective clip reference Z)
153
  }
154
 
155
  const _: () = assert!(core::mem::size_of::<DrawCrosshairCmd>() == 0x1C);
 
160
  pub command_type: u32, // 0
161
  pub layer: u32,
162
  pub color: u32,
163
+ pub x1: Fixed,
164
+ pub y1: Fixed,
165
+ pub x2: Fixed,
166
+ pub y2: Fixed,
167
+ pub ref_z: i32, // perspective clip reference Z (EDX & 0xFFFF0000)
168
  }
169
 
170
  const _: () = assert!(core::mem::size_of::<DrawRectCmd>() == 0x20);
 
174
  pub struct DrawSpriteOffsetCmd {
175
  pub command_type: u32, // 6
176
  pub layer: u32,
177
+ pub flags: u32,
178
+ pub x: Fixed,
179
+ pub y: Fixed,
180
+ pub ref_z: i32, // first Z reference for perspective clip (hardcoded 0)
181
+ pub ref_z_2: i32, // second Z reference (EDX & 0xFFFF0000)
182
+ pub sprite: SpriteOp,
183
+ pub palette: u32,
184
  }
185
 
186
  const _: () = assert!(core::mem::size_of::<DrawSpriteOffsetCmd>() == 0x24);
 
190
  pub struct DrawBitmapGlobalCmd {
191
  pub command_type: u32, // 1
192
  pub layer: u32,
193
+ pub x: Fixed,
194
+ pub y: Fixed,
195
+ pub bitmap: *mut DisplayBitGrid,
196
+ pub src_x: i32, // hardcoded 0
197
+ pub src_y: i32,
198
+ pub src_w: i32,
199
+ pub src_h: i32,
200
+ pub flags: u32,
201
  }
202
 
203
  const _: () = assert!(core::mem::size_of::<DrawBitmapGlobalCmd>() == 0x28);
 
207
  pub struct DrawTextboxLocalCmd {
208
  pub command_type: u32, // 2
209
  pub layer: u32,
210
+ pub mode: u32,
211
+ pub x: Fixed,
212
+ pub y: Fixed,
213
+ pub ref_z: i32, // perspective clip reference Z (hardcoded 0)
214
+ pub ref_z_2: i32, // second Z reference (hardcoded 0)
215
+ pub bitmap: *mut DisplayBitGrid,
216
+ pub src_x: i32, // hardcoded 0
217
+ pub src_y: i32, // hardcoded 0
218
+ pub src_w: i32,
219
+ pub src_h: i32,
220
+ pub flags: u32,
221
  }
222
 
223
  const _: () = assert!(core::mem::size_of::<DrawTextboxLocalCmd>() == 0x34);
 
230
  pub command_type: u32, // 8
231
  pub layer: u32, // hardcoded 0xE_0000
232
  pub count: u32, // vertex count (from EDI)
233
+ pub color: u32, // line color
234
  }
235
 
236
  const _: () = assert!(core::mem::size_of::<DrawLineStripHeader>() == 0x10);
 
243
  pub command_type: u32, // 9
244
  pub layer: u32, // hardcoded 0xE_0000
245
  pub count: u32, // vertex count (from ESI)
246
+ pub color1: u32, // line color 1
247
+ pub color2: u32, // line color 2
248
  }
249
 
250
  const _: () = assert!(core::mem::size_of::<DrawPolygonHeader>() == 0x14);
crates/openwa-core/src/trig.rs ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Interpolated fixed-point trig table lookup.
2
+ //!
3
+ //! WA stores sine and cosine values as 1025-entry fixed-point 16.16 tables
4
+ //! at [`va::G_SIN_TABLE`] and [`va::G_COS_TABLE`]. Each table covers one full
5
+ //! period in 1024 steps (2π / 1024 radians per step); the 1025th entry is a
6
+ //! sentinel that allows interpolation at the wraparound.
7
+ //!
8
+ //! The lookup angle is a 16-bit quantity where the full range 0..0xFFFF
9
+ //! maps to one full period. The upper 10 bits select the table entry and
10
+ //! the lower 6 bits are used for linear interpolation between adjacent
11
+ //! entries.
12
+
13
+ use crate::fixed::Fixed;
14
+ use crate::rebase::rb;
15
+
16
+ /// Interpolated lookup from a 1025-entry fixed-point trig table.
17
+ ///
18
+ /// `table` must point to at least 1025 `i32` values (the standard WA
19
+ /// sine/cosine layout). `angle` is a 16-bit angle where the full range
20
+ /// 0..0xFFFF maps to one full period.
21
+ ///
22
+ /// Returns a Fixed 16.16 value interpolated between the two nearest
23
+ /// table entries.
24
+ #[inline]
25
+ pub unsafe fn trig_lookup(table: *const i32, angle: u32) -> Fixed {
26
+ let index = ((angle as i32) >> 6) as usize & 0x3FF;
27
+ let frac = Fixed::from_raw(((angle & 0x3F) << 10) as i32);
28
+ let base = Fixed::from_raw(*table.add(index));
29
+ let next = Fixed::from_raw(*table.add(index + 1));
30
+ (next - base).mul_raw(frac) + base
31
+ }
32
+
33
+ /// Sine lookup from WA's global sine table.
34
+ ///
35
+ /// # Safety
36
+ ///
37
+ /// Requires ASLR rebase to have been computed (i.e. the DLL is loaded).
38
+ #[inline]
39
+ pub unsafe fn sin_lookup(angle: u32) -> Fixed {
40
+ trig_lookup(rb(crate::address::va::G_SIN_TABLE) as *const i32, angle)
41
+ }
42
+
43
+ /// Cosine lookup from WA's global cosine table.
44
+ ///
45
+ /// # Safety
46
+ ///
47
+ /// Requires ASLR rebase to have been computed (i.e. the DLL is loaded).
48
+ #[inline]
49
+ pub unsafe fn cos_lookup(angle: u32) -> Fixed {
50
+ trig_lookup(rb(crate::address::va::G_COS_TABLE) as *const i32, angle)
51
+ }
crates/openwa-dll/src/replacements/bitgrid.rs CHANGED
@@ -827,7 +827,7 @@ unsafe extern "cdecl" fn wa_blit_sprite_rect(
827
  /// Trampoline to the original WA blit function (for unsupported modes).
828
  static ORIG_BLIT: AtomicU32 = AtomicU32::new(0);
829
 
830
- /// Naked trampoline: captures ESI (dst BitGrid) and forwards to cdecl impl.
831
  ///
832
  /// Original calling convention: ESI=dst, 9 stdcall params, RET 0x24.
833
  #[unsafe(naked)]
@@ -850,18 +850,15 @@ unsafe extern "C" fn blit_hook_trampoline() {
850
  "mov esp, ebp",
851
  "pop ebp",
852
  "ret 0x24",
853
- impl_fn = sym blit_impl,
854
  );
855
  }
856
 
857
- /// Rust implementation of the core sprite blit (BitGrid__BlitSpriteRect, 0x4F6910).
858
  ///
859
- /// Handles all blend modes for both 8bpp and 1-bit surfaces:
860
- /// - 8bpp modes 0-3: via `blit_sprite_rect` (orientation-aware, PixelGrid wrapper)
861
- /// - 1-bit byte-aligned fast path: memcpy/OR/nop for modes 0-3 with Normal orientation
862
- /// - Generic per-pixel fallback: handles all remaining cases (unaligned 1-bit, modes 4-5,
863
- /// mixed cpp, etc.) via `blit_generic_perpixel`
864
- pub(crate) unsafe extern "cdecl" fn blit_impl(
865
  dst: *mut openwa_core::bitgrid::DisplayBitGrid,
866
  dst_x: i32,
867
  dst_y: i32,
@@ -873,368 +870,18 @@ pub(crate) unsafe extern "cdecl" fn blit_impl(
873
  color_table: *const u8,
874
  flags: u32,
875
  ) -> u32 {
876
- if width == 0 || height == 0 {
877
- return 0;
878
- }
879
-
880
- let blend_mode = flags & 0xFFFF;
881
- let src_cpp = (*src).cells_per_unit;
882
- let dst_cpp = (*dst).cells_per_unit;
883
-
884
- // Fast path: 8bpp surfaces with blend modes 0-3
885
- if dst_cpp == 8 && src_cpp == 8 && blend_mode <= 3 {
886
- return blit_8bpp(
887
- dst,
888
- dst_x,
889
- dst_y,
890
- width,
891
- height,
892
- src,
893
- src_x,
894
- src_y,
895
- color_table,
896
- flags,
897
- );
898
- }
899
-
900
- // --- Clipping (shared by 1-bit fast path and generic fallback) ---
901
- // This mirrors the clipping logic at the top of BitGrid__BlitSpriteRect.
902
- let dst_right = dst_x + width;
903
- let dst_bottom = dst_y + height;
904
-
905
- let clip_left = (*dst).clip_left as i32;
906
- let clip_top = (*dst).clip_top as i32;
907
- let clip_right = (*dst).clip_right as i32;
908
- let clip_bottom = (*dst).clip_bottom as i32;
909
-
910
- // Early-out: completely outside clip rect
911
- if dst_x >= clip_right
912
- || dst_right <= clip_left
913
- || dst_y >= clip_bottom
914
- || dst_bottom <= clip_top
915
- {
916
- return 0;
917
- }
918
-
919
- // Clamp visible region to clip rect
920
- let vis_left = dst_x.max(clip_left);
921
- let vis_right = dst_right.min(clip_right);
922
- let vis_top = dst_y.max(clip_top);
923
- let vis_bottom = dst_bottom.min(clip_bottom);
924
-
925
- if vis_left >= vis_right || vis_top >= vis_bottom {
926
- return 0;
927
- }
928
-
929
- // Adjust source coordinates for clipping and orientation.
930
- // The orientation switch in the original adjusts src_x/src_y before the cpp check.
931
- let orientation_code = (flags >> 16) & 0xFFFF;
932
- let (adj_src_x, adj_src_y) = adjust_src_for_orientation(
933
- src_x,
934
- src_y,
935
- dst_x,
936
- dst_y,
937
- width,
938
- height,
939
- vis_left,
940
- vis_top,
941
- vis_right,
942
- vis_bottom,
943
- orientation_code,
944
- );
945
-
946
- // Build slices from raw BitGrid data pointers
947
- let dst_data =
948
- core::slice::from_raw_parts_mut((*dst).data, ((*dst).row_stride * (*dst).height) as usize);
949
- let src_data =
950
- core::slice::from_raw_parts((*src).data, ((*src).row_stride * (*src).height) as usize);
951
-
952
- // 1-bit byte-aligned fast path
953
- if dst_cpp == 1 && src_cpp == 1 {
954
- let left_aligned = (vis_left & 7) == 0;
955
- let right_aligned = (vis_right & 7) == 0;
956
- let src_aligned = (adj_src_x & 7) == 0;
957
-
958
- if left_aligned && right_aligned && src_aligned && color_table.is_null() && flags < 4 {
959
- openwa_core::render::display::sprite_blit::blit_1bit_aligned(
960
- dst_data,
961
- (*dst).row_stride,
962
- src_data,
963
- (*src).row_stride,
964
- vis_left,
965
- vis_top,
966
- vis_right,
967
- vis_bottom,
968
- adj_src_x,
969
- adj_src_y,
970
- blend_mode,
971
- );
972
- return 1;
973
- }
974
- }
975
-
976
- // Convert color_table pointer to typed reference for the generic path
977
- let color_table_ref: Option<&[u8; 256]> = if !color_table.is_null() {
978
- Some(&*(color_table as *const [u8; 256]))
979
- } else {
980
- None
981
- };
982
-
983
- // Generic per-pixel fallback — handles all remaining cases
984
- openwa_core::render::display::sprite_blit::blit_generic_perpixel(
985
- dst_data,
986
- (*dst).row_stride,
987
- dst_cpp,
988
- src_data,
989
- (*src).row_stride,
990
- src_cpp,
991
- vis_left,
992
- vis_top,
993
- vis_right,
994
- vis_bottom,
995
- adj_src_x,
996
- adj_src_y,
997
- color_table_ref,
998
- blend_mode,
999
- )
1000
- }
1001
-
1002
- /// Adjust source coordinates for clipping and orientation.
1003
- ///
1004
- /// Port of the orientation switch in BitGrid__BlitSpriteRect (0x4F69D3..0x4F6A9D).
1005
- /// Maps the 16 orientation codes (0-15) to 8 unique source adjustments.
1006
- fn adjust_src_for_orientation(
1007
- src_x: i32,
1008
- src_y: i32,
1009
- dst_x: i32,
1010
- dst_y: i32,
1011
- width: i32,
1012
- height: i32,
1013
- vis_left: i32,
1014
- vis_top: i32,
1015
- vis_right: i32,
1016
- vis_bottom: i32,
1017
- orientation: u32,
1018
- ) -> (i32, i32) {
1019
- match orientation {
1020
- 1 | 10 => {
1021
- // MirrorX: src_x from right side, src_y from top
1022
- let sx = src_x + (dst_x - vis_right) + width;
1023
- let sy = src_y + (vis_top - dst_y);
1024
- (sx, sy)
1025
- }
1026
- 2 | 9 => {
1027
- // MirrorY: src_x from left, src_y from bottom
1028
- let sx = src_x + (vis_left - dst_x);
1029
- let sy = src_y + (dst_y - vis_bottom) + height;
1030
- (sx, sy)
1031
- }
1032
- 3 | 8 => {
1033
- // MirrorXY: src_x from right, src_y from bottom
1034
- let sx = src_x + (dst_x - vis_right) + width;
1035
- let sy = src_y + (dst_y - vis_bottom) + height;
1036
- (sx, sy)
1037
- }
1038
- 4 | 15 => {
1039
- // Rotate90: src_x tracks dst_y from bottom, src_y tracks dst_x from left
1040
- let sx = src_x + (dst_y - vis_bottom) + height;
1041
- let sy = src_y + (vis_left - dst_x);
1042
- (sx, sy)
1043
- }
1044
- 5 | 14 => {
1045
- // Rotate90+MirrorX
1046
- let sx = src_x + (dst_y - vis_bottom) + height;
1047
- let sy = src_y + (dst_x - vis_right) + width;
1048
- (sx, sy)
1049
- }
1050
- 6 | 13 => {
1051
- // Rotate90+MirrorY
1052
- let sx = src_x + (vis_top - dst_y);
1053
- let sy = src_y + (vis_left - dst_x);
1054
- (sx, sy)
1055
- }
1056
- 7 | 12 => {
1057
- // Rotate90+MirrorXY
1058
- let sx = src_x + (vis_top - dst_y);
1059
- let sy = src_y + (dst_x - vis_right) + width;
1060
- (sx, sy)
1061
- }
1062
- _ => {
1063
- // Normal (0, 11, or any default)
1064
- let sx = src_x + (vis_left - dst_x);
1065
- let sy = src_y + (vis_top - dst_y);
1066
- (sx, sy)
1067
- }
1068
- }
1069
- }
1070
-
1071
- /// 8bpp blit path — extracted from the original blit_impl for clarity.
1072
- ///
1073
- /// Handles blend modes 0-3 on 8bpp surfaces via PixelGrid wrapper.
1074
- unsafe fn blit_8bpp(
1075
- dst: *mut openwa_core::bitgrid::DisplayBitGrid,
1076
- dst_x: i32,
1077
- dst_y: i32,
1078
- width: i32,
1079
- height: i32,
1080
- src: *mut openwa_core::bitgrid::DisplayBitGrid,
1081
- src_x: i32,
1082
- src_y: i32,
1083
- color_table: *const u8,
1084
- flags: u32,
1085
- ) -> u32 {
1086
- use openwa_core::render::display::sprite_blit::{
1087
- blit_sprite_rect, BlitBlend, BlitOrientation, BlitSource,
1088
- };
1089
-
1090
- let color_table_ref: Option<&[u8; 256]> = if !color_table.is_null() {
1091
- Some(&*(color_table as *const [u8; 256]))
1092
- } else {
1093
- None
1094
- };
1095
-
1096
- let orientation = BlitOrientation::from_flags(flags);
1097
- let blend = BlitBlend::from_flags(flags);
1098
-
1099
- let src_data =
1100
- core::slice::from_raw_parts((*src).data, ((*src).row_stride * (*src).height) as usize);
1101
- let blit_src = BlitSource {
1102
- data: src_data,
1103
- width: (*src).width,
1104
- height: (*src).height,
1105
- row_stride: (*src).row_stride,
1106
- };
1107
-
1108
- let dst_data_len = ((*dst).row_stride * (*dst).height) as usize;
1109
- let dst_grid = openwa_core::render::display::line_draw::PixelGridMut {
1110
- data: core::slice::from_raw_parts_mut((*dst).data, dst_data_len),
1111
- width: (*dst).width,
1112
- height: (*dst).height,
1113
- row_stride: (*dst).row_stride,
1114
- clip_left: (*dst).clip_left,
1115
- clip_top: (*dst).clip_top,
1116
- clip_right: (*dst).clip_right,
1117
- clip_bottom: (*dst).clip_bottom,
1118
- };
1119
-
1120
- blit_sprite_rect(
1121
- dst_grid,
1122
- &blit_src,
1123
- dst_x,
1124
- dst_y,
1125
- width,
1126
- height,
1127
- src_x,
1128
- src_y,
1129
- color_table_ref,
1130
- orientation,
1131
- blend,
1132
- ) as u32
1133
- }
1134
-
1135
- /// Stippled blit on raw BitGrid pointers.
1136
- ///
1137
- /// Wraps the raw pointers into PixelGrid/BlitSource and calls
1138
- /// `sprite_blit::blit_stippled`.
1139
- pub(crate) unsafe fn blit_stippled_raw(
1140
- dst: *mut openwa_core::bitgrid::DisplayBitGrid,
1141
- src: *mut openwa_core::bitgrid::DisplayBitGrid,
1142
- dst_x: i32,
1143
- dst_y: i32,
1144
- width: i32,
1145
- height: i32,
1146
- src_x: i32,
1147
- src_y: i32,
1148
- stipple_mode: u32,
1149
- parity: u32,
1150
- ) {
1151
- use openwa_core::render::display::sprite_blit::{blit_stippled, BlitSource};
1152
-
1153
- let src_data =
1154
- core::slice::from_raw_parts((*src).data, ((*src).row_stride * (*src).height) as usize);
1155
- let blit_src = BlitSource {
1156
- data: src_data,
1157
- width: (*src).width,
1158
- height: (*src).height,
1159
- row_stride: (*src).row_stride,
1160
- };
1161
-
1162
- let dst_data_len = ((*dst).row_stride * (*dst).height) as usize;
1163
- let mut dst_grid = openwa_core::render::display::line_draw::PixelGridMut {
1164
- data: core::slice::from_raw_parts_mut((*dst).data, dst_data_len),
1165
- width: (*dst).width,
1166
- height: (*dst).height,
1167
- row_stride: (*dst).row_stride,
1168
- clip_left: (*dst).clip_left,
1169
- clip_top: (*dst).clip_top,
1170
- clip_right: (*dst).clip_right,
1171
- clip_bottom: (*dst).clip_bottom,
1172
- };
1173
-
1174
- blit_stippled(
1175
- &mut dst_grid,
1176
- &blit_src,
1177
  dst_x,
1178
  dst_y,
1179
  width,
1180
  height,
 
1181
  src_x,
1182
  src_y,
1183
- stipple_mode,
1184
- parity,
1185
- );
1186
- }
1187
-
1188
- /// Tiled blit on raw BitGrid pointers.
1189
- ///
1190
- /// Wraps the raw pointers into PixelGrid/BlitSource and calls
1191
- /// `sprite_blit::blit_tiled`.
1192
- pub(crate) unsafe fn blit_tiled_raw(
1193
- dst: *mut openwa_core::bitgrid::DisplayBitGrid,
1194
- src: *mut openwa_core::bitgrid::DisplayBitGrid,
1195
- initial_x: i32,
1196
- dst_y: i32,
1197
- tile_width: i32,
1198
- tile_height: i32,
1199
- clip_left: i32,
1200
- clip_right: i32,
1201
- flags: u32,
1202
- ) {
1203
- use openwa_core::render::display::sprite_blit::{blit_tiled, BlitSource};
1204
-
1205
- let src_data =
1206
- core::slice::from_raw_parts((*src).data, ((*src).row_stride * (*src).height) as usize);
1207
- let blit_src = BlitSource {
1208
- data: src_data,
1209
- width: (*src).width,
1210
- height: (*src).height,
1211
- row_stride: (*src).row_stride,
1212
- };
1213
-
1214
- let dst_data_len = ((*dst).row_stride * (*dst).height) as usize;
1215
- let mut dst_grid = openwa_core::render::display::line_draw::PixelGridMut {
1216
- data: core::slice::from_raw_parts_mut((*dst).data, dst_data_len),
1217
- width: (*dst).width,
1218
- height: (*dst).height,
1219
- row_stride: (*dst).row_stride,
1220
- clip_left: (*dst).clip_left,
1221
- clip_top: (*dst).clip_top,
1222
- clip_right: (*dst).clip_right,
1223
- clip_bottom: (*dst).clip_bottom,
1224
- };
1225
-
1226
- blit_tiled(
1227
- &mut dst_grid,
1228
- &blit_src,
1229
- initial_x,
1230
- dst_y,
1231
- tile_width,
1232
- tile_height,
1233
- clip_left,
1234
- clip_right,
1235
- None, // color_table — tiled mode doesn't use one
1236
  flags,
1237
- );
1238
  }
1239
 
1240
  /// Call the original WA blit function via trampoline.
 
827
  /// Trampoline to the original WA blit function (for unsupported modes).
828
  static ORIG_BLIT: AtomicU32 = AtomicU32::new(0);
829
 
830
+ /// Naked trampoline: captures ESI (dst BitGrid) and forwards to cdecl wrapper.
831
  ///
832
  /// Original calling convention: ESI=dst, 9 stdcall params, RET 0x24.
833
  #[unsafe(naked)]
 
850
  "mov esp, ebp",
851
  "pop ebp",
852
  "ret 0x24",
853
+ impl_fn = sym blit_impl_cdecl,
854
  );
855
  }
856
 
857
+ /// Thin cdecl wrapper around `openwa_core::bitgrid::blit::blit_impl`.
858
  ///
859
+ /// The naked asm trampoline calls this with cdecl convention; this simply
860
+ /// forwards to the core implementation which is a plain `unsafe fn`.
861
+ unsafe extern "cdecl" fn blit_impl_cdecl(
 
 
 
862
  dst: *mut openwa_core::bitgrid::DisplayBitGrid,
863
  dst_x: i32,
864
  dst_y: i32,
 
870
  color_table: *const u8,
871
  flags: u32,
872
  ) -> u32 {
873
+ openwa_core::bitgrid::blit::blit_impl(
874
+ dst,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
875
  dst_x,
876
  dst_y,
877
  width,
878
  height,
879
+ src,
880
  src_x,
881
  src_y,
882
+ color_table,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
883
  flags,
884
+ )
885
  }
886
 
887
  /// Call the original WA blit function via trampoline.
crates/openwa-dll/src/replacements/render.rs DELETED
@@ -1,1463 +0,0 @@
1
- //! Render subsystem hooks — RenderQueue enqueue + DisplayGfx vtable patches.
2
- //!
3
- //! Combines two subsystems:
4
- //! - RenderQueue enqueue hooks (full Rust replacements for command enqueueing)
5
- //! - DisplayGfx vtable patches (DisplayBase pure-call stubs, headless destructor,
6
- //! and ported DisplayGfx methods)
7
-
8
- use core::ffi::c_char;
9
-
10
- use openwa_core::address::va;
11
- use openwa_core::rebase::rb;
12
- use openwa_core::render::queue::*;
13
- use openwa_core::render::sprite::gfx_dir::GfxDir;
14
- use openwa_core::render::SpriteOp;
15
- use openwa_core::task::{BungeeTrailTask, WeaponAimTask};
16
-
17
- use crate::hook::{self, usercall_trampoline};
18
- use crate::log_line;
19
-
20
- // ==========================================================================
21
- // RenderQueue enqueue hooks
22
- // ==========================================================================
23
- //
24
- // All functions enqueue commands to the RenderQueue's downward-growing buffer.
25
- // Calling conventions are __usercall variants with register + stack params.
26
-
27
- // ---------------------------------------------------------------------------
28
- // EnqueueTiledBitmap (0x541D60) — type 0xD, EAX=this, 3 stack, RET 0xC
29
- //
30
- // Mis-labelled `RQ_DrawPixel` in earlier reverse-engineering passes. It does
31
- // NOT enqueue a single-pixel draw — it enqueues a tile-cached bitmap draw
32
- // dispatched by `RenderDrawingQueue` case 0xD into vtable slot 11.
33
- // ---------------------------------------------------------------------------
34
-
35
- usercall_trampoline!(fn trampoline_enqueue_tiled_bitmap; impl_fn = enqueue_tiled_bitmap_impl;
36
- reg = eax; stack_params = 3; ret_bytes = "0xC");
37
-
38
- unsafe extern "cdecl" fn enqueue_tiled_bitmap_impl(
39
- this: u32,
40
- y_fixed16: u32,
41
- source_descriptor: u32,
42
- flags: u32,
43
- ) {
44
- let q = &mut *(this as *mut RenderQueue);
45
-
46
- if let Some(entry) = q.alloc::<DrawTiledBitmapCmd>() {
47
- *entry = DrawTiledBitmapCmd {
48
- command_type: command_type::DRAW_TILED_BITMAP,
49
- layer: 0x1B_0000,
50
- x_fixed16: 0xFF00_0000,
51
- y_fixed16,
52
- source_descriptor,
53
- flags: flags as u8,
54
- _pad: [0; 3],
55
- };
56
- }
57
- }
58
-
59
- // ---------------------------------------------------------------------------
60
- // DrawLineStrip (0x541DD0) — type 8, EAX=this, EDI=count, 2 stack, RET 0x8
61
- // Allocation: count * 0xC + 0x1C (variable size)
62
- // ---------------------------------------------------------------------------
63
-
64
- usercall_trampoline!(fn trampoline_draw_line_strip; impl_fn = draw_line_strip_impl;
65
- regs = [eax, edi]; stack_params = 2; ret_bytes = "0x8");
66
-
67
- unsafe extern "cdecl" fn draw_line_strip_impl(
68
- this: u32,
69
- count: u32,
70
- vertex_ptr: u32,
71
- param_1: u32,
72
- ) {
73
- let q = &mut *(this as *mut RenderQueue);
74
- let total_size = count as usize * 0xC + 0x1C;
75
-
76
- if let Some(ptr) = q.alloc_raw(total_size) {
77
- let header = &mut *(ptr as *mut DrawLineStripHeader);
78
- *header = DrawLineStripHeader {
79
- command_type: command_type::DRAW_LINE_STRIP,
80
- layer: 0xE_0000,
81
- count,
82
- param_1,
83
- };
84
- core::ptr::copy_nonoverlapping(
85
- vertex_ptr as *const u8,
86
- ptr.add(core::mem::size_of::<DrawLineStripHeader>()),
87
- count as usize * 0xC,
88
- );
89
- }
90
- }
91
-
92
- // ---------------------------------------------------------------------------
93
- // DrawPolygon (0x541E50) — type 9, ECX=this, ESI=count, 3 stack, RET 0xC
94
- // Allocation: count * 0xC + 0x20 (variable size)
95
- // ---------------------------------------------------------------------------
96
-
97
- usercall_trampoline!(fn trampoline_draw_polygon; impl_fn = draw_polygon_impl;
98
- regs = [ecx, esi]; stack_params = 3; ret_bytes = "0xC");
99
-
100
- unsafe extern "cdecl" fn draw_polygon_impl(
101
- this: u32,
102
- count: u32,
103
- vertex_ptr: u32,
104
- param_1: u32,
105
- param_2: u32,
106
- ) {
107
- let q = &mut *(this as *mut RenderQueue);
108
- let total_size = count as usize * 0xC + 0x20;
109
-
110
- if let Some(ptr) = q.alloc_raw(total_size) {
111
- let header = &mut *(ptr as *mut DrawPolygonHeader);
112
- *header = DrawPolygonHeader {
113
- command_type: command_type::DRAW_POLYGON,
114
- layer: 0xE_0000,
115
- count,
116
- param_1,
117
- param_2,
118
- };
119
- core::ptr::copy_nonoverlapping(
120
- vertex_ptr as *const u8,
121
- ptr.add(core::mem::size_of::<DrawPolygonHeader>()),
122
- count as usize * 0xC,
123
- );
124
- }
125
- }
126
-
127
- // ---------------------------------------------------------------------------
128
- // DrawCrosshair (0x541ED0) — type 0xB, ECX=this, 5 stack, RET 0x14
129
- // Enqueues a crosshair draw command. Dispatched by RenderDrawingQueue
130
- // case 0xB → DisplayGfx::draw_crosshair (vtable slot 16).
131
- // ---------------------------------------------------------------------------
132
-
133
- usercall_trampoline!(fn trampoline_draw_crosshair; impl_fn = draw_crosshair_impl;
134
- reg = ecx; stack_params = 5; ret_bytes = "0x14");
135
-
136
- unsafe extern "cdecl" fn draw_crosshair_impl(
137
- this: u32,
138
- layer: u32,
139
- x_pos: u32,
140
- y_pos: u32,
141
- color_fg: u32,
142
- color_bg: u32,
143
- ) {
144
- let q = &mut *(this as *mut RenderQueue);
145
-
146
- if let Some(entry) = q.alloc::<DrawCrosshairCmd>() {
147
- *entry = DrawCrosshairCmd {
148
- command_type: command_type::DRAW_CROSSHAIR,
149
- layer,
150
- color_fg,
151
- color_bg,
152
- x_pos,
153
- y_pos,
154
- _reserved: 0,
155
- };
156
- }
157
- }
158
-
159
- // ---------------------------------------------------------------------------
160
- // DrawRect (0x541F40) — type 0, ECX=this, EDX=y_clip, 6 stack, RET 0x18
161
- // ---------------------------------------------------------------------------
162
-
163
- usercall_trampoline!(fn trampoline_draw_rect; impl_fn = draw_rect_impl;
164
- regs = [ecx, edx]; stack_params = 6; ret_bytes = "0x18");
165
-
166
- unsafe extern "cdecl" fn draw_rect_impl(
167
- this: u32,
168
- y_clip: u32,
169
- layer: u32,
170
- x1: u32,
171
- y1: u32,
172
- x2: u32,
173
- y2: u32,
174
- color: u32,
175
- ) {
176
- let q = &mut *(this as *mut RenderQueue);
177
-
178
- if let Some(entry) = q.alloc::<DrawRectCmd>() {
179
- *entry = DrawRectCmd {
180
- command_type: command_type::DRAW_RECT,
181
- layer,
182
- color,
183
- x1: x1 & 0xFFFF0000,
184
- y1: y1 & 0xFFFF0000,
185
- x2: x2 & 0xFFFF0000,
186
- y2: y2 & 0xFFFF0000,
187
- y_clip: y_clip & 0xFFFF0000,
188
- };
189
- }
190
- }
191
-
192
- // ---------------------------------------------------------------------------
193
- // DrawSpriteGlobal (0x541FE0) — type 4, EAX=y, ECX=this, 4 stack, RET 0x10
194
- // ---------------------------------------------------------------------------
195
-
196
- usercall_trampoline!(fn trampoline_draw_sprite_global; impl_fn = draw_sprite_global_impl;
197
- regs = [eax, ecx]; stack_params = 4; ret_bytes = "0x10");
198
-
199
- unsafe extern "cdecl" fn draw_sprite_global_impl(
200
- y_pos: u32,
201
- this: u32,
202
- layer: u32,
203
- x_pos: u32,
204
- sprite_id: u32,
205
- frame: u32,
206
- ) {
207
- let q = &mut *(this as *mut RenderQueue);
208
-
209
- if let Some(entry) = q.alloc::<DrawSpriteCmd>() {
210
- *entry = DrawSpriteCmd {
211
- command_type: command_type::DRAW_SPRITE_GLOBAL,
212
- layer,
213
- x_pos: x_pos & 0xFFFF0000,
214
- y_pos: y_pos & 0xFFFF0000,
215
- sprite: SpriteOp(sprite_id),
216
- palette: frame,
217
- };
218
- }
219
- }
220
-
221
- // ---------------------------------------------------------------------------
222
- // DrawSpriteLocal (0x542060) — type 5, EAX=y, ECX=this, 4 stack, RET 0x10
223
- // ---------------------------------------------------------------------------
224
-
225
- usercall_trampoline!(fn trampoline_draw_sprite_local; impl_fn = draw_sprite_local_impl;
226
- regs = [eax, ecx]; stack_params = 4; ret_bytes = "0x10");
227
-
228
- unsafe extern "cdecl" fn draw_sprite_local_impl(
229
- y_pos: u32,
230
- this: u32,
231
- layer: u32,
232
- x_pos: u32,
233
- sprite_id: u32,
234
- frame: u32,
235
- ) {
236
- let q = &mut *(this as *mut RenderQueue);
237
-
238
- if let Some(entry) = q.alloc::<DrawSpriteCmd>() {
239
- *entry = DrawSpriteCmd {
240
- command_type: command_type::DRAW_SPRITE_LOCAL,
241
- layer,
242
- x_pos: x_pos & 0xFFFF0000,
243
- y_pos: y_pos & 0xFFFF0000,
244
- sprite: SpriteOp(sprite_id),
245
- palette: frame,
246
- };
247
- }
248
- }
249
-
250
- // ---------------------------------------------------------------------------
251
- // DrawSpriteOffset (0x5420E0) — type 6, ECX=this, EDX=y_clip, 6 stack, RET 0x18
252
- // ---------------------------------------------------------------------------
253
-
254
- usercall_trampoline!(fn trampoline_draw_sprite_offset; impl_fn = draw_sprite_offset_impl;
255
- regs = [ecx, edx]; stack_params = 6; ret_bytes = "0x18");
256
-
257
- unsafe extern "cdecl" fn draw_sprite_offset_impl(
258
- this: u32,
259
- y_clip: u32,
260
- layer: u32,
261
- x_pos: u32,
262
- y_pos: u32,
263
- sprite_id: u32,
264
- param_7: u32,
265
- param_8: u32,
266
- ) {
267
- let q = &mut *(this as *mut RenderQueue);
268
-
269
- if let Some(entry) = q.alloc::<DrawSpriteOffsetCmd>() {
270
- *entry = DrawSpriteOffsetCmd {
271
- command_type: command_type::DRAW_SPRITE_OFFSET,
272
- layer,
273
- sprite_id,
274
- x_pos: x_pos & 0xFFFF0000,
275
- y_pos: y_pos & 0xFFFF0000,
276
- _reserved: 0,
277
- y_clip: y_clip & 0xFFFF0000,
278
- param_7,
279
- param_8,
280
- };
281
- }
282
- }
283
-
284
- // ---------------------------------------------------------------------------
285
- // DrawBitmapGlobal (0x542170) — type 1, ECX=this, EDX=y, 7 stack, RET 0x1C
286
- // ---------------------------------------------------------------------------
287
-
288
- usercall_trampoline!(fn trampoline_draw_bitmap_global; impl_fn = draw_bitmap_global_impl;
289
- regs = [ecx, edx]; stack_params = 7; ret_bytes = "0x1C");
290
-
291
- unsafe extern "cdecl" fn draw_bitmap_global_impl(
292
- this: u32,
293
- y_pos: u32,
294
- layer: u32,
295
- x_pos: u32,
296
- bitmap_ptr: u32,
297
- param_6: u32,
298
- param_7: u32,
299
- param_8: u32,
300
- param_9: u32,
301
- ) {
302
- let q = &mut *(this as *mut RenderQueue);
303
-
304
- if let Some(entry) = q.alloc::<DrawBitmapGlobalCmd>() {
305
- *entry = DrawBitmapGlobalCmd {
306
- command_type: command_type::DRAW_BITMAP_GLOBAL,
307
- layer,
308
- x_pos: x_pos & 0xFFFF0000,
309
- y_pos: y_pos & 0xFFFF0000,
310
- bitmap_ptr,
311
- _reserved: 0,
312
- param_6,
313
- param_7,
314
- param_8,
315
- param_9,
316
- };
317
- }
318
- }
319
-
320
- // ---------------------------------------------------------------------------
321
- // DrawTextboxLocal (0x542200) — type 2, ECX=this, EDX=y, 6 stack, RET 0x18
322
- // ---------------------------------------------------------------------------
323
-
324
- usercall_trampoline!(fn trampoline_draw_textbox_local; impl_fn = draw_textbox_local_impl;
325
- regs = [ecx, edx]; stack_params = 6; ret_bytes = "0x18");
326
-
327
- unsafe extern "cdecl" fn draw_textbox_local_impl(
328
- this: u32,
329
- y_pos: u32,
330
- layer: u32,
331
- x_pos: u32,
332
- text_ptr: u32,
333
- param_6: u32,
334
- param_7: u32,
335
- param_8: u32,
336
- ) {
337
- let q = &mut *(this as *mut RenderQueue);
338
-
339
- if let Some(entry) = q.alloc::<DrawTextboxLocalCmd>() {
340
- *entry = DrawTextboxLocalCmd {
341
- command_type: command_type::DRAW_TEXTBOX_LOCAL,
342
- layer,
343
- _reserved_2: 0,
344
- x_pos: x_pos & 0xFFFF0000,
345
- y_pos: y_pos & 0xFFFF0000,
346
- _reserved_5: 0,
347
- _reserved_6: 0,
348
- text_ptr,
349
- _reserved_8: 0,
350
- _reserved_9: 0,
351
- param_6,
352
- param_7,
353
- param_8,
354
- };
355
- }
356
- }
357
-
358
- // ---------------------------------------------------------------------------
359
- // Fixed-point trig helpers
360
- // ---------------------------------------------------------------------------
361
-
362
- /// Fixed-point 16.16 multiply: ((a * b) >> 16)
363
- #[inline]
364
- fn fixed_mul(a: i32, b: i32) -> i32 {
365
- ((a as i64 * b as i64) >> 16) as i32
366
- }
367
-
368
- /// Interpolated lookup from a 1024-entry fixed-point trig table.
369
- /// Index = (angle >> 6) & 0x3FF, fraction = (angle & 0x3F) << 10.
370
- #[inline]
371
- unsafe fn trig_lookup(table: *const i32, angle: u32) -> i32 {
372
- let index = ((angle as i32) >> 6) as usize & 0x3FF;
373
- let frac = ((angle & 0x3F) << 10) as i32;
374
- let base = *table.add(index);
375
- let next = *table.add(index + 1);
376
- fixed_mul(next - base, frac) + base
377
- }
378
-
379
- // ---------------------------------------------------------------------------
380
- // DrawBungeeTrail (0x500720) — stdcall(task, style, fill), RET 0xC
381
- //
382
- // Draws bungee drop trajectory path:
383
- // 1. Sprite at trail start position
384
- // 2. Series of vertices computed by accumulating angle + trig interpolation
385
- // 3. Final vertex at task position (0x84/0x88)
386
- // 4. DrawPolygon (if fill != 0) or DrawLineStrip
387
- // Triggered by Bungee weapon (field_0x30==4, field_0x34==7) check in FUN_00519F60.
388
- // Gated by task+0xBC flag set by InitWormTrail (0x5008D0).
389
- // ---------------------------------------------------------------------------
390
-
391
- unsafe extern "stdcall" fn draw_bungee_trail_impl(task_ptr: u32, style: u32, fill: u32) {
392
- let task = &*(task_ptr as *const BungeeTrailTask);
393
-
394
- if task.trail_visible == 0 {
395
- return;
396
- }
397
-
398
- let ddgame = &*task.base.ddgame;
399
- let rq = &mut *ddgame.render_queue;
400
-
401
- let seg_data = task.segment_data;
402
- if seg_data.is_null() {
403
- return;
404
- }
405
-
406
- let segment_count = task.segment_count;
407
- if segment_count <= 0 {
408
- return;
409
- }
410
-
411
- let mut x = task.trail_start_x;
412
- let mut y = task.trail_start_y;
413
-
414
- let first_angle = *(seg_data.add(4) as *const i32);
415
-
416
- // Enqueue start sprite (command type 5 = local)
417
- if let Some(entry) = rq.alloc::<DrawSpriteCmd>() {
418
- *entry = DrawSpriteCmd {
419
- command_type: command_type::DRAW_SPRITE_LOCAL,
420
- layer: 0xDFFFF,
421
- x_pos: x as u32 & 0xFFFF0000,
422
- y_pos: y as u32 & 0xFFFF0000,
423
- sprite: SpriteOp(0x45),
424
- palette: (first_angle + 0x8100) as u32,
425
- };
426
- }
427
-
428
- // Build vertex array from trail segments
429
- const MAX_VERTICES: usize = 256;
430
- let mut verts = [[0i32; 3]; MAX_VERTICES];
431
- let mut vert_count: usize = 0;
432
- let mut accumulated_angle: u32 = 0;
433
-
434
- let sin_table = rb(va::G_SIN_TABLE) as *const i32;
435
- let cos_table = rb(va::G_COS_TABLE) as *const i32;
436
-
437
- for i in 0..segment_count {
438
- let seg_angle = *(seg_data.add(4 + i as usize * 8) as *const i32);
439
-
440
- // Include vertex if: first segment, or segment has nonzero angle, or fill mode
441
- if (i == 0 || seg_angle != 0 || fill != 0) && vert_count < MAX_VERTICES {
442
- verts[vert_count] = [x, y, 0];
443
- vert_count += 1;
444
- }
445
-
446
- accumulated_angle = accumulated_angle.wrapping_add(seg_angle as u32);
447
-
448
- let sin_interp = trig_lookup(sin_table, accumulated_angle);
449
- let cos_interp = trig_lookup(cos_table, accumulated_angle);
450
-
451
- x = x.wrapping_add(sin_interp.wrapping_mul(8));
452
- y = y.wrapping_sub(cos_interp.wrapping_mul(8));
453
- }
454
-
455
- // Final vertex = task position (target)
456
- if vert_count < MAX_VERTICES {
457
- verts[vert_count] = [task.pos_x.0, task.pos_y.0, 0];
458
- vert_count += 1;
459
- }
460
-
461
- // Enqueue as polygon or line strip
462
- if fill != 0 {
463
- let total_size = vert_count * 0xC + 0x20;
464
- if let Some(ptr) = rq.alloc_raw(total_size) {
465
- let header = &mut *(ptr as *mut DrawPolygonHeader);
466
- *header = DrawPolygonHeader {
467
- command_type: command_type::DRAW_POLYGON,
468
- layer: 0xE_0000,
469
- count: vert_count as u32,
470
- param_1: style,
471
- param_2: fill,
472
- };
473
- core::ptr::copy_nonoverlapping(
474
- verts.as_ptr() as *const u8,
475
- ptr.add(core::mem::size_of::<DrawPolygonHeader>()),
476
- vert_count * 0xC,
477
- );
478
- }
479
- } else {
480
- let total_size = vert_count * 0xC + 0x1C;
481
- if let Some(ptr) = rq.alloc_raw(total_size) {
482
- let header = &mut *(ptr as *mut DrawLineStripHeader);
483
- *header = DrawLineStripHeader {
484
- command_type: command_type::DRAW_LINE_STRIP,
485
- layer: 0xE_0000,
486
- count: vert_count as u32,
487
- param_1: style,
488
- };
489
- core::ptr::copy_nonoverlapping(
490
- verts.as_ptr() as *const u8,
491
- ptr.add(core::mem::size_of::<DrawLineStripHeader>()),
492
- vert_count * 0xC,
493
- );
494
- }
495
- }
496
- }
497
-
498
- // ---------------------------------------------------------------------------
499
- // DrawCrosshairLine (0x5197D0) — usercall(EDI=task_ptr), plain RET
500
- //
501
- // Draws the weapon aiming crosshair line:
502
- // 1. Compute direction from angle at task+0x264
503
- // 2. Compute line length from DDGame scale + task offset
504
- // 3. Endpoint = start + direction * length (with overflow clamping)
505
- // 4. DrawPolygon (2 vertices) for the line
506
- // 5. Conditionally DrawSpriteLocal at endpoint (crosshair sprite)
507
- // ---------------------------------------------------------------------------
508
-
509
- usercall_trampoline!(fn trampoline_draw_crosshair_line; impl_fn = draw_crosshair_line_impl;
510
- reg = edi);
511
-
512
- unsafe extern "cdecl" fn draw_crosshair_line_impl(task_ptr: u32) {
513
- let task = &*(task_ptr as *const WeaponAimTask);
514
- let gt = &task.game_task;
515
-
516
- if task.aim_active == 0 {
517
- return;
518
- }
519
-
520
- let ddgame = &*gt.base.ddgame;
521
- let rq = &mut *ddgame.render_queue;
522
-
523
- let start_x = gt.pos_x.0;
524
- let start_y = gt.pos_y.0;
525
-
526
- let angle = task.aim_angle;
527
-
528
- // Trig interpolation
529
- let sin_table = rb(va::G_SIN_TABLE) as *const i32;
530
- let cos_table = rb(va::G_COS_TABLE) as *const i32;
531
- let sin_interp = trig_lookup(sin_table, angle);
532
- let cos_interp = trig_lookup(cos_table, angle);
533
-
534
- // Scale = fixed_mul(DDGame.parallax_scale, 0x140000) + task.aim_range_offset
535
- let scale = fixed_mul(ddgame.parallax_scale, 0x14_0000) + task.aim_range_offset;
536
-
537
- // Endpoint = start + direction * scale
538
- let mut endpoint_x = fixed_mul(sin_interp, scale).wrapping_add(start_x);
539
- let mut endpoint_y = fixed_mul(cos_interp, scale).wrapping_add(start_y);
540
-
541
- // Overflow clamping — when endpoint overflows i32 due to large scale
542
- let mut overflowed = false;
543
- let mut clamp_factor = 0i32;
544
-
545
- let threshold = (*ddgame.game_info).game_version;
546
-
547
- if threshold > 0x11E {
548
- // Check X overflow: sin > 0 but endpoint wrapped below start
549
- if sin_interp > 0 && endpoint_x < start_x {
550
- overflowed = true;
551
- clamp_factor = (0x7FFFFFFFi32 - start_x) / sin_interp;
552
- }
553
- // Check Y overflow: cos > 0 but endpoint wrapped below start
554
- if cos_interp > 0 && endpoint_y < start_y {
555
- let y_clamp = (0x7FFFFFFFi32 - start_y) / cos_interp;
556
- if !overflowed || y_clamp < clamp_factor {
557
- clamp_factor = y_clamp;
558
- }
559
- overflowed = true;
560
- }
561
- if overflowed {
562
- endpoint_x = start_x + clamp_factor * sin_interp;
563
- endpoint_y = start_y + clamp_factor * cos_interp;
564
- }
565
- }
566
-
567
- // Enqueue polygon line (2 vertices)
568
- let poly_param_1 = ddgame.gfx_color_table[8]; // crosshair line style
569
- let poly_param_2 = ddgame.gfx_color_table[6]; // crosshair line color
570
- let verts: [[i32; 3]; 2] = [[start_x, start_y, 0], [endpoint_x, endpoint_y, 0]];
571
- let total_size = 2 * 0xC + 0x20;
572
- if let Some(ptr) = rq.alloc_raw(total_size) {
573
- let header = &mut *(ptr as *mut DrawPolygonHeader);
574
- *header = DrawPolygonHeader {
575
- command_type: command_type::DRAW_POLYGON,
576
- layer: 0xE_0000,
577
- count: 2,
578
- param_1: poly_param_1,
579
- param_2: poly_param_2,
580
- };
581
- core::ptr::copy_nonoverlapping(
582
- verts.as_ptr() as *const u8,
583
- ptr.add(core::mem::size_of::<DrawPolygonHeader>()),
584
- 2 * 0xC,
585
- );
586
- }
587
-
588
- // Draw crosshair sprite at endpoint (only if no overflow clamping)
589
- if !overflowed {
590
- if let Some(entry) = rq.alloc::<DrawSpriteCmd>() {
591
- *entry = DrawSpriteCmd {
592
- command_type: command_type::DRAW_SPRITE_LOCAL,
593
- layer: 0x4_0000,
594
- x_pos: endpoint_x as u32 & 0xFFFF0000,
595
- y_pos: endpoint_y as u32 & 0xFFFF0000,
596
- sprite: SpriteOp(0x44),
597
- palette: (0x8000u32).wrapping_sub(angle),
598
- };
599
- }
600
- }
601
- }
602
-
603
- // ---------------------------------------------------------------------------
604
- // RenderQueue installation
605
- // ---------------------------------------------------------------------------
606
-
607
- fn install_render_queue() -> Result<(), String> {
608
- unsafe {
609
- let _ = hook::install(
610
- "EnqueueTiledBitmap",
611
- va::RQ_ENQUEUE_TILED_BITMAP,
612
- trampoline_enqueue_tiled_bitmap as *const (),
613
- )?;
614
-
615
- let _ = hook::install(
616
- "DrawLineStrip",
617
- va::RQ_DRAW_LINE_STRIP,
618
- trampoline_draw_line_strip as *const (),
619
- )?;
620
-
621
- let _ = hook::install(
622
- "DrawPolygon",
623
- va::RQ_DRAW_POLYGON,
624
- trampoline_draw_polygon as *const (),
625
- )?;
626
-
627
- let _ = hook::install(
628
- "DrawCrosshair",
629
- va::RQ_DRAW_CROSSHAIR,
630
- trampoline_draw_crosshair as *const (),
631
- )?;
632
-
633
- let _ = hook::install(
634
- "DrawRect",
635
- va::RQ_DRAW_RECT,
636
- trampoline_draw_rect as *const (),
637
- )?;
638
-
639
- let _ = hook::install(
640
- "DrawSpriteGlobal",
641
- va::RQ_DRAW_SPRITE_GLOBAL,
642
- trampoline_draw_sprite_global as *const (),
643
- )?;
644
-
645
- let _ = hook::install(
646
- "DrawSpriteLocal",
647
- va::RQ_DRAW_SPRITE_LOCAL,
648
- trampoline_draw_sprite_local as *const (),
649
- )?;
650
-
651
- let _ = hook::install(
652
- "DrawSpriteOffset",
653
- va::RQ_DRAW_SPRITE_OFFSET,
654
- trampoline_draw_sprite_offset as *const (),
655
- )?;
656
-
657
- let _ = hook::install(
658
- "DrawBitmapGlobal",
659
- va::RQ_DRAW_BITMAP_GLOBAL,
660
- trampoline_draw_bitmap_global as *const (),
661
- )?;
662
-
663
- let _ = hook::install(
664
- "DrawTextboxLocal",
665
- va::RQ_DRAW_TEXTBOX_LOCAL,
666
- trampoline_draw_textbox_local as *const (),
667
- )?;
668
-
669
- let _ = hook::install(
670
- "DrawBungeeTrail",
671
- va::DRAW_BUNGEE_TRAIL,
672
- draw_bungee_trail_impl as *const (),
673
- )?;
674
-
675
- let _ = hook::install(
676
- "DrawCrosshairLine",
677
- va::DRAW_CROSSHAIR_LINE,
678
- trampoline_draw_crosshair_line as *const (),
679
- )?;
680
-
681
- let _ = hook::install(
682
- "RenderDrawingQueue",
683
- va::RQ_RENDER_DRAWING_QUEUE,
684
- trampoline_render_drawing_queue as *const (),
685
- )?;
686
- }
687
- Ok(())
688
- }
689
-
690
- // ---------------------------------------------------------------------------
691
- // RenderDrawingQueue (0x542350) — usercall(EAX=RenderQueue*) + 2 stack
692
- // (DisplayGfx*, ClipContext*), RET 0x8.
693
- //
694
- // The per-frame render-queue dispatcher. Pure Rust port lives in
695
- // `openwa_core::render::queue_dispatch::render_drawing_queue`. The
696
- // trampoline captures EAX and forwards the two stack args.
697
- // ---------------------------------------------------------------------------
698
-
699
- use openwa_core::render::queue_dispatch::{render_drawing_queue, ClipContext};
700
-
701
- usercall_trampoline!(fn trampoline_render_drawing_queue;
702
- impl_fn = render_drawing_queue_impl;
703
- reg = eax; stack_params = 2; ret_bytes = "0x8");
704
-
705
- unsafe extern "cdecl" fn render_drawing_queue_impl(
706
- rq: *mut RenderQueue,
707
- display: *mut DisplayGfx,
708
- clip: *mut ClipContext,
709
- ) {
710
- render_drawing_queue(rq, display, clip);
711
- }
712
-
713
- // ==========================================================================
714
- // DisplayGfx vtable hooks
715
- // ==========================================================================
716
- //
717
- // Patches DisplayBase vtables in WA.exe's .rdata:
718
- // - Primary vtable (0x6645F8): replaces _purecall slots with safe no-op stubs
719
- // - Headless vtable (0x66A0F8): replaces destructor with Rust version that
720
- // correctly frees our Rust-allocated sprite cache sub-objects
721
-
722
- use openwa_core::bitgrid::DisplayBitGrid;
723
- use openwa_core::fixed::Fixed;
724
- use openwa_core::render::display::destructor as display_destructor;
725
- use openwa_core::render::display::vtable::{self as display_vtable, DisplayGfxVtable};
726
- use openwa_core::render::display::DisplayBase;
727
- use openwa_core::render::display::DisplayGfx;
728
- use openwa_core::vtable::patch_vtable;
729
- use openwa_core::vtable_replace;
730
- use openwa_core::wa_alloc::wa_free;
731
-
732
- /// The _purecall function address (calls abort).
733
- const PURECALL: u32 = 0x005D_4E16;
734
-
735
- /// Number of slots in the DisplayBase vtable.
736
- const VTABLE_SLOTS: usize = 32;
737
-
738
- unsafe extern "thiscall" fn noop_thiscall(_this: *mut u8) {}
739
-
740
- /// Rust destructor for headless DisplayBase. Frees the sprite cache chain
741
- /// (SpriteCache -> FrameCache -> buffer) that was allocated by new_headless().
742
- unsafe extern "thiscall" fn headless_destructor(
743
- this: *mut DisplayBase,
744
- flags: u8,
745
- ) -> *mut DisplayBase {
746
- let sprite_cache = (*this).sprite_cache;
747
- if !sprite_cache.is_null() {
748
- let frame_cache = (*sprite_cache).frame_cache;
749
- if !frame_cache.is_null() {
750
- let buf = (*frame_cache).buffer;
751
- if !buf.is_null() {
752
- wa_free(buf);
753
- }
754
- wa_free(frame_cache);
755
- }
756
- wa_free(sprite_cache);
757
- }
758
- if flags & 1 != 0 {
759
- wa_free(this);
760
- }
761
- this
762
- }
763
-
764
- // No saved originals needed — all paths are fully ported or use direct bridges.
765
-
766
- /// Rust port of DisplayGfx::BlitSprite (slot 19, 0x56B080).
767
- ///
768
- /// Standard thiscall: ECX=this, stack params: x, y, sprite_flags, palette (RET 0x10).
769
- ///
770
- unsafe extern "thiscall" fn blit_sprite(
771
- this: *mut DisplayGfx,
772
- x: Fixed,
773
- y: Fixed,
774
- sprite_flags: u32,
775
- palette: u32,
776
- ) {
777
- use openwa_core::render::display::gfx::DisplayGfx;
778
- use openwa_core::render::display::vtable as display_vtable;
779
- use openwa_core::render::SpriteFlags;
780
-
781
- let gfx = this as *mut DisplayGfx;
782
- let base = this as *const u8;
783
-
784
- // ---------------------------------------------------------------
785
- // Extract sprite index and flags
786
- // ---------------------------------------------------------------
787
- let sprite = SpriteOp(sprite_flags);
788
- let flags = sprite.flags();
789
- let sprite_id = sprite.index() as u32;
790
-
791
- if sprite_id == 0 {
792
- return;
793
- }
794
-
795
- // ---------------------------------------------------------------
796
- // Palette manipulation
797
- // ---------------------------------------------------------------
798
- let mut pal: u32 = palette;
799
- if flags.contains(SpriteFlags::INVERT_PALETTE) {
800
- pal = 0x10000u32.wrapping_sub(palette);
801
- if sprite_id.wrapping_sub(0x1D5) < 3 {
802
- // Special sprite IDs: scale by 8/18
803
- pal = (0x10000u32.wrapping_sub(palette).wrapping_mul(8)) / 0x12;
804
- }
805
- }
806
- if flags.contains(SpriteFlags::PALETTE_XFORM) {
807
- // Bit 25: palette transform (modular arithmetic for color cycling)
808
- let tmp = ((pal.wrapping_mul(0x1F) as i32)
809
- .wrapping_add(((pal.wrapping_mul(0x1F) as i32) >> 31) & 0x1F)
810
- >> 5) as u32;
811
- let tmp = tmp.wrapping_add(0x400) & 0xFFFF;
812
- pal = (tmp.wrapping_rem(0xF800)) / 2;
813
- if (pal & 0x400) != 0 {
814
- pal = (pal & !0x400) | 0x8000;
815
- }
816
- }
817
-
818
- // ---------------------------------------------------------------
819
- // Check sprite arrays — bitmap path if not in primary arrays
820
- // ---------------------------------------------------------------
821
- let arr1 = *(base.add(sprite_id as usize * 4 + 0x1008) as *const u32);
822
- let arr2 = *(base.add(sprite_id as usize * 4 + 0x2008) as *const u32);
823
-
824
- if arr1 == 0 && arr2 == 0 {
825
- // Bitmap sprite path — sprite is in the bitmap table at 0x3DD4.
826
- let bitmap_obj = (*gfx).sprite_table[sprite_id as usize];
827
- if bitmap_obj.is_null() {
828
- return;
829
- }
830
-
831
- // Get frame data and dimensions from bitmap sprite object.
832
- let mut sprite_w: i32 = 0;
833
- let mut sprite_h: i32 = 0;
834
- let mut rect_left: i32 = 0;
835
- let mut rect_top: i32 = 0;
836
- let mut rect_right: i32 = 0;
837
- let mut rect_bottom: i32 = 0;
838
- let frame_data = display_vtable::get_bitmap_sprite_info(
839
- bitmap_obj,
840
- pal,
841
- &mut sprite_w,
842
- &mut sprite_h,
843
- &mut rect_left,
844
- &mut rect_top,
845
- &mut rect_right,
846
- &mut rect_bottom,
847
- );
848
- if frame_data.is_null() {
849
- return;
850
- }
851
-
852
- let camera_x = (*gfx).camera_x;
853
- let camera_y = (*gfx).camera_y;
854
- let half_w = sprite_w / 2;
855
- let half_h = sprite_h / 2;
856
- let blit_h = rect_bottom - rect_top;
857
- let dst_x = (x.0 >> 16) + (camera_x - half_w) + rect_left;
858
- let dst_y = (y.0 >> 16) + (camera_y - half_h) + rect_top;
859
-
860
- if !flags.contains(SpriteFlags::TILED) {
861
- // Non-tiled: clipped blit at (dst_x, dst_y).
862
- display_vtable::blit_bitmap_clipped_native(
863
- gfx, dst_x, dst_y, sprite_w, blit_h, frame_data, 2,
864
- );
865
- } else {
866
- // Tiled: horizontal tile across the clip rect.
867
- display_vtable::blit_bitmap_tiled_native(
868
- gfx, dst_x, sprite_w, dst_y, blit_h, frame_data,
869
- );
870
- }
871
- return;
872
- }
873
-
874
- // ---------------------------------------------------------------
875
- // Bit 24: palette ×4 adjust with orientation-dependent high bits
876
- // ---------------------------------------------------------------
877
- // The original ASM at 0x56B145 scales pal by 4, adds 0x8000, then
878
- // uses the overflow quadrant (bits 16-17 of the scaled value) to
879
- // select an orientation+blend combo. The low 16 bits become the
880
- // new palette value.
881
- let mut orient_local: u32 = 0x0000_0001; // blend=1 (ColorTable/transparency), orientation=0 (Normal)
882
- if flags.contains(SpriteFlags::PALETTE_X4) {
883
- let scaled = pal.wrapping_mul(4).wrapping_add(0x8000);
884
- pal = scaled & 0xFFFF;
885
- let quad = ((scaled as i32) >> 16) & 3;
886
- orient_local = match quad {
887
- 0 => 0x0008_0001, // MirrorXY + ColorTable
888
- 1 => 0x000C_0001, // Rotate90MirrorXY + ColorTable
889
- 2 => 0x0004_0001, // Rotate90 + ColorTable
890
- _ => 0x0000_0001, // Normal + ColorTable
891
- };
892
- }
893
-
894
- // ---------------------------------------------------------------
895
- // Sprite frame lookup via DisplayGfx::GetSpriteFrameForBlit (slot 33).
896
- // ---------------------------------------------------------------
897
- // Resolves the sprite ID + animation value into the actual decompressed
898
- // frame surface plus its bounding box and full sprite-cell dimensions
899
- // (used below for centering and clipped blits).
900
- let mut out_sprite_w: i32 = 0;
901
- let mut out_sprite_h: i32 = 0;
902
- let mut out_rect_left: i32 = 0;
903
- let mut out_rect_top: i32 = 0;
904
- let mut out_rect_right: i32 = 0;
905
- let mut out_rect_bottom: i32 = 0;
906
- let mut out_anim_frac: u32 = 0;
907
-
908
- let mut sprite_surface = DisplayGfx::get_sprite_frame_for_blit_raw(
909
- gfx,
910
- sprite_id,
911
- pal,
912
- &mut out_sprite_w,
913
- &mut out_sprite_h,
914
- &mut out_rect_left,
915
- &mut out_rect_top,
916
- &mut out_rect_right,
917
- &mut out_rect_bottom,
918
- &mut out_anim_frac,
919
- );
920
-
921
- if sprite_surface.is_null() {
922
- return;
923
- }
924
-
925
- let sprite_w = out_sprite_w;
926
- let sprite_h = out_sprite_h;
927
- let rect_left = out_rect_left;
928
- let rect_top = out_rect_top;
929
- let rect_right = out_rect_right;
930
- let rect_bottom = out_rect_bottom;
931
-
932
- // Size checks
933
- if rect_left >= rect_right || rect_top >= rect_bottom {
934
- return;
935
- }
936
-
937
- let mut blit_w = rect_right - rect_left;
938
- let mut blit_h = rect_bottom - rect_top;
939
-
940
- // ---------------------------------------------------------------
941
- // Shadow clear (high_flags bit 22)
942
- // ---------------------------------------------------------------
943
- if flags.contains(SpriteFlags::SHADOW_CLEAR) {
944
- // Blit sprite to layer_2 as shadow base
945
- let layer2 = (*gfx).layer_2;
946
- super::bitgrid::blit_impl(
947
- layer2,
948
- 0,
949
- 0,
950
- blit_w,
951
- blit_h,
952
- sprite_surface,
953
- 0,
954
- 0,
955
- core::ptr::null(),
956
- 0, // mode 0 = copy
957
- );
958
- // Manipulate color_add_table entry for shadow
959
- let color_idx = ((*gfx)._unknown_356c as usize) * 0x100;
960
- let table_byte = &mut (*gfx).color_add_table[color_idx];
961
- let saved = *table_byte;
962
- *table_byte = 0;
963
-
964
- // Call BitGrid__ClearColumn_Maybe (0x4F6590) — clears shadow channel
965
- let clear_fn: unsafe extern "cdecl" fn(*mut u8) =
966
- core::mem::transmute(rb(0x004F6590) as usize);
967
- clear_fn(table_byte as *mut u8);
968
-
969
- *table_byte = saved;
970
-
971
- // Replace sprite surface with layer_2 (shadow-processed)
972
- sprite_surface = layer2;
973
- }
974
-
975
- // ---------------------------------------------------------------
976
- // Extra orientation flags from high_flags
977
- // ---------------------------------------------------------------
978
- if flags.contains(SpriteFlags::MIRROR_X) {
979
- orient_local |= 0x0001_0000;
980
- }
981
- if flags.contains(SpriteFlags::MIRROR_Y) {
982
- orient_local |= 0x0002_0000;
983
- }
984
-
985
- // ---------------------------------------------------------------
986
- // 16-case orientation switch for camera coordinate mapping
987
- // ---------------------------------------------------------------
988
- let camera_x = (*gfx).camera_x;
989
- let camera_y = (*gfx).camera_y;
990
-
991
- // Signed divide toward zero (matches MSVC CDQ+SUB+SAR pattern)
992
- let half_w = if sprite_w < 0 {
993
- (sprite_w + 1) / 2
994
- } else {
995
- sprite_w / 2
996
- };
997
- let half_h = if sprite_h < 0 {
998
- (sprite_h + 1) / 2
999
- } else {
1000
- sprite_h / 2
1001
- };
1002
-
1003
- let x_px = x.0 >> 16;
1004
- let y_px = y.0 >> 16;
1005
-
1006
- let (dst_x, dst_y);
1007
- let orientation_key = (orient_local >> 16) as i32;
1008
-
1009
- match orientation_key {
1010
- 1 | 10 => {
1011
- // MirrorX
1012
- dst_x = camera_x + half_w + x_px - rect_right;
1013
- dst_y = camera_y - half_h + rect_top + y_px;
1014
- }
1015
- 2 | 9 => {
1016
- // MirrorY — X same as Normal, Y mirrored
1017
- dst_x = camera_x - half_w + rect_left + x_px;
1018
- dst_y = camera_y + half_h + y_px - rect_bottom;
1019
- }
1020
- 3 | 8 => {
1021
- // MirrorXY
1022
- dst_x = camera_x + half_w + x_px - rect_right;
1023
- dst_y = camera_y + half_h + y_px - rect_bottom;
1024
- }
1025
- 4 | 15 => {
1026
- // Rotate90 — swap axes
1027
- dst_x = camera_x - half_h + rect_top + x_px;
1028
- dst_y = camera_y + half_w + y_px - rect_right;
1029
- blit_w = rect_bottom - rect_top;
1030
- blit_h = rect_right - rect_left;
1031
- }
1032
- 5 | 14 => {
1033
- // Rotate90MirrorX
1034
- dst_x = camera_x + half_h + x_px - rect_bottom;
1035
- dst_y = camera_y + half_w + y_px - rect_right;
1036
- blit_w = rect_bottom - rect_top;
1037
- blit_h = rect_right - rect_left;
1038
- }
1039
- 6 | 13 => {
1040
- // Rotate90MirrorY
1041
- dst_x = camera_x - half_h + rect_top + x_px;
1042
- dst_y = camera_y - half_w + rect_left + y_px;
1043
- blit_w = rect_bottom - rect_top;
1044
- blit_h = rect_right - rect_left;
1045
- }
1046
- 7 | 12 => {
1047
- // Rotate90MirrorXY
1048
- dst_x = camera_x + half_h + x_px - rect_bottom;
1049
- dst_y = camera_y - half_w + rect_left + y_px;
1050
- blit_w = rect_bottom - rect_top;
1051
- blit_h = rect_right - rect_left;
1052
- }
1053
- _ => {
1054
- // Normal (0, 11, and any other value)
1055
- dst_x = camera_x - half_w + rect_left + x_px;
1056
- dst_y = camera_y - half_h + rect_top + y_px;
1057
- }
1058
- }
1059
-
1060
- // ---------------------------------------------------------------
1061
- // Blit dispatch based on high_flags
1062
- // ---------------------------------------------------------------
1063
-
1064
- if blit_w <= 0 || blit_h <= 0 {
1065
- return;
1066
- }
1067
-
1068
- // Stippled mode (checkerboard per-pixel blit)
1069
- if flags.intersects(SpriteFlags::STIPPLED_0 | SpriteFlags::STIPPLED_1) {
1070
- let stipple_mode: u32 = if flags.contains(SpriteFlags::STIPPLED_1) {
1071
- 1
1072
- } else {
1073
- 0
1074
- };
1075
- let parity = *(rb(openwa_core::address::va::G_STIPPLE_PARITY) as *const u32);
1076
-
1077
- display_vtable::acquire_render_lock(gfx);
1078
-
1079
- super::bitgrid::blit_stippled_raw(
1080
- (*gfx).layer_0,
1081
- sprite_surface,
1082
- dst_x,
1083
- dst_y,
1084
- blit_w,
1085
- blit_h,
1086
- 0,
1087
- 0,
1088
- stipple_mode,
1089
- parity,
1090
- );
1091
- return;
1092
- }
1093
-
1094
- // Tiled mode (horizontal sprite tiling)
1095
- if flags.contains(SpriteFlags::TILED) {
1096
- display_vtable::acquire_render_lock(gfx);
1097
-
1098
- super::bitgrid::blit_tiled_raw(
1099
- (*gfx).layer_0,
1100
- sprite_surface,
1101
- dst_x,
1102
- dst_y,
1103
- blit_w,
1104
- blit_h,
1105
- (*gfx).base.clip_x1,
1106
- (*gfx).base.clip_x2,
1107
- orient_local,
1108
- );
1109
- return;
1110
- }
1111
-
1112
- // Determine color table pointer
1113
- let color_table: *const u8 = if flags.contains(SpriteFlags::ADDITIVE) {
1114
- (*gfx).color_add_table.as_ptr()
1115
- } else if flags.contains(SpriteFlags::COLOR_BLEND) {
1116
- (*gfx).color_blend_table.as_ptr()
1117
- } else {
1118
- core::ptr::null()
1119
- };
1120
-
1121
- display_vtable::acquire_render_lock(gfx);
1122
-
1123
- // src_x=0, src_y=0 always — vtable[33] already set up the sprite surface
1124
- super::bitgrid::blit_impl(
1125
- (*gfx).layer_0,
1126
- dst_x,
1127
- dst_y,
1128
- blit_w,
1129
- blit_h,
1130
- sprite_surface,
1131
- 0,
1132
- 0,
1133
- color_table,
1134
- orient_local,
1135
- );
1136
- }
1137
-
1138
- /// Thiscall wrapper for DisplayGfx::DrawScaledSprite (slot 20).
1139
- ///
1140
- /// Computes coordinates in core, then dispatches the blit via blit_impl.
1141
- unsafe extern "thiscall" fn draw_scaled_sprite(
1142
- this: *mut DisplayGfx,
1143
- x: Fixed,
1144
- y: Fixed,
1145
- sprite: *mut DisplayBitGrid,
1146
- src_x: i32,
1147
- src_y: i32,
1148
- src_w: i32,
1149
- src_h: i32,
1150
- flags: u32,
1151
- ) {
1152
- use openwa_core::render::display::vtable::{self as display_vtable, DrawScaledSpriteResult};
1153
-
1154
- match display_vtable::draw_scaled_sprite(this, x, y, sprite, src_x, src_y, src_w, src_h, flags)
1155
- {
1156
- DrawScaledSpriteResult::Blit {
1157
- layer,
1158
- dst_x,
1159
- dst_y,
1160
- width,
1161
- height,
1162
- sprite,
1163
- src_x,
1164
- src_y,
1165
- color_table,
1166
- blit_flags,
1167
- } => {
1168
- super::bitgrid::blit_impl(
1169
- layer,
1170
- dst_x,
1171
- dst_y,
1172
- width,
1173
- height,
1174
- sprite,
1175
- src_x,
1176
- src_y,
1177
- color_table,
1178
- blit_flags,
1179
- );
1180
- }
1181
- DrawScaledSpriteResult::Stippled {
1182
- layer,
1183
- dst_x,
1184
- dst_y,
1185
- width,
1186
- height,
1187
- sprite,
1188
- src_x,
1189
- src_y,
1190
- stipple_mode,
1191
- } => {
1192
- let parity = *(rb(openwa_core::address::va::G_STIPPLE_PARITY) as *const u32);
1193
- super::bitgrid::blit_stippled_raw(
1194
- layer,
1195
- sprite,
1196
- dst_x,
1197
- dst_y,
1198
- width,
1199
- height,
1200
- src_x,
1201
- src_y,
1202
- stipple_mode,
1203
- parity,
1204
- );
1205
- }
1206
- DrawScaledSpriteResult::Handled => {}
1207
- }
1208
- }
1209
-
1210
- // =========================================================================
1211
- // Font vtable method wrappers
1212
- // =========================================================================
1213
- //
1214
- // Font slot wrappers (slots 7/8/9/10/34/35/36) all live in openwa-core's
1215
- // `display::vtable` module. The wrappers in this file (slots 31/34/35/37
1216
- // for sprite/font loading) are thin forwarders only because they need to
1217
- // capture a bridge function pointer (`wa_load_sprite_from_vfs` etc.) at
1218
- // install time.
1219
-
1220
- // =========================================================================
1221
- // Sprite loading vtable method wrappers
1222
- // =========================================================================
1223
-
1224
- /// Thiscall wrapper for DisplayGfx::LoadSprite (vtable slot 31, 0x523400).
1225
- unsafe extern "thiscall" fn load_sprite(
1226
- this: *mut DisplayGfx,
1227
- layer: u32,
1228
- id: u32,
1229
- flag: u32,
1230
- gfx_dir: *mut GfxDir,
1231
- name: *const c_char,
1232
- ) -> i32 {
1233
- display_vtable::load_sprite(
1234
- this,
1235
- layer,
1236
- id,
1237
- flag,
1238
- gfx_dir,
1239
- name,
1240
- wa_load_sprite_from_vfs,
1241
- )
1242
- }
1243
-
1244
- /// Bridge to LoadSpriteFromVfs (0x4FAAF0).
1245
- /// Usercall: ECX=gfx, EAX=name, stack(sprite, layer_ctx), RET 0x8.
1246
- ///
1247
- /// Verified from caller at 0x523489:
1248
- /// ECX ← layer_contexts[layer] (gfx/VFS context)... wait, re-checked:
1249
- /// ECX ← gfx param, EAX ← name param.
1250
- /// Stack: sprite (EDI from ConstructSprite), layer_ctx.
1251
- #[unsafe(naked)]
1252
- unsafe extern "cdecl" fn wa_load_sprite_from_vfs(
1253
- _sprite: *mut openwa_core::render::sprite::Sprite,
1254
- _gfx_dir: *mut GfxDir,
1255
- _name: *const c_char,
1256
- _layer_ctx: *mut openwa_core::render::palette::PaletteContext,
1257
- ) -> i32 {
1258
- core::arch::naked_asm!(
1259
- // cdecl: +4=sprite, +8=gfx, +12=name, +16=layer_ctx
1260
- "mov ecx, [esp+8]", // gfx → ECX
1261
- "mov eax, [esp+12]", // name → EAX
1262
- "push dword ptr [esp+16]", // layer_ctx
1263
- "push dword ptr [esp+8]", // sprite (shifted +4 by push)
1264
- "call [{ADDR}]", // RET 0x8 cleans 2 stack params
1265
- "ret",
1266
- ADDR = sym LOAD_SPRITE_FROM_VFS_ADDR,
1267
- );
1268
- }
1269
-
1270
- static mut LOAD_SPRITE_FROM_VFS_ADDR: u32 = 0;
1271
-
1272
- // GetSpriteFrameForBlit (slot 33) is NOT ported — see the docstring on
1273
- // `DisplayVtable::get_sprite_frame_for_blit` in openwa-core for the full
1274
- // rationale. Our `blit_sprite` (slot 19) above calls it via the bound
1275
- // vtable wrapper on every sprite render.
1276
-
1277
- unsafe extern "thiscall" fn load_sprite_by_layer(
1278
- this: *mut DisplayGfx,
1279
- layer: u32,
1280
- id: u32,
1281
- gfx_dir: *mut GfxDir,
1282
- name: *const c_char,
1283
- ) -> i32 {
1284
- display_vtable::load_sprite_by_layer(this, layer, id, gfx_dir, name)
1285
- }
1286
-
1287
- unsafe extern "thiscall" fn load_font(
1288
- this: *mut DisplayGfx,
1289
- mode: u32,
1290
- font_id: i32,
1291
- gfx_dir: *mut GfxDir,
1292
- filename: *const c_char,
1293
- ) -> u32 {
1294
- display_vtable::load_font(this, mode, font_id, gfx_dir, filename)
1295
- }
1296
-
1297
- unsafe extern "thiscall" fn load_font_extension(
1298
- this: *mut DisplayGfx,
1299
- font_id: i32,
1300
- path: *const c_char,
1301
- char_map: *const c_char,
1302
- palette_value: u32,
1303
- flag: i32,
1304
- ) -> u32 {
1305
- display_vtable::load_font_extension(this, font_id, path, char_map, palette_value, flag)
1306
- }
1307
-
1308
- // ---------------------------------------------------------------------------
1309
- // Display installation
1310
- // ---------------------------------------------------------------------------
1311
-
1312
- fn install_display() -> Result<(), String> {
1313
- let _ = log_line("[Display] Patching DisplayBase vtables");
1314
-
1315
- unsafe {
1316
- let purecall_addr = rb(PURECALL);
1317
- let noop_addr = noop_thiscall as *const () as u32;
1318
-
1319
- // Patch primary vtable (0x6645F8): replace _purecall with no-ops.
1320
- let primary = rb(va::DISPLAY_BASE_VTABLE) as *mut u32;
1321
- patch_vtable(primary, VTABLE_SLOTS, |vt| {
1322
- let mut patched = 0u32;
1323
- for i in 0..VTABLE_SLOTS {
1324
- let slot = vt.add(i);
1325
- if *slot == purecall_addr {
1326
- *slot = noop_addr;
1327
- patched += 1;
1328
- }
1329
- }
1330
- let _ = log_line(&format!(
1331
- "[Display] Primary: patched {patched}/{VTABLE_SLOTS} _purecall -> no-op"
1332
- ));
1333
- })?;
1334
-
1335
- // Patch headless vtable (0x66A0F8): replace destructor (slot 0)
1336
- // with our Rust version that frees the Rust-allocated sprite cache.
1337
- let headless = rb(va::DISPLAY_BASE_HEADLESS_VTABLE) as *mut u32;
1338
- patch_vtable(headless, VTABLE_SLOTS, |vt| {
1339
- *vt = headless_destructor as *const () as u32;
1340
- let _ = log_line("[Display] Headless: patched slot 0 (destructor) -> Rust");
1341
- })?;
1342
-
1343
- // Initialize bridge address statics for sprite loading
1344
- LOAD_SPRITE_FROM_VFS_ADDR = rb(va::LOAD_SPRITE_FROM_VFS);
1345
-
1346
- // Patch DisplayGfx vtable (0x66A218): replace ported methods with Rust.
1347
- vtable_replace!(DisplayGfxVtable, va::DISPLAY_GFX_VTABLE, {
1348
- destructor => display_destructor::display_gfx_destructor,
1349
- get_dimensions => display_vtable::get_dimensions,
1350
- set_layer_color => display_vtable::set_layer_color,
1351
- set_active_layer => display_vtable::set_active_layer,
1352
- get_sprite_info => display_vtable::get_sprite_info,
1353
- draw_text_on_bitmap => display_vtable::draw_text_on_bitmap,
1354
- draw_tiled_bitmap => display_vtable::draw_tiled_bitmap,
1355
- get_font_info => display_vtable::get_font_info,
1356
- get_font_metric => display_vtable::get_font_metric,
1357
- set_font_param => display_vtable::set_font_param,
1358
- draw_polyline => display_vtable::draw_polyline,
1359
- draw_line => display_vtable::draw_line,
1360
- draw_line_clipped => display_vtable::draw_line_clipped,
1361
- draw_pixel_strip => display_vtable::draw_pixel_strip,
1362
- draw_crosshair => display_vtable::draw_crosshair,
1363
- draw_outlined_pixel => display_vtable::draw_outlined_pixel,
1364
- fill_rect => display_vtable::fill_rect,
1365
- draw_via_callback => display_vtable::draw_via_callback,
1366
- draw_tiled_terrain => display_vtable::draw_tiled_terrain,
1367
- flush_render => display_vtable::flush_render,
1368
- set_camera_offset => display_vtable::set_camera_offset,
1369
- set_clip_rect => display_vtable::set_clip_rect,
1370
- is_sprite_loaded => display_vtable::is_sprite_loaded,
1371
- load_sprite => load_sprite,
1372
- draw_scaled_sprite => draw_scaled_sprite,
1373
- set_layer_visibility => display_vtable::set_layer_visibility,
1374
- update_palette => display_vtable::update_palette,
1375
- set_font_palette => display_vtable::set_font_palette,
1376
- slot 19 => blit_sprite,
1377
- get_sprite_frame_for_blit => display_vtable::get_sprite_frame_for_blit,
1378
- load_sprite_by_layer => load_sprite_by_layer,
1379
- load_font => load_font,
1380
- load_font_extension => load_font_extension,
1381
- })?;
1382
- let _ = log_line("[Display] DisplayGfx: patched 32 methods -> Rust");
1383
-
1384
- // DisplayGfx::LoadSpriteEx (vtable slot 30) has zero callers in both
1385
- // WA.exe (no instructions reach vtable[+0x78] on a DisplayGfx) and our
1386
- // own Rust code (no `bind_DisplayVtable!` invocation, no direct call).
1387
- // Trap it so we'll be alerted the moment any future caller appears.
1388
- hook::install_trap!("DisplayGfx__LoadSpriteEx", va::DISPLAY_GFX_LOAD_SPRITE_EX);
1389
-
1390
- // ── Slot 33 leaf functions: trap the WA-side originals ────────
1391
- //
1392
- // After `vtable_replace!` swapped slot 33 to our Rust impl, the
1393
- // original `DisplayGfx::GetSpriteFrameForBlit` (0x5237C0) is no
1394
- // longer reached, which means its leaves have no remaining
1395
- // callers in shipping WA either:
1396
- //
1397
- // - `Sprite__GetFrameForBlit` (0x4FAD30): only caller was the
1398
- // original slot 33 dispatcher.
1399
- // - `SpriteBank__GetFrameForBlit` (0x4F9710): same. Also bank
1400
- // objects are themselves unreachable (LoadSpriteEx is trapped).
1401
- // - `FrameCache__Allocate` (0x4FA950): only callers were the
1402
- // two GetFrameForBlit helpers above.
1403
- // - `DisplayBitGrid::SetExternalBuffer` (0x4F6470): only caller
1404
- // was `SpriteBank__GetFrameForBlit`.
1405
- //
1406
- // `Sprite_LZSS_Decode` (0x5B29E0) is NOT trapped — it has
1407
- // independent callers (`IMG_Decode`, `FUN_0048EE32`) reachable
1408
- // from `DDGame__Constructor`/`SoundEmitter`/`LoadHudAndWeaponSprites`,
1409
- // all still bridged. Trapping it would break those.
1410
- hook::install_trap!("Sprite__GetFrameForBlit", va::SPRITE_GET_FRAME_FOR_BLIT);
1411
- hook::install_trap!(
1412
- "SpriteBank__GetFrameForBlit",
1413
- va::SPRITE_BANK_GET_FRAME_FOR_BLIT
1414
- );
1415
- hook::install_trap!("FrameCache__Allocate", va::FRAME_CACHE_ALLOCATE);
1416
- hook::install_trap!(
1417
- "DisplayBitGrid__SetExternalBuffer",
1418
- va::DISPLAY_BIT_GRID_SET_EXTERNAL_BUFFER
1419
- );
1420
-
1421
- // ── Slot 0 destructor leaves: trap the WA-side originals ──────
1422
- //
1423
- // After `vtable_replace!` swapped slot 0 to our Rust impl, the
1424
- // original `DisplayGfx::DestructorImpl` (0x56A010) is no longer
1425
- // reached, so its only-callee helpers lose their last live
1426
- // caller too.
1427
- //
1428
- // `TileBitmapSet::Destructor` (0x569BC0) has a *second* xref to
1429
- // `FUN_0056C2E0` (a tile-bitmap-set realloc helper called only
1430
- // from `CBitmap__Constructor_Maybe` at 0x56C060). That second
1431
- // chain is **structurally dead** in shipping WA: 0x56C060 sits
1432
- // in the DisplayGfx vtable .rdata block at slot index 38 (offset
1433
- // 0x98 past the vtable base 0x66A218), but no instruction in
1434
- // WA.exe loads from offset 0x98 of any pointer — verified by
1435
- // byte-pattern search for `FF 90 98 00 00 00` (CALL [reg+0x98])
1436
- // and `8B 4? 98` (MOV reg,[reg+0x98]) returning no DisplayGfx
1437
- // hits. The compiler emitted slots 38/39 of DisplayGfx into
1438
- // .rdata but the C++ source has no live caller for them — they
1439
- // are vestigial vtable entries from a removed feature. So
1440
- // trapping `TileBitmapSet::Destructor` is safe.
1441
- hook::install_trap!(
1442
- "DisplayGfx__DestructorImpl",
1443
- va::DISPLAY_GFX_DESTRUCTOR_IMPL
1444
- );
1445
- hook::install_trap!(
1446
- "DisplayGfx__FreeLayerSpriteTable",
1447
- va::DISPLAY_GFX_FREE_LAYER_SPRITE_TABLE
1448
- );
1449
- hook::install_trap!("TileBitmapSet__Destructor", va::TILE_BITMAP_SET_DESTRUCTOR);
1450
- }
1451
-
1452
- Ok(())
1453
- }
1454
-
1455
- // ==========================================================================
1456
- // Combined installation
1457
- // ==========================================================================
1458
-
1459
- pub fn install() -> Result<(), String> {
1460
- install_render_queue()?;
1461
- install_display()?;
1462
- Ok(())
1463
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
crates/openwa-dll/src/replacements/render/display_vtable.rs ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! DisplayGfx vtable patches — DisplayBase pure-call stubs, headless destructor,
2
+ //! and ported DisplayGfx methods.
3
+ //!
4
+ //! Patches DisplayBase vtables in WA.exe's .rdata:
5
+ //! - Primary vtable (0x6645F8): replaces _purecall slots with safe no-op stubs
6
+ //! - Headless vtable (0x66A0F8): replaces destructor with Rust version that
7
+ //! correctly frees our Rust-allocated sprite cache sub-objects
8
+ //! - DisplayGfx vtable (0x66A218): replaces ported methods with Rust
9
+
10
+ use core::ffi::c_char;
11
+
12
+ use openwa_core::address::va;
13
+ use openwa_core::bitgrid::DisplayBitGrid;
14
+ use openwa_core::fixed::Fixed;
15
+ use openwa_core::rebase::rb;
16
+ use openwa_core::render::display::destructor as display_destructor;
17
+ use openwa_core::render::display::vtable::{self as display_vtable_impl, DisplayGfxVtable};
18
+ use openwa_core::render::display::DisplayBase;
19
+ use openwa_core::render::display::DisplayGfx;
20
+ use openwa_core::render::sprite::gfx_dir::GfxDir;
21
+ use openwa_core::vtable::patch_vtable;
22
+ use openwa_core::vtable_replace;
23
+ use openwa_core::wa_alloc::wa_free;
24
+
25
+ use crate::hook;
26
+ use crate::log_line;
27
+
28
+ /// The _purecall function address (calls abort).
29
+ const PURECALL: u32 = 0x005D_4E16;
30
+
31
+ /// Number of slots in the DisplayBase vtable.
32
+ const VTABLE_SLOTS: usize = 32;
33
+
34
+ unsafe extern "thiscall" fn noop_thiscall(_this: *mut u8) {}
35
+
36
+ /// Rust destructor for headless DisplayBase. Frees the sprite cache chain
37
+ /// (SpriteCache -> FrameCache -> buffer) that was allocated by new_headless().
38
+ unsafe extern "thiscall" fn headless_destructor(
39
+ this: *mut DisplayBase,
40
+ flags: u8,
41
+ ) -> *mut DisplayBase {
42
+ let sprite_cache = (*this).sprite_cache;
43
+ if !sprite_cache.is_null() {
44
+ let frame_cache = (*sprite_cache).frame_cache;
45
+ if !frame_cache.is_null() {
46
+ let buf = (*frame_cache).buffer;
47
+ if !buf.is_null() {
48
+ wa_free(buf);
49
+ }
50
+ wa_free(frame_cache);
51
+ }
52
+ wa_free(sprite_cache);
53
+ }
54
+ if flags & 1 != 0 {
55
+ wa_free(this);
56
+ }
57
+ this
58
+ }
59
+
60
+ // No saved originals needed — all paths are fully ported or use direct bridges.
61
+
62
+ /// Thiscall stub for DisplayGfx::BlitSprite (slot 19, 0x56B080).
63
+ /// Implementation in `openwa_core::render::display::blit_sprite`.
64
+ unsafe extern "thiscall" fn blit_sprite(
65
+ this: *mut DisplayGfx,
66
+ x: Fixed,
67
+ y: Fixed,
68
+ sprite_flags: u32,
69
+ palette: u32,
70
+ ) {
71
+ openwa_core::render::display::blit_sprite::blit_sprite(this, x, y, sprite_flags, palette);
72
+ }
73
+
74
+ /// Thiscall stub for DisplayGfx::DrawScaledSprite (slot 20).
75
+ /// Implementation in `openwa_core::render::display::blit_sprite`.
76
+ unsafe extern "thiscall" fn draw_scaled_sprite(
77
+ this: *mut DisplayGfx,
78
+ x: Fixed,
79
+ y: Fixed,
80
+ sprite: *mut DisplayBitGrid,
81
+ src_x: i32,
82
+ src_y: i32,
83
+ src_w: i32,
84
+ src_h: i32,
85
+ flags: u32,
86
+ ) {
87
+ openwa_core::render::display::blit_sprite::draw_scaled_sprite(
88
+ this, x, y, sprite, src_x, src_y, src_w, src_h, flags,
89
+ );
90
+ }
91
+
92
+ // =========================================================================
93
+ // Font vtable method wrappers
94
+ // =========================================================================
95
+ //
96
+ // Font slot wrappers (slots 7/8/9/10/34/35/36) all live in openwa-core's
97
+ // `display::vtable` module. The wrappers in this file (slots 31/34/35/37
98
+ // for sprite/font loading) are thin forwarders only because they need to
99
+ // capture a bridge function pointer (`wa_load_sprite_from_vfs` etc.) at
100
+ // install time.
101
+
102
+ // =========================================================================
103
+ // Sprite loading vtable method wrappers
104
+ // =========================================================================
105
+
106
+ /// Thiscall wrapper for DisplayGfx::LoadSprite (vtable slot 31, 0x523400).
107
+ unsafe extern "thiscall" fn load_sprite(
108
+ this: *mut DisplayGfx,
109
+ layer: u32,
110
+ id: u32,
111
+ flag: u32,
112
+ gfx_dir: *mut GfxDir,
113
+ name: *const c_char,
114
+ ) -> i32 {
115
+ display_vtable_impl::load_sprite(
116
+ this,
117
+ layer,
118
+ id,
119
+ flag,
120
+ gfx_dir,
121
+ name,
122
+ wa_load_sprite_from_vfs,
123
+ )
124
+ }
125
+
126
+ /// Bridge to LoadSpriteFromVfs (0x4FAAF0).
127
+ /// Usercall: ECX=gfx, EAX=name, stack(sprite, layer_ctx), RET 0x8.
128
+ ///
129
+ /// Verified from caller at 0x523489:
130
+ /// ECX ← layer_contexts[layer] (gfx/VFS context)... wait, re-checked:
131
+ /// ECX ← gfx param, EAX ← name param.
132
+ /// Stack: sprite (EDI from ConstructSprite), layer_ctx.
133
+ #[unsafe(naked)]
134
+ unsafe extern "cdecl" fn wa_load_sprite_from_vfs(
135
+ _sprite: *mut openwa_core::render::sprite::Sprite,
136
+ _gfx_dir: *mut GfxDir,
137
+ _name: *const c_char,
138
+ _layer_ctx: *mut openwa_core::render::palette::PaletteContext,
139
+ ) -> i32 {
140
+ core::arch::naked_asm!(
141
+ // cdecl: +4=sprite, +8=gfx, +12=name, +16=layer_ctx
142
+ "mov ecx, [esp+8]", // gfx → ECX
143
+ "mov eax, [esp+12]", // name → EAX
144
+ "push dword ptr [esp+16]", // layer_ctx
145
+ "push dword ptr [esp+8]", // sprite (shifted +4 by push)
146
+ "call [{ADDR}]", // RET 0x8 cleans 2 stack params
147
+ "ret",
148
+ ADDR = sym LOAD_SPRITE_FROM_VFS_ADDR,
149
+ );
150
+ }
151
+
152
+ static mut LOAD_SPRITE_FROM_VFS_ADDR: u32 = 0;
153
+
154
+ // GetSpriteFrameForBlit (slot 33) is NOT ported — see the docstring on
155
+ // `DisplayVtable::get_sprite_frame_for_blit` in openwa-core for the full
156
+ // rationale. Our `blit_sprite` (slot 19) above calls it via the bound
157
+ // vtable wrapper on every sprite render.
158
+
159
+ unsafe extern "thiscall" fn load_sprite_by_layer(
160
+ this: *mut DisplayGfx,
161
+ layer: u32,
162
+ id: u32,
163
+ gfx_dir: *mut GfxDir,
164
+ name: *const c_char,
165
+ ) -> i32 {
166
+ display_vtable_impl::load_sprite_by_layer(this, layer, id, gfx_dir, name)
167
+ }
168
+
169
+ unsafe extern "thiscall" fn load_font(
170
+ this: *mut DisplayGfx,
171
+ mode: u32,
172
+ font_id: i32,
173
+ gfx_dir: *mut GfxDir,
174
+ filename: *const c_char,
175
+ ) -> u32 {
176
+ display_vtable_impl::load_font(this, mode, font_id, gfx_dir, filename)
177
+ }
178
+
179
+ unsafe extern "thiscall" fn load_font_extension(
180
+ this: *mut DisplayGfx,
181
+ font_id: i32,
182
+ path: *const c_char,
183
+ char_map: *const c_char,
184
+ palette_value: u32,
185
+ flag: i32,
186
+ ) -> u32 {
187
+ display_vtable_impl::load_font_extension(this, font_id, path, char_map, palette_value, flag)
188
+ }
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // Installation
192
+ // ---------------------------------------------------------------------------
193
+
194
+ pub fn install() -> Result<(), String> {
195
+ let _ = log_line("[Display] Patching DisplayBase vtables");
196
+
197
+ unsafe {
198
+ let purecall_addr = rb(PURECALL);
199
+ let noop_addr = noop_thiscall as *const () as u32;
200
+
201
+ // Patch primary vtable (0x6645F8): replace _purecall with no-ops.
202
+ let primary = rb(va::DISPLAY_BASE_VTABLE) as *mut u32;
203
+ patch_vtable(primary, VTABLE_SLOTS, |vt| {
204
+ let mut patched = 0u32;
205
+ for i in 0..VTABLE_SLOTS {
206
+ let slot = vt.add(i);
207
+ if *slot == purecall_addr {
208
+ *slot = noop_addr;
209
+ patched += 1;
210
+ }
211
+ }
212
+ let _ = log_line(&format!(
213
+ "[Display] Primary: patched {patched}/{VTABLE_SLOTS} _purecall -> no-op"
214
+ ));
215
+ })?;
216
+
217
+ // Patch headless vtable (0x66A0F8): replace destructor (slot 0)
218
+ // with our Rust version that frees the Rust-allocated sprite cache.
219
+ let headless = rb(va::DISPLAY_BASE_HEADLESS_VTABLE) as *mut u32;
220
+ patch_vtable(headless, VTABLE_SLOTS, |vt| {
221
+ *vt = headless_destructor as *const () as u32;
222
+ let _ = log_line("[Display] Headless: patched slot 0 (destructor) -> Rust");
223
+ })?;
224
+
225
+ // Initialize bridge address statics for sprite loading
226
+ LOAD_SPRITE_FROM_VFS_ADDR = rb(va::LOAD_SPRITE_FROM_VFS);
227
+
228
+ // Patch DisplayGfx vtable (0x66A218): replace ported methods with Rust.
229
+ vtable_replace!(DisplayGfxVtable, va::DISPLAY_GFX_VTABLE, {
230
+ destructor => display_destructor::display_gfx_destructor,
231
+ get_dimensions => display_vtable_impl::get_dimensions,
232
+ set_layer_color => display_vtable_impl::set_layer_color,
233
+ set_active_layer => display_vtable_impl::set_active_layer,
234
+ get_sprite_info => display_vtable_impl::get_sprite_info,
235
+ draw_text_on_bitmap => display_vtable_impl::draw_text_on_bitmap,
236
+ draw_tiled_bitmap => display_vtable_impl::draw_tiled_bitmap,
237
+ get_font_info => display_vtable_impl::get_font_info,
238
+ get_font_metric => display_vtable_impl::get_font_metric,
239
+ set_font_param => display_vtable_impl::set_font_param,
240
+ draw_polyline => display_vtable_impl::draw_polyline,
241
+ draw_line => display_vtable_impl::draw_line,
242
+ draw_line_clipped => display_vtable_impl::draw_line_clipped,
243
+ draw_pixel_strip => display_vtable_impl::draw_pixel_strip,
244
+ draw_crosshair => display_vtable_impl::draw_crosshair,
245
+ draw_outlined_pixel => display_vtable_impl::draw_outlined_pixel,
246
+ fill_rect => display_vtable_impl::fill_rect,
247
+ draw_via_callback => display_vtable_impl::draw_via_callback,
248
+ draw_tiled_terrain => display_vtable_impl::draw_tiled_terrain,
249
+ flush_render => display_vtable_impl::flush_render,
250
+ set_camera_offset => display_vtable_impl::set_camera_offset,
251
+ set_clip_rect => display_vtable_impl::set_clip_rect,
252
+ is_sprite_loaded => display_vtable_impl::is_sprite_loaded,
253
+ load_sprite => load_sprite,
254
+ draw_scaled_sprite => draw_scaled_sprite,
255
+ set_layer_visibility => display_vtable_impl::set_layer_visibility,
256
+ update_palette => display_vtable_impl::update_palette,
257
+ set_font_palette => display_vtable_impl::set_font_palette,
258
+ slot 19 => blit_sprite,
259
+ get_sprite_frame_for_blit => display_vtable_impl::get_sprite_frame_for_blit,
260
+ load_sprite_by_layer => load_sprite_by_layer,
261
+ load_font => load_font,
262
+ load_font_extension => load_font_extension,
263
+ })?;
264
+ let _ = log_line("[Display] DisplayGfx: patched 32 methods -> Rust");
265
+
266
+ // DisplayGfx::LoadSpriteEx (vtable slot 30) has zero callers in both
267
+ // WA.exe (no instructions reach vtable[+0x78] on a DisplayGfx) and our
268
+ // own Rust code (no `bind_DisplayVtable!` invocation, no direct call).
269
+ // Trap it so we'll be alerted the moment any future caller appears.
270
+ hook::install_trap!("DisplayGfx__LoadSpriteEx", va::DISPLAY_GFX_LOAD_SPRITE_EX);
271
+
272
+ // ── Slot 33 leaf functions: trap the WA-side originals ────────
273
+ //
274
+ // After `vtable_replace!` swapped slot 33 to our Rust impl, the
275
+ // original `DisplayGfx::GetSpriteFrameForBlit` (0x5237C0) is no
276
+ // longer reached, which means its leaves have no remaining
277
+ // callers in shipping WA either:
278
+ //
279
+ // - `Sprite__GetFrameForBlit` (0x4FAD30): only caller was the
280
+ // original slot 33 dispatcher.
281
+ // - `SpriteBank__GetFrameForBlit` (0x4F9710): same. Also bank
282
+ // objects are themselves unreachable (LoadSpriteEx is trapped).
283
+ // - `FrameCache__Allocate` (0x4FA950): only callers were the
284
+ // two GetFrameForBlit helpers above.
285
+ // - `DisplayBitGrid::SetExternalBuffer` (0x4F6470): only caller
286
+ // was `SpriteBank__GetFrameForBlit`.
287
+ //
288
+ // `Sprite_LZSS_Decode` (0x5B29E0) is NOT trapped — it has
289
+ // independent callers (`IMG_Decode`, `FUN_0048EE32`) reachable
290
+ // from `DDGame__Constructor`/`SoundEmitter`/`LoadHudAndWeaponSprites`,
291
+ // all still bridged. Trapping it would break those.
292
+ hook::install_trap!("Sprite__GetFrameForBlit", va::SPRITE_GET_FRAME_FOR_BLIT);
293
+ hook::install_trap!(
294
+ "SpriteBank__GetFrameForBlit",
295
+ va::SPRITE_BANK_GET_FRAME_FOR_BLIT
296
+ );
297
+ hook::install_trap!("FrameCache__Allocate", va::FRAME_CACHE_ALLOCATE);
298
+ hook::install_trap!(
299
+ "DisplayBitGrid__SetExternalBuffer",
300
+ va::DISPLAY_BIT_GRID_SET_EXTERNAL_BUFFER
301
+ );
302
+
303
+ // ── Slot 0 destructor leaves: trap the WA-side originals ──────
304
+ //
305
+ // After `vtable_replace!` swapped slot 0 to our Rust impl, the
306
+ // original `DisplayGfx::DestructorImpl` (0x56A010) is no longer
307
+ // reached, so its only-callee helpers lose their last live
308
+ // caller too.
309
+ //
310
+ // `TileBitmapSet::Destructor` (0x569BC0) has a *second* xref to
311
+ // `FUN_0056C2E0` (a tile-bitmap-set realloc helper called only
312
+ // from `CBitmap__Constructor_Maybe` at 0x56C060). That second
313
+ // chain is **structurally dead** in shipping WA: 0x56C060 sits
314
+ // in the DisplayGfx vtable .rdata block at slot index 38 (offset
315
+ // 0x98 past the vtable base 0x66A218), but no instruction in
316
+ // WA.exe loads from offset 0x98 of any pointer — verified by
317
+ // byte-pattern search for `FF 90 98 00 00 00` (CALL [reg+0x98])
318
+ // and `8B 4? 98` (MOV reg,[reg+0x98]) returning no DisplayGfx
319
+ // hits. The compiler emitted slots 38/39 of DisplayGfx into
320
+ // .rdata but the C++ source has no live caller for them — they
321
+ // are vestigial vtable entries from a removed feature. So
322
+ // trapping `TileBitmapSet::Destructor` is safe.
323
+ hook::install_trap!(
324
+ "DisplayGfx__DestructorImpl",
325
+ va::DISPLAY_GFX_DESTRUCTOR_IMPL
326
+ );
327
+ hook::install_trap!(
328
+ "DisplayGfx__FreeLayerSpriteTable",
329
+ va::DISPLAY_GFX_FREE_LAYER_SPRITE_TABLE
330
+ );
331
+ hook::install_trap!("TileBitmapSet__Destructor", va::TILE_BITMAP_SET_DESTRUCTOR);
332
+ }
333
+
334
+ Ok(())
335
+ }
crates/openwa-dll/src/replacements/render/mod.rs ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Render subsystem hooks — RenderQueue enqueue + DisplayGfx vtable patches.
2
+ //!
3
+ //! Split into two submodules:
4
+ //! - `render_queue`: RenderQueue enqueue hooks and dispatcher
5
+ //! - `display_vtable`: DisplayGfx vtable patches and stubs
6
+
7
+ mod display_vtable;
8
+ mod render_queue;
9
+
10
+ pub fn install() -> Result<(), String> {
11
+ render_queue::install()?;
12
+ display_vtable::install()?;
13
+ Ok(())
14
+ }
crates/openwa-dll/src/replacements/render/render_queue.rs ADDED
@@ -0,0 +1,478 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! RenderQueue enqueue hooks — full Rust replacements for command enqueueing,
2
+ //! plus the per-frame RenderDrawingQueue dispatcher bridge.
3
+
4
+ use openwa_core::address::va;
5
+ use openwa_core::fixed::Fixed;
6
+ use openwa_core::render::display::DisplayGfx;
7
+ use openwa_core::render::queue::*;
8
+ use openwa_core::render::queue_dispatch::{render_drawing_queue, ClipContext};
9
+ use openwa_core::render::SpriteOp;
10
+
11
+ use crate::hook::{self, usercall_trampoline};
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // EnqueueTiledBitmap (0x541D60) — type 0xD, EAX=this, 3 stack, RET 0xC
15
+ //
16
+ // Mis-labelled `RQ_DrawPixel` in earlier reverse-engineering passes. It does
17
+ // NOT enqueue a single-pixel draw — it enqueues a tile-cached bitmap draw
18
+ // dispatched by `RenderDrawingQueue` case 0xD into vtable slot 11.
19
+ // ---------------------------------------------------------------------------
20
+
21
+ usercall_trampoline!(fn trampoline_enqueue_tiled_bitmap; impl_fn = enqueue_tiled_bitmap_impl;
22
+ reg = eax; stack_params = 3; ret_bytes = "0xC");
23
+
24
+ unsafe extern "cdecl" fn enqueue_tiled_bitmap_impl(
25
+ this: u32,
26
+ y_fixed16: u32,
27
+ source_descriptor: u32,
28
+ flags: u32,
29
+ ) {
30
+ let q = &mut *(this as *mut RenderQueue);
31
+
32
+ if let Some(entry) = q.alloc::<DrawTiledBitmapCmd>() {
33
+ *entry = DrawTiledBitmapCmd {
34
+ command_type: command_type::DRAW_TILED_BITMAP,
35
+ layer: 0x1B_0000,
36
+ x: Fixed(0xFF00_0000u32 as i32),
37
+ y: Fixed(y_fixed16 as i32),
38
+ source: source_descriptor as *const _,
39
+ flags: flags as u8,
40
+ _pad: [0; 3],
41
+ };
42
+ }
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // DrawLineStrip (0x541DD0) — type 8, EAX=this, EDI=count, 2 stack, RET 0x8
47
+ // Allocation: count * 0xC + 0x1C (variable size)
48
+ // ---------------------------------------------------------------------------
49
+
50
+ usercall_trampoline!(fn trampoline_draw_line_strip; impl_fn = draw_line_strip_impl;
51
+ regs = [eax, edi]; stack_params = 2; ret_bytes = "0x8");
52
+
53
+ unsafe extern "cdecl" fn draw_line_strip_impl(
54
+ this: u32,
55
+ count: u32,
56
+ vertex_ptr: u32,
57
+ param_1: u32,
58
+ ) {
59
+ let q = &mut *(this as *mut RenderQueue);
60
+ let total_size = count as usize * 0xC + 0x1C;
61
+
62
+ if let Some(ptr) = q.alloc_raw(total_size) {
63
+ let header = &mut *(ptr as *mut DrawLineStripHeader);
64
+ *header = DrawLineStripHeader {
65
+ command_type: command_type::DRAW_LINE_STRIP,
66
+ layer: 0xE_0000,
67
+ count,
68
+ color: param_1,
69
+ };
70
+ core::ptr::copy_nonoverlapping(
71
+ vertex_ptr as *const u8,
72
+ ptr.add(core::mem::size_of::<DrawLineStripHeader>()),
73
+ count as usize * 0xC,
74
+ );
75
+ }
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // DrawPolygon (0x541E50) — type 9, ECX=this, ESI=count, 3 stack, RET 0xC
80
+ // Allocation: count * 0xC + 0x20 (variable size)
81
+ // ---------------------------------------------------------------------------
82
+
83
+ usercall_trampoline!(fn trampoline_draw_polygon; impl_fn = draw_polygon_impl;
84
+ regs = [ecx, esi]; stack_params = 3; ret_bytes = "0xC");
85
+
86
+ unsafe extern "cdecl" fn draw_polygon_impl(
87
+ this: u32,
88
+ count: u32,
89
+ vertex_ptr: u32,
90
+ param_1: u32,
91
+ param_2: u32,
92
+ ) {
93
+ let q = &mut *(this as *mut RenderQueue);
94
+ let total_size = count as usize * 0xC + 0x20;
95
+
96
+ if let Some(ptr) = q.alloc_raw(total_size) {
97
+ let header = &mut *(ptr as *mut DrawPolygonHeader);
98
+ *header = DrawPolygonHeader {
99
+ command_type: command_type::DRAW_POLYGON,
100
+ layer: 0xE_0000,
101
+ count,
102
+ color1: param_1,
103
+ color2: param_2,
104
+ };
105
+ core::ptr::copy_nonoverlapping(
106
+ vertex_ptr as *const u8,
107
+ ptr.add(core::mem::size_of::<DrawPolygonHeader>()),
108
+ count as usize * 0xC,
109
+ );
110
+ }
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // DrawCrosshair (0x541ED0) — type 0xB, ECX=this, 5 stack, RET 0x14
115
+ // Enqueues a crosshair draw command. Dispatched by RenderDrawingQueue
116
+ // case 0xB → DisplayGfx::draw_crosshair (vtable slot 16).
117
+ // ---------------------------------------------------------------------------
118
+
119
+ usercall_trampoline!(fn trampoline_draw_crosshair; impl_fn = draw_crosshair_impl;
120
+ reg = ecx; stack_params = 5; ret_bytes = "0x14");
121
+
122
+ unsafe extern "cdecl" fn draw_crosshair_impl(
123
+ this: u32,
124
+ layer: u32,
125
+ x_pos: u32,
126
+ y_pos: u32,
127
+ color_fg: u32,
128
+ color_bg: u32,
129
+ ) {
130
+ let q = &mut *(this as *mut RenderQueue);
131
+
132
+ if let Some(entry) = q.alloc::<DrawCrosshairCmd>() {
133
+ *entry = DrawCrosshairCmd {
134
+ command_type: command_type::DRAW_CROSSHAIR,
135
+ layer,
136
+ color_fg,
137
+ color_bg,
138
+ x: Fixed(x_pos as i32),
139
+ y: Fixed(y_pos as i32),
140
+ ref_z: 0,
141
+ };
142
+ }
143
+ }
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // DrawRect (0x541F40) — type 0, ECX=this, EDX=y_clip, 6 stack, RET 0x18
147
+ // ---------------------------------------------------------------------------
148
+
149
+ usercall_trampoline!(fn trampoline_draw_rect; impl_fn = draw_rect_impl;
150
+ regs = [ecx, edx]; stack_params = 6; ret_bytes = "0x18");
151
+
152
+ unsafe extern "cdecl" fn draw_rect_impl(
153
+ this: u32,
154
+ y_clip: u32,
155
+ layer: u32,
156
+ x1: u32,
157
+ y1: u32,
158
+ x2: u32,
159
+ y2: u32,
160
+ color: u32,
161
+ ) {
162
+ let q = &mut *(this as *mut RenderQueue);
163
+
164
+ if let Some(entry) = q.alloc::<DrawRectCmd>() {
165
+ *entry = DrawRectCmd {
166
+ command_type: command_type::DRAW_RECT,
167
+ layer,
168
+ color,
169
+ x1: Fixed(x1 as i32).floor(),
170
+ y1: Fixed(y1 as i32).floor(),
171
+ x2: Fixed(x2 as i32).floor(),
172
+ y2: Fixed(y2 as i32).floor(),
173
+ ref_z: Fixed(y_clip as i32).floor().0,
174
+ };
175
+ }
176
+ }
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // DrawSpriteGlobal (0x541FE0) — type 4, EAX=y, ECX=this, 4 stack, RET 0x10
180
+ // ---------------------------------------------------------------------------
181
+
182
+ usercall_trampoline!(fn trampoline_draw_sprite_global; impl_fn = draw_sprite_global_impl;
183
+ regs = [eax, ecx]; stack_params = 4; ret_bytes = "0x10");
184
+
185
+ unsafe extern "cdecl" fn draw_sprite_global_impl(
186
+ y_pos: u32,
187
+ this: u32,
188
+ layer: u32,
189
+ x_pos: u32,
190
+ sprite_id: u32,
191
+ frame: u32,
192
+ ) {
193
+ let q = &mut *(this as *mut RenderQueue);
194
+
195
+ if let Some(entry) = q.alloc::<DrawSpriteCmd>() {
196
+ *entry = DrawSpriteCmd {
197
+ command_type: command_type::DRAW_SPRITE_GLOBAL,
198
+ layer,
199
+ x: Fixed(x_pos as i32).floor(),
200
+ y: Fixed(y_pos as i32).floor(),
201
+ sprite: SpriteOp(sprite_id),
202
+ palette: frame,
203
+ };
204
+ }
205
+ }
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // DrawSpriteLocal (0x542060) — type 5, EAX=y, ECX=this, 4 stack, RET 0x10
209
+ // ---------------------------------------------------------------------------
210
+
211
+ usercall_trampoline!(fn trampoline_draw_sprite_local; impl_fn = draw_sprite_local_impl;
212
+ regs = [eax, ecx]; stack_params = 4; ret_bytes = "0x10");
213
+
214
+ unsafe extern "cdecl" fn draw_sprite_local_impl(
215
+ y_pos: u32,
216
+ this: u32,
217
+ layer: u32,
218
+ x_pos: u32,
219
+ sprite_id: u32,
220
+ frame: u32,
221
+ ) {
222
+ let q = &mut *(this as *mut RenderQueue);
223
+
224
+ if let Some(entry) = q.alloc::<DrawSpriteCmd>() {
225
+ *entry = DrawSpriteCmd {
226
+ command_type: command_type::DRAW_SPRITE_LOCAL,
227
+ layer,
228
+ x: Fixed(x_pos as i32).floor(),
229
+ y: Fixed(y_pos as i32).floor(),
230
+ sprite: SpriteOp(sprite_id),
231
+ palette: frame,
232
+ };
233
+ }
234
+ }
235
+
236
+ // ---------------------------------------------------------------------------
237
+ // DrawSpriteOffset (0x5420E0) — type 6, ECX=this, EDX=y_clip, 6 stack, RET 0x18
238
+ // ---------------------------------------------------------------------------
239
+
240
+ usercall_trampoline!(fn trampoline_draw_sprite_offset; impl_fn = draw_sprite_offset_impl;
241
+ regs = [ecx, edx]; stack_params = 6; ret_bytes = "0x18");
242
+
243
+ unsafe extern "cdecl" fn draw_sprite_offset_impl(
244
+ this: u32,
245
+ y_clip: u32,
246
+ layer: u32,
247
+ x_pos: u32,
248
+ y_pos: u32,
249
+ sprite_id: u32,
250
+ param_7: u32,
251
+ param_8: u32,
252
+ ) {
253
+ let q = &mut *(this as *mut RenderQueue);
254
+
255
+ if let Some(entry) = q.alloc::<DrawSpriteOffsetCmd>() {
256
+ *entry = DrawSpriteOffsetCmd {
257
+ command_type: command_type::DRAW_SPRITE_OFFSET,
258
+ layer,
259
+ flags: sprite_id,
260
+ x: Fixed(x_pos as i32).floor(),
261
+ y: Fixed(y_pos as i32).floor(),
262
+ ref_z: 0,
263
+ ref_z_2: Fixed(y_clip as i32).floor().0,
264
+ sprite: SpriteOp(param_7),
265
+ palette: param_8,
266
+ };
267
+ }
268
+ }
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // DrawBitmapGlobal (0x542170) — type 1, ECX=this, EDX=y, 7 stack, RET 0x1C
272
+ // ---------------------------------------------------------------------------
273
+
274
+ usercall_trampoline!(fn trampoline_draw_bitmap_global; impl_fn = draw_bitmap_global_impl;
275
+ regs = [ecx, edx]; stack_params = 7; ret_bytes = "0x1C");
276
+
277
+ unsafe extern "cdecl" fn draw_bitmap_global_impl(
278
+ this: u32,
279
+ y_pos: u32,
280
+ layer: u32,
281
+ x_pos: u32,
282
+ bitmap_ptr: u32,
283
+ param_6: u32,
284
+ param_7: u32,
285
+ param_8: u32,
286
+ param_9: u32,
287
+ ) {
288
+ let q = &mut *(this as *mut RenderQueue);
289
+
290
+ if let Some(entry) = q.alloc::<DrawBitmapGlobalCmd>() {
291
+ *entry = DrawBitmapGlobalCmd {
292
+ command_type: command_type::DRAW_BITMAP_GLOBAL,
293
+ layer,
294
+ x: Fixed(x_pos as i32).floor(),
295
+ y: Fixed(y_pos as i32).floor(),
296
+ bitmap: bitmap_ptr as *mut _,
297
+ src_x: 0,
298
+ src_y: param_6 as i32,
299
+ src_w: param_7 as i32,
300
+ src_h: param_8 as i32,
301
+ flags: param_9,
302
+ };
303
+ }
304
+ }
305
+
306
+ // ---------------------------------------------------------------------------
307
+ // DrawTextboxLocal (0x542200) — type 2, ECX=this, EDX=y, 6 stack, RET 0x18
308
+ // ---------------------------------------------------------------------------
309
+
310
+ usercall_trampoline!(fn trampoline_draw_textbox_local; impl_fn = draw_textbox_local_impl;
311
+ regs = [ecx, edx]; stack_params = 6; ret_bytes = "0x18");
312
+
313
+ unsafe extern "cdecl" fn draw_textbox_local_impl(
314
+ this: u32,
315
+ y_pos: u32,
316
+ layer: u32,
317
+ x_pos: u32,
318
+ text_ptr: u32,
319
+ param_6: u32,
320
+ param_7: u32,
321
+ param_8: u32,
322
+ ) {
323
+ let q = &mut *(this as *mut RenderQueue);
324
+
325
+ if let Some(entry) = q.alloc::<DrawTextboxLocalCmd>() {
326
+ *entry = DrawTextboxLocalCmd {
327
+ command_type: command_type::DRAW_TEXTBOX_LOCAL,
328
+ layer,
329
+ mode: 0,
330
+ x: Fixed(x_pos as i32).floor(),
331
+ y: Fixed(y_pos as i32).floor(),
332
+ ref_z: 0,
333
+ ref_z_2: 0,
334
+ bitmap: text_ptr as *mut _,
335
+ src_x: 0,
336
+ src_y: 0,
337
+ src_w: param_6 as i32,
338
+ src_h: param_7 as i32,
339
+ flags: param_8,
340
+ };
341
+ }
342
+ }
343
+
344
+ // ---------------------------------------------------------------------------
345
+ // DrawBungeeTrail (0x500720) — stdcall(task, style, fill), RET 0xC
346
+ //
347
+ // Draws bungee drop trajectory path. Logic lives in openwa_core::render::bungee_trail.
348
+ // ---------------------------------------------------------------------------
349
+
350
+ unsafe extern "stdcall" fn draw_bungee_trail_impl(task_ptr: u32, style: u32, fill: u32) {
351
+ openwa_core::render::bungee_trail::draw_bungee_trail(task_ptr, style, fill);
352
+ }
353
+
354
+ // ---------------------------------------------------------------------------
355
+ // DrawCrosshairLine (0x5197D0) — usercall(EDI=task_ptr), plain RET
356
+ //
357
+ // Draws the weapon aiming crosshair line:
358
+ // 1. Compute direction from angle at task+0x264
359
+ // 2. Compute line length from DDGame scale + task offset
360
+ // 3. Endpoint = start + direction * length (with overflow clamping)
361
+ // 4. DrawPolygon (2 vertices) for the line
362
+ // 5. Conditionally DrawSpriteLocal at endpoint (crosshair sprite)
363
+ // ---------------------------------------------------------------------------
364
+
365
+ usercall_trampoline!(fn trampoline_draw_crosshair_line; impl_fn = draw_crosshair_line_impl;
366
+ reg = edi);
367
+
368
+ unsafe extern "cdecl" fn draw_crosshair_line_impl(task_ptr: u32) {
369
+ openwa_core::render::crosshair_line::draw_crosshair_line(task_ptr);
370
+ }
371
+
372
+ // ---------------------------------------------------------------------------
373
+ // RenderDrawingQueue (0x542350) — usercall(EAX=RenderQueue*) + 2 stack
374
+ // (DisplayGfx*, ClipContext*), RET 0x8.
375
+ //
376
+ // The per-frame render-queue dispatcher. Pure Rust port lives in
377
+ // `openwa_core::render::queue_dispatch::render_drawing_queue`. The
378
+ // trampoline captures EAX and forwards the two stack args.
379
+ // ---------------------------------------------------------------------------
380
+
381
+ usercall_trampoline!(fn trampoline_render_drawing_queue;
382
+ impl_fn = render_drawing_queue_impl;
383
+ reg = eax; stack_params = 2; ret_bytes = "0x8");
384
+
385
+ unsafe extern "cdecl" fn render_drawing_queue_impl(
386
+ rq: *mut RenderQueue,
387
+ display: *mut DisplayGfx,
388
+ clip: *mut ClipContext,
389
+ ) {
390
+ render_drawing_queue(rq, display, clip);
391
+ }
392
+
393
+ // ---------------------------------------------------------------------------
394
+ // Installation
395
+ // ---------------------------------------------------------------------------
396
+
397
+ pub fn install() -> Result<(), String> {
398
+ unsafe {
399
+ let _ = hook::install(
400
+ "EnqueueTiledBitmap",
401
+ va::RQ_ENQUEUE_TILED_BITMAP,
402
+ trampoline_enqueue_tiled_bitmap as *const (),
403
+ )?;
404
+
405
+ let _ = hook::install(
406
+ "DrawLineStrip",
407
+ va::RQ_DRAW_LINE_STRIP,
408
+ trampoline_draw_line_strip as *const (),
409
+ )?;
410
+
411
+ let _ = hook::install(
412
+ "DrawPolygon",
413
+ va::RQ_DRAW_POLYGON,
414
+ trampoline_draw_polygon as *const (),
415
+ )?;
416
+
417
+ let _ = hook::install(
418
+ "DrawCrosshair",
419
+ va::RQ_DRAW_CROSSHAIR,
420
+ trampoline_draw_crosshair as *const (),
421
+ )?;
422
+
423
+ let _ = hook::install(
424
+ "DrawRect",
425
+ va::RQ_DRAW_RECT,
426
+ trampoline_draw_rect as *const (),
427
+ )?;
428
+
429
+ let _ = hook::install(
430
+ "DrawSpriteGlobal",
431
+ va::RQ_DRAW_SPRITE_GLOBAL,
432
+ trampoline_draw_sprite_global as *const (),
433
+ )?;
434
+
435
+ let _ = hook::install(
436
+ "DrawSpriteLocal",
437
+ va::RQ_DRAW_SPRITE_LOCAL,
438
+ trampoline_draw_sprite_local as *const (),
439
+ )?;
440
+
441
+ let _ = hook::install(
442
+ "DrawSpriteOffset",
443
+ va::RQ_DRAW_SPRITE_OFFSET,
444
+ trampoline_draw_sprite_offset as *const (),
445
+ )?;
446
+
447
+ let _ = hook::install(
448
+ "DrawBitmapGlobal",
449
+ va::RQ_DRAW_BITMAP_GLOBAL,
450
+ trampoline_draw_bitmap_global as *const (),
451
+ )?;
452
+
453
+ let _ = hook::install(
454
+ "DrawTextboxLocal",
455
+ va::RQ_DRAW_TEXTBOX_LOCAL,
456
+ trampoline_draw_textbox_local as *const (),
457
+ )?;
458
+
459
+ let _ = hook::install(
460
+ "DrawBungeeTrail",
461
+ va::DRAW_BUNGEE_TRAIL,
462
+ draw_bungee_trail_impl as *const (),
463
+ )?;
464
+
465
+ let _ = hook::install(
466
+ "DrawCrosshairLine",
467
+ va::DRAW_CROSSHAIR_LINE,
468
+ trampoline_draw_crosshair_line as *const (),
469
+ )?;
470
+
471
+ let _ = hook::install(
472
+ "RenderDrawingQueue",
473
+ va::RQ_RENDER_DRAWING_QUEUE,
474
+ trampoline_render_drawing_queue as *const (),
475
+ )?;
476
+ }
477
+ Ok(())
478
+ }