paavohtl commited on
Commit
c2c2c15
Β·
1 Parent(s): f9aa4a2

feat: port DisplayGfx::Destructor (slot 0) to pure Rust

Browse files

Replaces vtable[0] of DisplayGfx (0x66A218) with a native Rust impl
that mirrors the original DestructorImpl (0x56A010) one-to-one.

The cleanup body lives in the new `display::destructor` module and
ports both helpers along with it:

- `free_layer_sprite_table` β€” Rust port of 0x56A280: iterates
`sprite_table[1..0x3FF]`, runs `LayerSpriteFrame::Destructor` on
every entry of each sprite's count-prefixed `frame_array`, then
frees the frame array and the LayerSprite itself.
- `layer_sprite_frame_destructor` β€” Rust port of 0x5732E0: rebinds
the embedded CBitmap vtable and dispatches the surface's
scalar deleting destructor (`vtable[0](1)`) via raw thiscall.
- `tile_bitmap_set_destructor` β€” Rust port of 0x569BC0 (originally
a usercall on EDI): iterates `bitmap_ptrs[0..count]`, calls
`vtable[0](1)` on each non-null `CBitmap*`, frees `bitmap_ptrs`.
- `free_cbitmap_vec` β€” the inline CBitmap vec teardown loop:
releases each non-null `CBitmap.surface` via the new
`SurfaceVtable::release` (slot 6) typed binding, then runs the
bitmap's own `vtable[0](1)`.

The main body executes the 7 cleanup steps (rebind vtable / free
sprite table / free tile bitmap set / free CBitmap vec / destroy
3 layer BitGrids / destroy hook vec entries / release renderer
backend / free hook vec data / rebind+free embedded BitGrid /
free CBitmap vec data / chain to DisplayBase::Destructor) in the
original order. Steps 2a-2f are gated on `display_initialized != 0`,
mirroring the disasm at 0x56A040; steps 3-6 always run.

Adds `SurfaceVtable::release` at slot 6 β€” pure fastcall, no stack
args, writes a result code into the `*FastcallResult` buffer most
callers ignore. The CBitmap vec teardown is its first typed user.

The slot 0 thunk `display_gfx_destructor` is a thin wrapper:
runs the body, then `wa_free(this)` if `flags & 1` (the C++
`delete this` path), and returns `this` per the MSVC ABI.

The original wraps the body in C++ SEH so that step 6 (parent
destructor) can unwind. Our port intentionally omits the SEH frame:
none of the cleanup can throw, and Rust panics aren't C++
exceptions.

Verified headful via `replay-test.ps1 bomber_parachute` β€” game
exits cleanly with no crash or visual regression. Headless tests
pass but per the porting plan don't exercise slot 0 (headless mode
constructs DisplayBase, not DisplayGfx).

crates/openwa-core/src/render/display/context.rs CHANGED
@@ -340,6 +340,17 @@ pub struct SurfaceVtable {
340
  height: i32,
341
  bpp: i32,
342
  ) -> *mut FastcallResult,
 
 
 
 
 
 
 
 
 
 
 
343
  }
344
 
345
  /// Backend-specific surface object created by
 
340
  height: i32,
341
  bpp: i32,
342
  ) -> *mut FastcallResult,
343
+ /// release surface backing storage (slot 6).
344
+ ///
345
+ /// Pure fastcall β€” no stack args. Writes a result code into
346
+ /// `*result.value` (which most callers ignore). Called from the
347
+ /// `DisplayGfx` destructor's CBitmap-vec teardown loop on every
348
+ /// non-null `CBitmap.surface` before the bitmap itself is freed.
349
+ #[slot(6)]
350
+ pub release: unsafe extern "fastcall" fn(
351
+ this: *mut Surface,
352
+ result: *mut FastcallResult,
353
+ ) -> *mut FastcallResult,
354
  }
355
 
356
  /// Backend-specific surface object created by
crates/openwa-core/src/render/display/destructor.rs ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `DisplayGfx::Destructor` (slot 0) β€” pure-Rust port.
2
+ //!
3
+ //! Mirrors the original `DisplayGfx::DestructorImpl` (0x56A010) one-to-one.
4
+ //! See the disassembly in that function for the canonical step layout; the
5
+ //! commit message and the per-step doc comments below summarize the
6
+ //! reasoning. The slot 0 thunk lives in `vtable.rs`.
7
+ //!
8
+ //! # Steps (matching the disasm in 0x56A010)
9
+ //!
10
+ //! 1. Rebind `(*this).vtable` to the most-derived `DisplayGfxVtable` β€”
11
+ //! standard MSVC C++ destructor pattern. The parent destructor will
12
+ //! rebind to `DisplayBaseVtable` after we chain to it.
13
+ //! 2. If `display_initialized != 0`:
14
+ //! 1. `FreeLayerSpriteTable(this)` β€” free all 1024 [`LayerSprite`]
15
+ //! slots in `sprite_table[1..0x3FF]`.
16
+ //! 2. Free `tile_bitmap_sets[1]` (the only used slot) plus its
17
+ //! backing buffer.
18
+ //! 3. For every non-null entry in the CBitmap vec, call
19
+ //! `surface.vtable[6]` (release) on its surface, then
20
+ //! `bitmap.vtable[0](1)` (scalar deleting destructor).
21
+ //! 4. Call the layer destructor (`vtable[3](1)`) on `layer_2`,
22
+ //! `layer_1`, `layer_0` β€” in that order, mirroring MSVC's
23
+ //! reverse-construction destruction order.
24
+ //! 5. For every non-null entry in the FramePostProcessHook vec,
25
+ //! call its destructor `vtable[0](1)`.
26
+ //! 6. Call `g_RenderContext->vtable[7](1)` (release_frame_buffer)
27
+ //! β€” the renderer-backend release.
28
+ //! 3. Always-run cleanup (also taken when `display_initialized == 0`):
29
+ //! - Free the FramePostProcessHook vec data buffer + zero its
30
+ //! proxy/first/last/end fields.
31
+ //! - Rebind the embedded `BitGrid` at `+0x3DA8` to the base BitGrid
32
+ //! vtable, then free its data buffer if `external_buffer == 0`.
33
+ //! - Free the CBitmap vec data buffer + zero its three pointers.
34
+ //! - Chain to `DisplayBase::Destructor` (still WA-side; bridged).
35
+ //!
36
+ //! The original wraps the body in a C++ SEH frame because step 6 (the
37
+ //! parent destructor) might unwind. We don't need to mirror that β€” none
38
+ //! of our cleanup can throw, and Rust panics aren't C++ exceptions.
39
+
40
+ use core::ffi::c_void;
41
+ use core::ptr;
42
+
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};
49
+ use crate::render::display::vtable::{DisplayGfxVtable, DISPLAY_GFX_VTABLE};
50
+ use crate::render::sprite::sprite::{CBitmap, LayerSprite, LayerSpriteFrame};
51
+ use crate::wa_alloc::wa_free;
52
+
53
+ // =============================================================================
54
+ // Slot 0 thunk β€” the actual vtable[0] entry point.
55
+ // =============================================================================
56
+
57
+ /// `DisplayGfx::Destructor` (vtable[0] thunk, original at 0x569CE0).
58
+ ///
59
+ /// Standard MSVC scalar deleting destructor: runs the cleanup body, then
60
+ /// calls `_free(this)` if `flags & 1` is set (i.e. the C++ caller used
61
+ /// `delete this`). Returns `this` per the MSVC ABI.
62
+ #[unsafe(no_mangle)]
63
+ pub unsafe extern "thiscall" fn display_gfx_destructor(
64
+ this: *mut DisplayGfx,
65
+ flags: u32,
66
+ ) -> *mut DisplayGfx {
67
+ display_gfx_destructor_impl(this);
68
+ if flags & 1 != 0 {
69
+ wa_free(this);
70
+ }
71
+ this
72
+ }
73
+
74
+ // =============================================================================
75
+ // Body β€” the actual cleanup logic.
76
+ // =============================================================================
77
+
78
+ unsafe fn display_gfx_destructor_impl(this: *mut DisplayGfx) {
79
+ // Step 1: rebind the vtable to the most-derived DisplayGfx vtable.
80
+ // Original at 0x56A030: `MOV [ESI], 0x66A218`. The parent destructor
81
+ // (DisplayBase::Destructor) will overwrite it with the DisplayBase
82
+ // vtable when we chain to it at the end.
83
+ (*this).base.vtable = rb(DISPLAY_GFX_VTABLE) as *const DisplayGfxVtable;
84
+
85
+ // Step 2: only run if Init() actually completed.
86
+ if (*this).base.display_initialized != 0 {
87
+ // Step 2a: free the LayerSprite[1024] table at +0x3DD4.
88
+ free_layer_sprite_table(this);
89
+
90
+ // Step 2b: free tile_bitmap_sets[1] and its inner pointer table.
91
+ let tile_set = (*this).tile_bitmap_sets[1] as *mut TileBitmapSet;
92
+ if !tile_set.is_null() {
93
+ tile_bitmap_set_destructor(tile_set);
94
+ wa_free(tile_set);
95
+ (*this).tile_bitmap_sets[1] = ptr::null_mut();
96
+ }
97
+
98
+ // Step 2c: iterate the CBitmap vec, release surface + destroy bitmap.
99
+ free_cbitmap_vec(this);
100
+
101
+ // Step 2d: destroy the three render-context layer BitGrids in
102
+ // reverse-construction order. Each call is `vtable[3](this, 1)`.
103
+ for layer in [(*this).layer_2, (*this).layer_1, (*this).layer_0] {
104
+ if !layer.is_null() {
105
+ ((*(*layer).vtable).destructor)(layer, 1);
106
+ }
107
+ }
108
+
109
+ // Step 2e: destroy every FramePostProcessHook in the vec.
110
+ let mut p = (*this).hook_vec_first as *mut *mut FramePostProcessHook;
111
+ let end = (*this).hook_vec_last as *mut *mut FramePostProcessHook;
112
+ while p != end && !p.is_null() {
113
+ let hook = *p;
114
+ if !hook.is_null() {
115
+ ((*(*hook).vtable).destructor)(hook, 1);
116
+ }
117
+ p = p.add(1);
118
+ }
119
+
120
+ // Step 2f: release the renderer backend's framebuffer.
121
+ // Original at 0x56A1D6: `g_RenderContext->vtable[7](1)`. The
122
+ // typed binding is `release_frame_buffer(this, &result, 1)`.
123
+ let render_ctx = *(rb(va::G_RENDER_CONTEXT) as *const *mut RenderContext);
124
+ if !render_ctx.is_null() {
125
+ let mut result = FastcallResult::default();
126
+ ((*(*render_ctx).vtable).release_frame_buffer)(render_ctx, &mut result, 1);
127
+ }
128
+ }
129
+
130
+ // Step 3: free the hook vec backing buffer + clear pointers.
131
+ let hook_first = (*this).hook_vec_first;
132
+ if !hook_first.is_null() {
133
+ wa_free(hook_first);
134
+ }
135
+ (*this).hook_vec_first = ptr::null_mut();
136
+ (*this).hook_vec_last = ptr::null_mut();
137
+ (*this).hook_vec_end = ptr::null_mut();
138
+
139
+ // Step 4: rebind the embedded BitGrid at +0x3DA8 to the base BitGrid
140
+ // vtable, then conditionally free its data buffer.
141
+ let bg = &raw mut (*this).embedded_bitgrid;
142
+ (*bg).vtable = rb(BIT_GRID_BASE_VTABLE) as *const BitGridBaseVtable;
143
+ if (*bg).external_buffer == 0 {
144
+ // Note: the original frees `data` unconditionally inside the
145
+ // `external_buffer == 0` branch, even if `data` happens to be
146
+ // null β€” `_free(NULL)` is a no-op so it's safe.
147
+ wa_free((*bg).data);
148
+ }
149
+
150
+ // Step 5: free the CBitmap vec backing buffer + clear pointers.
151
+ let bitmap_ptr = (*this).bitmap_ptr;
152
+ if !bitmap_ptr.is_null() {
153
+ wa_free(bitmap_ptr);
154
+ }
155
+ (*this).bitmap_ptr = ptr::null_mut();
156
+ (*this).bitmap_end = ptr::null_mut();
157
+ (*this).bitmap_capacity = ptr::null_mut();
158
+
159
+ // Step 6: chain to the parent class destructor. Still WA-side until
160
+ // someone ports DisplayBase::Destructor too.
161
+ let parent_dtor: unsafe extern "thiscall" fn(*mut DisplayGfx) =
162
+ core::mem::transmute(rb(DISPLAY_BASE_DESTRUCTOR_IMPL) as usize);
163
+ parent_dtor(this);
164
+ }
165
+
166
+ // =============================================================================
167
+ // Step 2a β€” `DisplayGfx::FreeLayerSpriteTable` (0x56A280)
168
+ // =============================================================================
169
+
170
+ /// Pure Rust port of `DisplayGfx::FreeLayerSpriteTable` (0x56A280).
171
+ ///
172
+ /// Iterates `sprite_table[1..0x3FF]` (note: index 0 is reserved/unused).
173
+ /// For each non-null `LayerSprite`:
174
+ /// 1. If the sprite has a `frame_array`, run `LayerSpriteFrame::Destructor`
175
+ /// on every element via the standard MSVC count-prefixed array layout
176
+ /// (`count` lives at `frame_array[-4]`), then free the count-prefixed
177
+ /// block.
178
+ /// 2. Free the `LayerSprite` itself.
179
+ /// 3. Zero the slot.
180
+ unsafe fn free_layer_sprite_table(this: *mut DisplayGfx) {
181
+ for idx in 1usize..0x400 {
182
+ let slot = &raw mut (*this).sprite_table[idx];
183
+ let layer_sprite: *mut LayerSprite = *slot;
184
+ if layer_sprite.is_null() {
185
+ continue;
186
+ }
187
+
188
+ let frame_array = (*layer_sprite).frame_array;
189
+ if !frame_array.is_null() {
190
+ // Count-prefix lives at `frame_array[-4]`. The base of the
191
+ // freeable allocation is `frame_array - 4` bytes.
192
+ let count_ptr = (frame_array as *mut u8).sub(4) as *mut u32;
193
+ let count = *count_ptr as usize;
194
+ for i in 0..count {
195
+ layer_sprite_frame_destructor(frame_array.add(i));
196
+ }
197
+ wa_free(count_ptr);
198
+ }
199
+
200
+ wa_free(layer_sprite);
201
+ *slot = ptr::null_mut();
202
+ }
203
+ }
204
+
205
+ /// Pure Rust port of `LayerSpriteFrame::Destructor` (0x5732E0).
206
+ ///
207
+ /// Rebinds the embedded CBitmap vtable to the standard CBitmap vtable
208
+ /// (`0x643F64`) β€” necessary for any post-destruction code that might
209
+ /// dispatch through it β€” then releases the surface via its `vtable[0](1)`
210
+ /// scalar deleting destructor if non-null.
211
+ unsafe fn layer_sprite_frame_destructor(frame: *mut LayerSpriteFrame) {
212
+ use crate::render::sprite::sprite::CBITMAP_VTABLE_MAYBE;
213
+
214
+ (*frame).bitmap_vtable = rb(CBITMAP_VTABLE_MAYBE);
215
+ let surface = (*frame).surface as *mut c_void;
216
+ if !surface.is_null() {
217
+ // surface->vtable[0](this, 1) β€” scalar deleting destructor.
218
+ let vtable = *(surface as *const *const *const c_void);
219
+ let dtor: unsafe extern "thiscall" fn(*mut c_void, u32) -> *mut c_void =
220
+ core::mem::transmute(*vtable);
221
+ dtor(surface, 1);
222
+ }
223
+ }
224
+
225
+ // =============================================================================
226
+ // Step 2b β€” `TileBitmapSet::Destructor` (0x569BC0)
227
+ // =============================================================================
228
+
229
+ /// Pure Rust port of `TileBitmapSet::Destructor` (0x569BC0).
230
+ ///
231
+ /// The original takes `this` in EDI (usercall) β€” we sidestep the calling
232
+ /// convention entirely by porting the body. Iterates
233
+ /// `bitmap_ptrs[0..count]`, calls `vtable[0](1)` on each non-null
234
+ /// `CBitmap*`, then frees the `bitmap_ptrs` array.
235
+ unsafe fn tile_bitmap_set_destructor(set: *mut TileBitmapSet) {
236
+ let count = (*set).count as i32;
237
+ let bitmap_ptrs = (*set).bitmap_ptrs as *mut *mut CBitmap;
238
+ if bitmap_ptrs.is_null() || count <= 0 {
239
+ // Defensive: original asm always reads bitmap_ptrs unconditionally
240
+ // but we guard the (rare) null case. Still free below if non-null.
241
+ } else {
242
+ for i in 0..count as usize {
243
+ let bitmap = *bitmap_ptrs.add(i);
244
+ if !bitmap.is_null() {
245
+ cbitmap_vtable0(bitmap, 1);
246
+ }
247
+ }
248
+ }
249
+ if !bitmap_ptrs.is_null() {
250
+ wa_free(bitmap_ptrs);
251
+ }
252
+ }
253
+
254
+ // =============================================================================
255
+ // Step 2c β€” CBitmap vec teardown
256
+ // =============================================================================
257
+
258
+ /// Iterate the `CBitmap*` vec at `+0x3580` (start) / `+0x3584` (end) and
259
+ /// destroy every entry: release the backend surface (slot 6) + run the
260
+ /// scalar deleting destructor (slot 0) on the `CBitmap` itself.
261
+ unsafe fn free_cbitmap_vec(this: *mut DisplayGfx) {
262
+ let start = (*this).bitmap_ptr;
263
+ let end = (*this).bitmap_end;
264
+ if start.is_null() || end <= start {
265
+ return;
266
+ }
267
+ let count = end.offset_from(start) as usize;
268
+ for i in 0..count {
269
+ let bitmap = *start.add(i);
270
+ if bitmap.is_null() {
271
+ continue;
272
+ }
273
+ let surface = (*bitmap).surface;
274
+ if !surface.is_null() {
275
+ // surface->vtable[6](this, &result) β€” release the backend
276
+ // surface storage. Result code is unused.
277
+ let mut result = FastcallResult::default();
278
+ ((*(*surface).vtable).release)(surface, &mut result);
279
+ }
280
+ cbitmap_vtable0(bitmap, 1);
281
+ }
282
+ }
283
+
284
+ /// Call `CBitmap.vtable[0]` (the scalar deleting destructor) β€” `CBitmap`'s
285
+ /// vtable isn't yet typed in Rust, so we dispatch via a raw transmute. The
286
+ /// vtable is `0x643F64` for the standard tile-cache CBitmap and the
287
+ /// destructor returns `*mut CBitmap` per the MSVC ABI.
288
+ unsafe fn cbitmap_vtable0(bitmap: *mut CBitmap, flags: u32) {
289
+ let vtable = (*bitmap).vtable as *const usize;
290
+ let dtor: unsafe extern "thiscall" fn(*mut CBitmap, u32) -> *mut CBitmap =
291
+ core::mem::transmute(*vtable);
292
+ dtor(bitmap, flags);
293
+ }
crates/openwa-core/src/render/display/mod.rs CHANGED
@@ -1,5 +1,6 @@
1
  pub mod base;
2
  pub mod context;
 
3
  pub mod font;
4
  pub mod frame_hook;
5
  pub mod gfx;
 
1
  pub mod base;
2
  pub mod context;
3
+ pub mod destructor;
4
  pub mod font;
5
  pub mod frame_hook;
6
  pub mod gfx;
crates/openwa-dll/src/replacements/render.rs CHANGED
@@ -691,6 +691,7 @@ fn install_render_queue() -> Result<(), String> {
691
 
692
  use openwa_core::bitgrid::DisplayBitGrid;
693
  use openwa_core::fixed::Fixed;
 
694
  use openwa_core::render::display::vtable::{self as display_vtable, DisplayGfxVtable};
695
  use openwa_core::render::display::DisplayBase;
696
  use openwa_core::render::display::DisplayGfx;
@@ -1330,6 +1331,7 @@ fn install_display() -> Result<(), String> {
1330
 
1331
  // Patch DisplayGfx vtable (0x66A218): replace ported methods with Rust.
1332
  vtable_replace!(DisplayGfxVtable, va::DISPLAY_GFX_VTABLE, {
 
1333
  get_dimensions => display_vtable::get_dimensions,
1334
  set_layer_color => display_vtable::set_layer_color,
1335
  set_active_layer => display_vtable::set_active_layer,
 
691
 
692
  use openwa_core::bitgrid::DisplayBitGrid;
693
  use openwa_core::fixed::Fixed;
694
+ use openwa_core::render::display::destructor as display_destructor;
695
  use openwa_core::render::display::vtable::{self as display_vtable, DisplayGfxVtable};
696
  use openwa_core::render::display::DisplayBase;
697
  use openwa_core::render::display::DisplayGfx;
 
1331
 
1332
  // Patch DisplayGfx vtable (0x66A218): replace ported methods with Rust.
1333
  vtable_replace!(DisplayGfxVtable, va::DISPLAY_GFX_VTABLE, {
1334
+ destructor => display_destructor::display_gfx_destructor,
1335
  get_dimensions => display_vtable::get_dimensions,
1336
  set_layer_color => display_vtable::set_layer_color,
1337
  set_active_layer => display_vtable::set_active_layer,