Download crates/openwa-core/src/render/queue_dispatch.rs from nmarshall/wormz-a: direct link, hf CLI and curl.
- Browser
- Download file 43.7 kB
-
https://huggingface.co/nmarshall/wormz-a/resolve/main/crates/openwa-core/src/render/queue_dispatch.rs
- Command line
-
hf download hf://nmarshall/wormz-a/crates/openwa-core/src/render/queue_dispatch.rs
-
curl -L -o queue_dispatch.rs https://huggingface.co/nmarshall/wormz-a/resolve/main/crates/openwa-core/src/render/queue_dispatch.rs
43.7 kB
| //! `RenderDrawingQueue` (0x542350) — the per-frame render-queue dispatcher. | |
| //! | |
| //! This is the bridge between the producer-side `RenderQueue` (a buffer of | |
| //! enqueued draw commands) and the consumer-side `DisplayGfx` vtable. Once | |
| //! per frame, `GameRender_Maybe` (0x533DC0) sets up a [`ClipContext`] and | |
| //! invokes this function with `(EAX = render_queue, stack[0] = display_gfx, | |
| //! stack[1] = clip_ctx)`. The dispatcher then: | |
| //! | |
| //! 1. Sorts the entry-pointer array by each command's `layer` field | |
| //! (ascending; using a non-stable sort to mirror MSVC `qsort`). | |
| //! 2. Walks the sorted list from highest index downward (so high-layer | |
| //! commands draw first, low-layer commands draw on top). | |
| //! 3. Switches on `cmd[0]` (command type, 0..0xE) and dispatches to the | |
| //! appropriate `DisplayGfx` vtable slot, passing through one of the | |
| //! `RQ_*` clip / translate helpers as needed. | |
| //! | |
| //! The 14 command types map to slots 11..22 of [`DisplayGfxVtable`]: | |
| //! | |
| //! | type | producer | slot | DisplayGfx method | | |
| //! |------|----------------|------|------------------------| | |
| //! | 0 | DRAW_RECT | 18 | `fill_rect` | | |
| //! | 1 | DRAW_BITMAP_GLOBAL | 20 | `draw_scaled_sprite` | | |
| //! | 2 | DRAW_TEXTBOX_LOCAL | 20 | `draw_scaled_sprite` | | |
| //! | 3 | (no Rust producer) | 21 | `draw_via_callback` | | |
| //! | 4 | DRAW_SPRITE_GLOBAL | 19 | `blit_sprite` | | |
| //! | 5 | DRAW_SPRITE_LOCAL | 19 | `blit_sprite` | | |
| //! | 6 | DRAW_SPRITE_OFFSET | 19 | `blit_sprite` | | |
| //! | 7 | (no Rust producer) | 12 | `draw_polyline` | | |
| //! | 8 | DRAW_LINE_STRIP | 14 | `draw_line_clipped` | | |
| //! | 9 | DRAW_POLYGON | 13 | `draw_line` | | |
| //! | 0xA | (no Rust producer) | 15 | `draw_pixel_strip` | | |
| //! | 0xB | DRAW_CROSSHAIR | 16 | `draw_crosshair` | | |
| //! | 0xC | DRAW_OUTLINED_PIXEL | 17 | `draw_outlined_pixel` | | |
| //! | 0xD | DRAW_TILED_BITMAP | 11 | `draw_tiled_bitmap` | | |
| //! | 0xE | (no Rust producer) | 22 | `draw_tiled_terrain` | | |
| //! | |
| //! ## Calling convention | |
| //! | |
| //! `RenderDrawingQueue` is `__usercall(EAX = *mut RenderQueue, | |
| //! stack[0] = *mut DisplayGfx, stack[1] = *mut ClipContext)`, returning | |
| //! with `RET 0x8`. Verified from caller `GameRender_Maybe` at 0x5340af. | |
| //! | |
| //! ## Helper calling conventions (verified from disassembly) | |
| //! | |
| //! - `RQ_ClipCoordinates` (0x542BA0) is `__thiscall(ClipContext*)` taking | |
| //! `(x_in, y_in, ref_z, *out_x, *out_y, *out_scale)`. | |
| //! - `RQ_ClipCoordinatesWithRef` (0x542C70) has the same shape but uses | |
| //! the pivot fields `clip[2]/clip[3]`. | |
| //! - `RQ_TranslateCoordinates` (0x542B10) is `__usercall(ECX = x_in, | |
| //! EAX = y_in, EDI = ClipContext*, stack[0] = *out_x, stack[1] = *out_y)`. | |
| //! The Ghidra decompile lost the ECX input — verified from disasm at | |
| //! 0x542B1A (`AND ECX, 0xFFFF0000`). Same shape as `RQ_ClipCoordinates` | |
| //! but without the perspective scale. | |
| use crate::fixed::Fixed; | |
| use crate::render::display::gfx::DisplayGfx; | |
| use crate::render::message::{RenderMessage, TypedRenderCmd, COMMAND_TYPE_TYPED}; | |
| use crate::render::queue::RenderQueue; | |
| use crate::render::sprite::sprite_op::SpriteOp; | |
| // ============================================================================= | |
| // ClipContext — the 4-i32 anchor/pivot block GameRender_Maybe builds on stack | |
| // ============================================================================= | |
| /// Per-frame clip / camera context, built on the stack by | |
| /// `GameRender_Maybe` (0x533DC0) and passed as the second stack arg to | |
| /// [`render_drawing_queue`]. | |
| /// | |
| /// The first two fields are the camera anchor in fixed-16.16 coordinates; | |
| /// the second pair are pivot points for `RQ_ClipCoordinatesWithRef` (case 6 | |
| /// with `flags & 4`). All four are sourced from `DisplayGfx + 0x8CEC..0x8CFC` | |
| /// at the start of `GameRender_Maybe` and adjusted in place by the | |
| /// perspective-zoom math before being handed to the dispatcher. | |
| pub struct ClipContext { | |
| /// Camera anchor X in Fixed16. Subtracted from each command's source | |
| /// X by the clip helpers; only the upper 16 bits matter. | |
| pub cam_x: Fixed, | |
| /// Camera anchor Y in Fixed16. | |
| pub cam_y: Fixed, | |
| /// Pivot X for the perspective-with-pivot path | |
| /// (`RQ_ClipCoordinatesWithRef`, case 6 with `cmd.flags & 4`). | |
| /// Stored as Fixed16 with the fractional bits guaranteed zero by | |
| /// `GameRender_Maybe`'s setup math. | |
| pub pivot_x: Fixed, | |
| /// Pivot Y — same as `pivot_x`. | |
| pub pivot_y: Fixed, | |
| } | |
| const _: () = assert!(core::mem::size_of::<ClipContext>() == 0x10); | |
| // ============================================================================= | |
| // Pixel-aligned subtract with overflow saturation | |
| // ============================================================================= | |
| /// Pixel-aligned subtract with overflow saturation (the inner loop body | |
| /// shared by `RQ_ClipCoordinates` and `RQ_TranslateCoordinates`). | |
| /// | |
| /// Both inputs are masked to their upper 16 bits via [`Fixed::floor`] | |
| /// (truncated to pixel resolution while staying in fixed-16.16 | |
| /// representation). Returns `in_val - cam` clamped to `Fixed(i32::MIN)` | |
| /// / `Fixed(i32::MAX)` on either-direction overflow. | |
| /// | |
| /// `FixedDiv16_16` (0x5B3501), incidentally also used by these helpers, | |
| /// is the same operation as [`Fixed::div`] (the `Div` impl on `Fixed`): | |
| /// `(num << 16) / denom` with a 64-bit signed intermediate. We don't | |
| /// need a standalone `fixed_div_16_16` helper. | |
| fn clip_sub_saturate(in_val: Fixed, cam: Fixed) -> Fixed { | |
| let in_val = in_val.floor(); | |
| let cam = cam.floor(); | |
| let delta = Fixed(in_val.0.wrapping_sub(cam.0)); | |
| if in_val.0 < 0 { | |
| // in_val is negative; if cam is positive and delta wrapped positive, | |
| // it's an underflow → saturate to MIN. | |
| if cam.0 > 0 && delta.0 > 0 { | |
| return Fixed(i32::MIN); | |
| } | |
| } else { | |
| // in_val is non-negative; if cam is negative and delta wrapped | |
| // negative, it's an overflow → saturate to MAX. | |
| if cam.0 < 0 && delta.0 < 0 { | |
| return Fixed(i32::MAX); | |
| } | |
| } | |
| delta | |
| } | |
| /// Perspective scale `0x100_0000 / projected_z` as a Fixed16 ratio. | |
| /// Pulled out so the same expression doesn't have to be re-derived in | |
| /// each clip helper. | |
| fn perspective_scale(projected_z: i32) -> Fixed { | |
| // Equivalent to the WA `FixedDiv16_16` call: `(0x100_0000 << 16) / projected_z`. | |
| Fixed::from_raw(0x0100_0000) / Fixed::from_raw(projected_z) | |
| } | |
| // ============================================================================= | |
| // RQ_ClipCoordinates (0x542BA0) — perspective-scaled clip | |
| // ============================================================================= | |
| /// `RQ_ClipCoordinates` (0x542BA0) — perspective clip with no pivot. | |
| /// | |
| /// Returns `false` if `ref_z + 0x100_0000 < 1` (projection plane behind | |
| /// the eye); otherwise writes `(out_x, out_y, out_scale)` and returns true. | |
| /// | |
| /// Calling convention in WA: `__thiscall(ClipContext*)`. The Rust port | |
| /// takes the context by reference instead. | |
| pub fn rq_clip_coordinates( | |
| clip: &ClipContext, | |
| x_in: Fixed, | |
| y_in: Fixed, | |
| ref_z: i32, | |
| out_x: &mut Fixed, | |
| out_y: &mut Fixed, | |
| out_scale: &mut Fixed, | |
| ) -> bool { | |
| let delta_x = clip_sub_saturate(x_in, clip.cam_x); | |
| let delta_y = clip_sub_saturate(y_in, clip.cam_y); | |
| let projected_z = ref_z.wrapping_add(0x0100_0000); | |
| if projected_z < 1 { | |
| return false; | |
| } | |
| let scale = perspective_scale(projected_z); | |
| *out_scale = scale; | |
| *out_x = delta_x.mul_raw(scale); | |
| *out_y = delta_y.mul_raw(scale); | |
| true | |
| } | |
| // ============================================================================= | |
| // RQ_ClipCoordinatesWithRef (0x542C70) — perspective with pivot | |
| // ============================================================================= | |
| /// `RQ_ClipCoordinatesWithRef` (0x542C70) — perspective clip relative to | |
| /// a pivot point (read from `clip.pivot_x/pivot_y`). | |
| /// | |
| /// The `(cam − pivot)` delta is scaled by the perspective factor and added | |
| /// back to `pivot`, producing the projected camera position. The input | |
| /// `(x_in, y_in)` is then subtracted from that projected position with | |
| /// saturation. | |
| /// | |
| /// Used by case 6 (`DRAW_SPRITE_OFFSET`) when `flags & 4` is set. | |
| pub fn rq_clip_coordinates_with_ref( | |
| clip: &ClipContext, | |
| x_in: Fixed, | |
| y_in: Fixed, | |
| ref_z: i32, | |
| out_x: &mut Fixed, | |
| out_y: &mut Fixed, | |
| out_scale: &mut Fixed, | |
| ) -> bool { | |
| let projected_z = ref_z.wrapping_add(0x0100_0000); | |
| if projected_z < 1 { | |
| return false; | |
| } | |
| let scale = perspective_scale(projected_z); | |
| *out_scale = scale; | |
| // Project camera position relative to pivot, then subtract from input. | |
| let cam_x_pixel = clip.cam_x.floor(); | |
| let cam_y_pixel = clip.cam_y.floor(); | |
| let projected_cam_x = (cam_x_pixel - clip.pivot_x).mul_raw(scale) + clip.pivot_x; | |
| let projected_cam_y = (cam_y_pixel - clip.pivot_y).mul_raw(scale) + clip.pivot_y; | |
| *out_x = sub_saturate(x_in, projected_cam_x); | |
| *out_y = sub_saturate(y_in, projected_cam_y); | |
| true | |
| } | |
| /// Saturating signed subtract on `Fixed` values. Used by | |
| /// `rq_clip_coordinates_with_ref`'s outer subtract, which operates on | |
| /// already-projected positions where the inputs are no longer guaranteed | |
| /// pixel-aligned, so it does NOT pre-floor like [`clip_sub_saturate`]. | |
| fn sub_saturate(in_val: Fixed, cam: Fixed) -> Fixed { | |
| let delta = Fixed(in_val.0.wrapping_sub(cam.0)); | |
| if in_val.0 < 0 { | |
| if cam.0 > 0 && delta.0 > 0 { | |
| return Fixed(i32::MIN); | |
| } | |
| } else { | |
| if cam.0 < 0 && delta.0 < 0 { | |
| return Fixed(i32::MAX); | |
| } | |
| } | |
| delta | |
| } | |
| // ============================================================================= | |
| // RQ_TranslateCoordinates (0x542B10) — clip with no perspective | |
| // ============================================================================= | |
| /// `RQ_TranslateCoordinates` (0x542B10) — same shape as | |
| /// `rq_clip_coordinates` but without the perspective scaling. Always | |
| /// succeeds. | |
| /// | |
| /// In WA this is `__usercall(ECX = x_in, EAX = y_in, EDI = ClipContext*, | |
| /// stack[0] = *out_x, stack[1] = *out_y)`. The Ghidra decompile lost the | |
| /// ECX input (annotated `unaff_EDI` for the context and `in_EAX` for `y`, | |
| /// silently dropping `x`); verified from the prologue disassembly at | |
| /// 0x542B1A which masks ECX with `0xFFFF_0000`. | |
| pub fn rq_translate_coordinates( | |
| clip: &ClipContext, | |
| x_in: Fixed, | |
| y_in: Fixed, | |
| out_x: &mut Fixed, | |
| out_y: &mut Fixed, | |
| ) { | |
| *out_x = clip_sub_saturate(x_in, clip.cam_x); | |
| *out_y = clip_sub_saturate(y_in, clip.cam_y); | |
| } | |
| // ============================================================================= | |
| // RenderDrawingQueue (0x542350) — the dispatcher | |
| // ============================================================================= | |
| /// `RenderDrawingQueue` (0x542350) — the per-frame render-queue dispatcher. | |
| /// | |
| /// See module-level docs for the per-case slot mapping and calling | |
| /// convention. The Rust signature here flattens the WA usercall into a | |
| /// plain function: a thin trampoline in `openwa-dll` captures the EAX | |
| /// register input and forwards the two stack args. | |
| /// | |
| /// # Safety | |
| /// | |
| /// All three pointers must be valid for the duration of the call. The | |
| /// render queue's `entry_count` must accurately reflect the number of | |
| /// `entry_ptrs` slots in use, and each entry pointer must point at a | |
| /// valid command structure whose first `u32` is a command type in `0..=0xE` | |
| /// (commands with type `> 0xE` are silently ignored, matching WA — the | |
| /// `JA 0x542a9e` at 0x54239f branches into the loop tail). | |
| pub unsafe fn render_drawing_queue( | |
| rq: *mut RenderQueue, | |
| display: *mut DisplayGfx, | |
| clip: *mut ClipContext, | |
| ) { | |
| let count = (*rq).entry_count as usize; | |
| if count == 0 { | |
| return; | |
| } | |
| let entry_ptrs_base = core::ptr::addr_of_mut!((*rq).entry_ptrs) as *mut *mut u8; | |
| let entries: &mut [*mut u8] = core::slice::from_raw_parts_mut(entry_ptrs_base, count); | |
| // Sort by cmd[1] = layer field. The original uses MSVC qsort (unstable); | |
| // we mirror that with sort_unstable_by_key. | |
| entries.sort_unstable_by_key(|&p| { | |
| // SAFETY: entries are valid command pointers; cmd[1] is in range. | |
| unsafe { *(p as *const i32).add(1) } | |
| }); | |
| let clip_ref: &ClipContext = &*clip; | |
| // Walk from the highest index downward. | |
| for i in (0..count).rev() { | |
| let cmd = entries[i] as *const u32; | |
| let cmd_type = *cmd; | |
| match cmd_type { | |
| 0 => dispatch_case_0_fill_rect(display, clip_ref, cmd), | |
| 1 => dispatch_case_1_bitmap_global(display, cmd), | |
| 2 => dispatch_case_2_textbox_local(display, clip_ref, cmd), | |
| 3 => dispatch_case_3_via_callback(display, clip_ref, cmd), | |
| 4 => dispatch_case_4_sprite_global(display, cmd), | |
| 5 => dispatch_case_5_sprite_local(display, clip_ref, cmd), | |
| 6 => dispatch_case_6_sprite_offset(display, clip_ref, cmd), | |
| 7 => dispatch_case_7_polyline(display, cmd), | |
| 8 => dispatch_case_8_line_strip(display, clip_ref, cmd), | |
| 9 => dispatch_case_9_polygon(display, clip_ref, cmd), | |
| 0xA => dispatch_case_a_pixel_strip(display, clip_ref, cmd), | |
| 0xB => dispatch_case_b_crosshair(display, clip_ref, cmd), | |
| 0xC => dispatch_case_c_outlined_pixel(display, clip_ref, cmd), | |
| 0xD => dispatch_case_d_tiled_bitmap(display, clip_ref, cmd), | |
| 0xE => dispatch_case_e_tiled_terrain(display, clip_ref, cmd), | |
| COMMAND_TYPE_TYPED => { | |
| let typed = &(*(cmd as *const TypedRenderCmd)).message; | |
| dispatch_typed(display, clip_ref, typed); | |
| } | |
| _ => { /* unknown command type — silently skipped, matching WA */ } | |
| } | |
| } | |
| } | |
| // ============================================================================= | |
| // Per-case dispatchers | |
| // ============================================================================= | |
| // | |
| // Each case body mirrors the corresponding `case N:` arm of WA's | |
| // `RenderDrawingQueue` (0x542350). Field offsets are the `puVar1[N]` | |
| // indices from the decompile (each step = 4 bytes). | |
| // | |
| // Vtable dispatch goes through the auto-generated `DisplayGfx::*_raw` | |
| // methods (from `bind_DisplayGfxVtable!(DisplayGfx, base.vtable)`), | |
| // which take a raw `*mut DisplayGfx` to avoid creating `&mut self` — | |
| // see the noalias rule in CLAUDE.md. | |
| unsafe fn read_field(cmd: *const u32, idx: usize) -> i32 { | |
| *(cmd.add(idx) as *const i32) | |
| } | |
| unsafe fn read_fixed(cmd: *const u32, idx: usize) -> Fixed { | |
| Fixed::from_raw(*(cmd.add(idx) as *const i32)) | |
| } | |
| // ---- case 0: DRAW_RECT → slot 18 (fill_rect) ------------------------------- | |
| // | |
| // iVar4 = RQ_ClipCoordinates(this, cmd[3], cmd[4], cmd[7], &local_18, ¶m_1, ..); | |
| // if !iVar4 break; | |
| // iVar4 = RQ_ClipCoordinates(this, cmd[5], cmd[6], cmd[7], &local_10, &local_c, ..); | |
| // if !iVar4 break; | |
| // // x/y MIN-corner clamps to keep pixel-perfect rect edges | |
| // vtable[18](this, x1>>16, y1>>16, x2>>16, y2>>16, cmd[2]); | |
| unsafe fn dispatch_case_0_fill_rect(display: *mut DisplayGfx, clip: &ClipContext, cmd: *const u32) { | |
| let mut x1 = Fixed::ZERO; | |
| let mut y1 = Fixed::ZERO; | |
| let mut x2 = Fixed::ZERO; | |
| let mut y2 = Fixed::ZERO; | |
| let mut scale = Fixed::ZERO; | |
| if !rq_clip_coordinates( | |
| clip, | |
| read_fixed(cmd, 3), | |
| read_fixed(cmd, 4), | |
| read_field(cmd, 7), | |
| &mut x1, | |
| &mut y1, | |
| &mut scale, | |
| ) { | |
| return; | |
| } | |
| if !rq_clip_coordinates( | |
| clip, | |
| read_fixed(cmd, 5), | |
| read_fixed(cmd, 6), | |
| read_field(cmd, 7), | |
| &mut x2, | |
| &mut y2, | |
| &mut scale, | |
| ) { | |
| return; | |
| } | |
| // Pixel-perfect MIN/MAX rect-edge clamping when the source is at the | |
| // sentinel anchor values: cmd[3]/cmd[4] == -0x80000000 → clamp to MIN, | |
| // cmd[5]/cmd[6] == 0x7FFF0000 → clamp to MAX. WA uses these to draw | |
| // "infinite" rectangles in screen space. | |
| if read_field(cmd, 3) == i32::MIN { | |
| x1 = Fixed(i32::MIN); | |
| } | |
| if read_field(cmd, 4) == i32::MIN { | |
| y1 = Fixed(i32::MIN); | |
| } | |
| if read_field(cmd, 5) == 0x7FFF_0000 { | |
| x2 = Fixed(i32::MAX); | |
| } | |
| if read_field(cmd, 6) == 0x7FFF_0000 { | |
| y2 = Fixed(i32::MAX); | |
| } | |
| let color = *(cmd.add(2)); | |
| DisplayGfx::fill_rect_raw( | |
| display, | |
| x1.to_int(), | |
| y1.to_int(), | |
| x2.to_int(), | |
| y2.to_int(), | |
| color, | |
| ); | |
| } | |
| // ---- case 1: DRAW_BITMAP_GLOBAL → slot 20 (draw_scaled_sprite) ------------- | |
| // | |
| // vtable[20](this, cmd[2], cmd[3], cmd[4], cmd[5], cmd[6], cmd[7], cmd[8], cmd[9]); | |
| // | |
| // Pure passthrough — global bitmaps draw at the supplied world coords. | |
| unsafe fn dispatch_case_1_bitmap_global(display: *mut DisplayGfx, cmd: *const u32) { | |
| use crate::bitgrid::DisplayBitGrid; | |
| let x = read_fixed(cmd, 2); | |
| let y = read_fixed(cmd, 3); | |
| let sprite = read_field(cmd, 4) as *mut DisplayBitGrid; | |
| let src_x = read_field(cmd, 5); | |
| let src_y = read_field(cmd, 6); | |
| let src_w = read_field(cmd, 7); | |
| let src_h = read_field(cmd, 8); | |
| let flags = *(cmd.add(9)); | |
| DisplayGfx::draw_scaled_sprite_raw(display, x, y, sprite, src_x, src_y, src_w, src_h, flags); | |
| } | |
| // ---- case 2: DRAW_TEXTBOX_LOCAL → slot 20 (draw_scaled_sprite) ------------- | |
| // | |
| // `cmd[2] == 0` selects the simple variant (one clip call); otherwise the | |
| // box has separate top-edge / bottom-edge clip targets and a flag byte at | |
| // `cmd+8` controlling whether the result clamps to the second clip pair. | |
| unsafe fn dispatch_case_2_textbox_local( | |
| display: *mut DisplayGfx, | |
| clip: &ClipContext, | |
| cmd: *const u32, | |
| ) { | |
| use crate::bitgrid::DisplayBitGrid; | |
| let mode = *(cmd.add(2)); | |
| let mut x1 = Fixed::ZERO; | |
| let mut y1 = Fixed::ZERO; | |
| let mut scale = Fixed::ZERO; | |
| if mode == 0 { | |
| if !rq_clip_coordinates( | |
| clip, | |
| read_fixed(cmd, 3), | |
| read_fixed(cmd, 4), | |
| read_field(cmd, 5), | |
| &mut x1, | |
| &mut y1, | |
| &mut scale, | |
| ) { | |
| return; | |
| } | |
| } else { | |
| if !rq_clip_coordinates( | |
| clip, | |
| read_fixed(cmd, 3), | |
| read_fixed(cmd, 4), | |
| read_field(cmd, 5), | |
| &mut x1, | |
| &mut y1, | |
| &mut scale, | |
| ) { | |
| return; | |
| } | |
| let mut x2 = Fixed::ZERO; | |
| let mut y2 = Fixed::ZERO; | |
| if !rq_clip_coordinates( | |
| clip, | |
| read_fixed(cmd, 3), | |
| read_fixed(cmd, 4), | |
| read_field(cmd, 6), | |
| &mut x2, | |
| &mut y2, | |
| &mut scale, | |
| ) { | |
| return; | |
| } | |
| let flag_byte = *(cmd.add(2)) as u8; | |
| if flag_byte & 1 != 0 { | |
| x1 = x2; | |
| } | |
| if flag_byte & 2 != 0 { | |
| y1 = y2; | |
| } | |
| } | |
| let sprite = read_field(cmd, 7) as *mut DisplayBitGrid; | |
| let src_x = read_field(cmd, 8); | |
| let src_y = read_field(cmd, 9); | |
| let src_w = read_field(cmd, 10); | |
| let src_h = read_field(cmd, 11); | |
| let flags = *(cmd.add(12)); | |
| DisplayGfx::draw_scaled_sprite_raw(display, x1, y1, sprite, src_x, src_y, src_w, src_h, flags); | |
| } | |
| // ---- case 3 → slot 21 (draw_via_callback) ---------------------------------- | |
| // | |
| // iVar4 = RQ_ClipCoordinates(this, cmd[2], cmd[3], cmd[4], &x, &y, ..); | |
| // if !iVar4 break; | |
| // vtable[21](this, x, y, cmd[6], cmd[7], cmd[8]); | |
| unsafe fn dispatch_case_3_via_callback( | |
| display: *mut DisplayGfx, | |
| clip: &ClipContext, | |
| cmd: *const u32, | |
| ) { | |
| let mut x = Fixed::ZERO; | |
| let mut y = Fixed::ZERO; | |
| let mut scale = Fixed::ZERO; | |
| if !rq_clip_coordinates( | |
| clip, | |
| read_fixed(cmd, 2), | |
| read_fixed(cmd, 3), | |
| read_field(cmd, 4), | |
| &mut x, | |
| &mut y, | |
| &mut scale, | |
| ) { | |
| return; | |
| } | |
| let obj = read_field(cmd, 6) as *mut u8; | |
| let p5 = *(cmd.add(7)); | |
| let p6 = *(cmd.add(8)); | |
| DisplayGfx::draw_via_callback_raw(display, x, y, obj, p5, p6); | |
| } | |
| // ---- case 4: DRAW_SPRITE_GLOBAL → slot 19 (blit_sprite) -------------------- | |
| // | |
| // vtable[19](this, cmd[2], cmd[3], cmd[4], cmd[5]); | |
| unsafe fn dispatch_case_4_sprite_global(display: *mut DisplayGfx, cmd: *const u32) { | |
| let x = read_fixed(cmd, 2); | |
| let y = read_fixed(cmd, 3); | |
| let sprite = SpriteOp(*(cmd.add(4))); | |
| let palette = *(cmd.add(5)); | |
| DisplayGfx::blit_sprite_raw(display, x, y, sprite, palette); | |
| } | |
| // ---- case 5: DRAW_SPRITE_LOCAL → slot 19 (blit_sprite) --------------------- | |
| // | |
| // RQ_TranslateCoordinates(clip, cmd[2], cmd[3], &x, &y); | |
| // vtable[19](this, x, y, cmd[4], cmd[5]); | |
| unsafe fn dispatch_case_5_sprite_local( | |
| display: *mut DisplayGfx, | |
| clip: &ClipContext, | |
| cmd: *const u32, | |
| ) { | |
| let mut x = Fixed::ZERO; | |
| let mut y = Fixed::ZERO; | |
| rq_translate_coordinates(clip, read_fixed(cmd, 2), read_fixed(cmd, 3), &mut x, &mut y); | |
| let sprite = SpriteOp(*(cmd.add(4))); | |
| let palette = *(cmd.add(5)); | |
| DisplayGfx::blit_sprite_raw(display, x, y, sprite, palette); | |
| } | |
| // ---- case 6: DRAW_SPRITE_OFFSET → slot 19 (blit_sprite) -------------------- | |
| // | |
| // `cmd[2]` is a flag byte: bit 0/1 are top/bottom-edge clamps, bit 2 selects | |
| // the perspective-with-pivot helper instead of the plain perspective clip. | |
| // `cmd[2] == 0` selects the simple translate-only path used for purely | |
| // screen-space offset sprites. | |
| unsafe fn dispatch_case_6_sprite_offset( | |
| display: *mut DisplayGfx, | |
| clip: &ClipContext, | |
| cmd: *const u32, | |
| ) { | |
| let flags = *(cmd.add(2)); | |
| let mut x = Fixed::ZERO; | |
| let mut y = Fixed::ZERO; | |
| let mut scale = Fixed::ZERO; | |
| if flags == 0 { | |
| // Simple translate path (no perspective). | |
| rq_translate_coordinates(clip, read_fixed(cmd, 3), read_fixed(cmd, 4), &mut x, &mut y); | |
| } else { | |
| // Two clip calls (top + bottom Y) with optional MIN/MAX clamps. | |
| let use_pivot = (flags & 4) != 0; | |
| let clip_fn: fn( | |
| &ClipContext, | |
| Fixed, | |
| Fixed, | |
| i32, | |
| &mut Fixed, | |
| &mut Fixed, | |
| &mut Fixed, | |
| ) -> bool = if use_pivot { | |
| rq_clip_coordinates_with_ref | |
| } else { | |
| rq_clip_coordinates | |
| }; | |
| if !clip_fn( | |
| clip, | |
| read_fixed(cmd, 3), | |
| read_fixed(cmd, 4), | |
| read_field(cmd, 5), | |
| &mut x, | |
| &mut y, | |
| &mut scale, | |
| ) { | |
| return; | |
| } | |
| let mut x2 = Fixed::ZERO; | |
| let mut y2 = Fixed::ZERO; | |
| if !clip_fn( | |
| clip, | |
| read_fixed(cmd, 3), | |
| read_fixed(cmd, 4), | |
| read_field(cmd, 6), | |
| &mut x2, | |
| &mut y2, | |
| &mut scale, | |
| ) { | |
| return; | |
| } | |
| let flag_byte = *(cmd.add(2)) as u8; | |
| if flag_byte & 1 != 0 { | |
| x = x2; | |
| } | |
| if flag_byte & 2 != 0 { | |
| y = y2; | |
| } | |
| } | |
| let sprite = SpriteOp(*(cmd.add(7))); | |
| let palette = *(cmd.add(8)); | |
| DisplayGfx::blit_sprite_raw(display, x, y, sprite, palette); | |
| } | |
| // ---- case 7 → slot 12 (draw_polyline) -------------------------------------- | |
| // | |
| // vtable[12](this, &cmd[4], cmd[2], cmd[3]); | |
| // | |
| // `cmd[2]` is the vertex count, `cmd[3]` is the color, vertex data | |
| // follows starting at `cmd[4]`. | |
| unsafe fn dispatch_case_7_polyline(display: *mut DisplayGfx, cmd: *const u32) { | |
| let points = cmd.add(4) as *mut i32; | |
| let count = read_field(cmd, 2); | |
| let color = *(cmd.add(3)); | |
| DisplayGfx::draw_polyline_raw(display, points, count, color); | |
| } | |
| // ---- case 8: DRAW_LINE_STRIP → slot 14 (draw_line_clipped) ----------------- | |
| // | |
| // First vertex at `cmd[4..6]` plus `cmd[6]` (z), then iterate | |
| // `(count - 1)` more vertices and call slot 14 for each segment. | |
| // `cmd[3]` is the color shared by all segments. | |
| unsafe fn dispatch_case_8_line_strip( | |
| display: *mut DisplayGfx, | |
| clip: &ClipContext, | |
| cmd: *const u32, | |
| ) { | |
| let mut x1 = Fixed::ZERO; | |
| let mut y1 = Fixed::ZERO; | |
| let mut scale = Fixed::ZERO; | |
| if !rq_clip_coordinates( | |
| clip, | |
| read_fixed(cmd, 4), | |
| read_fixed(cmd, 5), | |
| read_field(cmd, 6), | |
| &mut x1, | |
| &mut y1, | |
| &mut scale, | |
| ) { | |
| return; | |
| } | |
| let count = read_field(cmd, 2); | |
| let color = *(cmd.add(3)); | |
| if count <= 1 { | |
| return; | |
| } | |
| // Vertex N is at cmd[8 + (N-1)*3 .. 8 + (N-1)*3 + 3], a 3-tuple of (x, y, z). | |
| // Walk N = 1..count. | |
| let mut vert = cmd.add(8) as *const u32; | |
| for _ in 1..count { | |
| let mut x2 = Fixed::ZERO; | |
| let mut y2 = Fixed::ZERO; | |
| if !rq_clip_coordinates( | |
| clip, | |
| Fixed::from_raw(*(vert.sub(1)) as i32), | |
| Fixed::from_raw(*vert as i32), | |
| *(vert.add(1)) as i32, | |
| &mut x2, | |
| &mut y2, | |
| &mut scale, | |
| ) { | |
| break; | |
| } | |
| DisplayGfx::draw_line_clipped_raw(display, x1, y1, x2, y2, color); | |
| x1 = x2; | |
| y1 = y2; | |
| vert = vert.add(3); | |
| } | |
| } | |
| // ---- case 9: DRAW_POLYGON → slot 13 (draw_line) ---------------------------- | |
| // | |
| // Same shape as case 8 but with two color params (cmd[3], cmd[4]) and the | |
| // first vertex starts at `cmd[5..8]`. Per-segment vertex stride is 3 u32s. | |
| unsafe fn dispatch_case_9_polygon(display: *mut DisplayGfx, clip: &ClipContext, cmd: *const u32) { | |
| let mut x1 = Fixed::ZERO; | |
| let mut y1 = Fixed::ZERO; | |
| let mut scale = Fixed::ZERO; | |
| if !rq_clip_coordinates( | |
| clip, | |
| read_fixed(cmd, 5), | |
| read_fixed(cmd, 6), | |
| read_field(cmd, 7), | |
| &mut x1, | |
| &mut y1, | |
| &mut scale, | |
| ) { | |
| return; | |
| } | |
| let count = read_field(cmd, 2); | |
| let color1 = *(cmd.add(3)); | |
| let color2 = *(cmd.add(4)); | |
| if count <= 1 { | |
| return; | |
| } | |
| let mut vert = cmd.add(9) as *const u32; | |
| for _ in 1..count { | |
| let mut x2 = Fixed::ZERO; | |
| let mut y2 = Fixed::ZERO; | |
| if !rq_clip_coordinates( | |
| clip, | |
| Fixed::from_raw(*(vert.sub(1)) as i32), | |
| Fixed::from_raw(*vert as i32), | |
| *(vert.add(1)) as i32, | |
| &mut x2, | |
| &mut y2, | |
| &mut scale, | |
| ) { | |
| break; | |
| } | |
| DisplayGfx::draw_line_raw(display, x1, y1, x2, y2, color1, color2); | |
| x1 = x2; | |
| y1 = y2; | |
| vert = vert.add(3); | |
| } | |
| } | |
| // ---- case 0xA → slot 15 (draw_pixel_strip) --------------------------------- | |
| // | |
| // RQ_TranslateCoordinates(clip, cmd[2], cmd[3], &x, &y); | |
| // // The translate's `clip_sub_saturate` floor()s its inputs, so its | |
| // // output drops the fractional 16 bits of cmd[2]/cmd[3]. The dispatcher | |
| // // re-adds those fractional bits back to recover pixel-fractional precision: | |
| // x += cmd[2] & 0xFFFF; | |
| // y += cmd[3] & 0xFFFF; | |
| // vtable[15](this, x, y, cmd[4], cmd[5], cmd[6], cmd[7]); | |
| unsafe fn dispatch_case_a_pixel_strip( | |
| display: *mut DisplayGfx, | |
| clip: &ClipContext, | |
| cmd: *const u32, | |
| ) { | |
| let mut x = Fixed::ZERO; | |
| let mut y = Fixed::ZERO; | |
| rq_translate_coordinates(clip, read_fixed(cmd, 2), read_fixed(cmd, 3), &mut x, &mut y); | |
| let cmd_x = *(cmd.add(2)); | |
| let cmd_y = *(cmd.add(3)); | |
| x = x + Fixed::from_raw((cmd_x & 0xFFFF) as i32); | |
| y = y + Fixed::from_raw((cmd_y & 0xFFFF) as i32); | |
| let dx = read_fixed(cmd, 4); | |
| let dy = read_fixed(cmd, 5); | |
| let count = read_field(cmd, 6); | |
| let color = *(cmd.add(7)); | |
| DisplayGfx::draw_pixel_strip_raw(display, x, y, dx, dy, count, color); | |
| } | |
| // ---- case 0xB: DRAW_CROSSHAIR → slot 16 (draw_crosshair) ------------------- | |
| // | |
| // iVar4 = RQ_ClipCoordinates(this, cmd[4], cmd[5], cmd[6], &x, &y, ..); | |
| // if !iVar4 break; | |
| // vtable[16](this, x>>16, y>>16, cmd[2], cmd[3]); | |
| unsafe fn dispatch_case_b_crosshair(display: *mut DisplayGfx, clip: &ClipContext, cmd: *const u32) { | |
| let mut x = Fixed::ZERO; | |
| let mut y = Fixed::ZERO; | |
| let mut scale = Fixed::ZERO; | |
| if !rq_clip_coordinates( | |
| clip, | |
| read_fixed(cmd, 4), | |
| read_fixed(cmd, 5), | |
| read_field(cmd, 6), | |
| &mut x, | |
| &mut y, | |
| &mut scale, | |
| ) { | |
| return; | |
| } | |
| let color_fg = *(cmd.add(2)); | |
| let color_bg = *(cmd.add(3)); | |
| DisplayGfx::draw_crosshair_raw(display, x.to_int(), y.to_int(), color_fg, color_bg); | |
| } | |
| // ---- case 0xC → slot 17 (draw_outlined_pixel) ------------------------------ | |
| // | |
| // iVar4 = RQ_ClipCoordinates(this, cmd[4], cmd[5], cmd[6], &x, &y, ..); | |
| // if !iVar4 break; | |
| // vtable[17](this, x>>16, y>>16, cmd[2], cmd[3]); | |
| unsafe fn dispatch_case_c_outlined_pixel( | |
| display: *mut DisplayGfx, | |
| clip: &ClipContext, | |
| cmd: *const u32, | |
| ) { | |
| let mut x = Fixed::ZERO; | |
| let mut y = Fixed::ZERO; | |
| let mut scale = Fixed::ZERO; | |
| if !rq_clip_coordinates( | |
| clip, | |
| read_fixed(cmd, 4), | |
| read_fixed(cmd, 5), | |
| read_field(cmd, 6), | |
| &mut x, | |
| &mut y, | |
| &mut scale, | |
| ) { | |
| return; | |
| } | |
| let color_fg = *(cmd.add(2)); | |
| let color_bg = read_field(cmd, 3); | |
| DisplayGfx::draw_outlined_pixel_raw(display, x.to_int(), y.to_int(), color_fg, color_bg); | |
| } | |
| // ---- case 0xD: DRAW_TILED_BITMAP → slot 11 (draw_tiled_bitmap) ------------- | |
| // | |
| // iVar4 = RQ_ClipCoordinates(this, 0, cmd[2], cmd[3], &x, &y, ..); | |
| // if !iVar4 break; | |
| // // Bit 0 of the flag byte at cmd[5] forces dest_x to 0 instead of x>>16. | |
| // vtable[11](this, (flag ? 0 : x>>16), y>>16, cmd[4]); | |
| unsafe fn dispatch_case_d_tiled_bitmap( | |
| display: *mut DisplayGfx, | |
| clip: &ClipContext, | |
| cmd: *const u32, | |
| ) { | |
| use crate::render::display::vtable::TiledBitmapSource; | |
| let mut x = Fixed::ZERO; | |
| let mut y = Fixed::ZERO; | |
| let mut scale = Fixed::ZERO; | |
| if !rq_clip_coordinates( | |
| clip, | |
| Fixed::ZERO, | |
| read_fixed(cmd, 2), | |
| read_field(cmd, 3), | |
| &mut x, | |
| &mut y, | |
| &mut scale, | |
| ) { | |
| return; | |
| } | |
| let flag_byte = *(cmd.add(5)) as u8; | |
| let dest_x = if flag_byte != 0 { 0 } else { x.to_int() }; | |
| let source = read_field(cmd, 4) as *const TiledBitmapSource; | |
| DisplayGfx::draw_tiled_bitmap_raw(display, dest_x, y.to_int(), source); | |
| } | |
| // ---- case 0xE → slot 22 (draw_tiled_terrain) ------------------------------- | |
| // | |
| // Same shape as case 2: `cmd[2] == 0` selects the one-clip variant; non-zero | |
| // runs the two-clip path with the same flag-byte clamps. | |
| unsafe fn dispatch_case_e_tiled_terrain( | |
| display: *mut DisplayGfx, | |
| clip: &ClipContext, | |
| cmd: *const u32, | |
| ) { | |
| let mode = *(cmd.add(2)); | |
| let mut x = Fixed::ZERO; | |
| let mut y = Fixed::ZERO; | |
| let mut scale = Fixed::ZERO; | |
| if mode == 0 { | |
| if !rq_clip_coordinates( | |
| clip, | |
| read_fixed(cmd, 3), | |
| read_fixed(cmd, 4), | |
| read_field(cmd, 5), | |
| &mut x, | |
| &mut y, | |
| &mut scale, | |
| ) { | |
| return; | |
| } | |
| } else { | |
| if !rq_clip_coordinates( | |
| clip, | |
| read_fixed(cmd, 3), | |
| read_fixed(cmd, 4), | |
| read_field(cmd, 5), | |
| &mut x, | |
| &mut y, | |
| &mut scale, | |
| ) { | |
| return; | |
| } | |
| let mut x2 = Fixed::ZERO; | |
| let mut y2 = Fixed::ZERO; | |
| if !rq_clip_coordinates( | |
| clip, | |
| read_fixed(cmd, 3), | |
| read_fixed(cmd, 4), | |
| read_field(cmd, 6), | |
| &mut x2, | |
| &mut y2, | |
| &mut scale, | |
| ) { | |
| return; | |
| } | |
| let flag_byte = *(cmd.add(2)) as u8; | |
| if flag_byte & 1 != 0 { | |
| x = x2; | |
| } | |
| if flag_byte & 2 != 0 { | |
| y = y2; | |
| } | |
| } | |
| let count = read_field(cmd, 7); | |
| let flags = *(cmd.add(8)); | |
| DisplayGfx::draw_tiled_terrain_raw(display, x, y, count, flags); | |
| } | |
| // ============================================================================= | |
| // Typed message dispatcher | |
| // ============================================================================= | |
| /// Dispatch a [`RenderMessage`] to the appropriate `DisplayGfx` vtable method. | |
| unsafe fn dispatch_typed(display: *mut DisplayGfx, clip: &ClipContext, msg: &RenderMessage) { | |
| match *msg { | |
| RenderMessage::Sprite { | |
| local, | |
| x, | |
| y, | |
| sprite, | |
| palette, | |
| } => { | |
| if local { | |
| let mut out_x = Fixed::ZERO; | |
| let mut out_y = Fixed::ZERO; | |
| rq_translate_coordinates(clip, x, y, &mut out_x, &mut out_y); | |
| DisplayGfx::blit_sprite_raw(display, out_x, out_y, sprite, palette); | |
| } else { | |
| DisplayGfx::blit_sprite_raw(display, x, y, sprite, palette); | |
| } | |
| } | |
| RenderMessage::FillRect { | |
| color, | |
| x1, | |
| y1, | |
| x2, | |
| y2, | |
| ref_z, | |
| } => { | |
| let mut ox1 = Fixed::ZERO; | |
| let mut oy1 = Fixed::ZERO; | |
| let mut ox2 = Fixed::ZERO; | |
| let mut oy2 = Fixed::ZERO; | |
| let mut scale = Fixed::ZERO; | |
| if !rq_clip_coordinates(clip, x1, y1, ref_z, &mut ox1, &mut oy1, &mut scale) { | |
| return; | |
| } | |
| if !rq_clip_coordinates(clip, x2, y2, ref_z, &mut ox2, &mut oy2, &mut scale) { | |
| return; | |
| } | |
| // Sentinel clamps for "infinite" screen-space rectangles. | |
| if x1.0 == i32::MIN { | |
| ox1 = Fixed(i32::MIN); | |
| } | |
| if y1.0 == i32::MIN { | |
| oy1 = Fixed(i32::MIN); | |
| } | |
| if x2.0 == 0x7FFF_0000 { | |
| ox2 = Fixed(i32::MAX); | |
| } | |
| if y2.0 == 0x7FFF_0000 { | |
| oy2 = Fixed(i32::MAX); | |
| } | |
| DisplayGfx::fill_rect_raw( | |
| display, | |
| ox1.to_int(), | |
| oy1.to_int(), | |
| ox2.to_int(), | |
| oy2.to_int(), | |
| color, | |
| ); | |
| } | |
| RenderMessage::Crosshair { | |
| color_fg, | |
| color_bg, | |
| x, | |
| y, | |
| } => { | |
| let mut ox = Fixed::ZERO; | |
| let mut oy = Fixed::ZERO; | |
| let mut scale = Fixed::ZERO; | |
| if !rq_clip_coordinates(clip, x, y, 0, &mut ox, &mut oy, &mut scale) { | |
| return; | |
| } | |
| DisplayGfx::draw_crosshair_raw(display, ox.to_int(), oy.to_int(), color_fg, color_bg); | |
| } | |
| RenderMessage::TiledBitmap { | |
| x, | |
| y, | |
| source, | |
| flags, | |
| } => { | |
| let mut ox = Fixed::ZERO; | |
| let mut oy = Fixed::ZERO; | |
| let mut scale = Fixed::ZERO; | |
| if !rq_clip_coordinates(clip, Fixed::ZERO, x, y.0, &mut ox, &mut oy, &mut scale) { | |
| return; | |
| } | |
| let dest_x = if flags & 1 != 0 { 0 } else { ox.to_int() }; | |
| DisplayGfx::draw_tiled_bitmap_raw(display, dest_x, oy.to_int(), source); | |
| } | |
| RenderMessage::SpriteOffset { | |
| flags, | |
| x, | |
| y, | |
| ref_z_2, | |
| sprite, | |
| palette, | |
| } => { | |
| let mut ox = Fixed::ZERO; | |
| let mut oy = Fixed::ZERO; | |
| let mut scale = Fixed::ZERO; | |
| if flags == 0 { | |
| rq_translate_coordinates(clip, x, y, &mut ox, &mut oy); | |
| } else { | |
| let use_pivot = (flags & 4) != 0; | |
| let clip_fn: fn( | |
| &ClipContext, | |
| Fixed, | |
| Fixed, | |
| i32, | |
| &mut Fixed, | |
| &mut Fixed, | |
| &mut Fixed, | |
| ) -> bool = if use_pivot { | |
| rq_clip_coordinates_with_ref | |
| } else { | |
| rq_clip_coordinates | |
| }; | |
| // ref_z (first Z reference) is always 0. | |
| if !clip_fn(clip, x, y, 0, &mut ox, &mut oy, &mut scale) { | |
| return; | |
| } | |
| let mut ox2 = Fixed::ZERO; | |
| let mut oy2 = Fixed::ZERO; | |
| if !clip_fn(clip, x, y, ref_z_2, &mut ox2, &mut oy2, &mut scale) { | |
| return; | |
| } | |
| if flags & 1 != 0 { | |
| ox = ox2; | |
| } | |
| if flags & 2 != 0 { | |
| oy = oy2; | |
| } | |
| } | |
| DisplayGfx::blit_sprite_raw(display, ox, oy, sprite, palette); | |
| } | |
| RenderMessage::BitmapGlobal { | |
| x, | |
| y, | |
| bitmap, | |
| src_y, | |
| src_w, | |
| src_h, | |
| flags, | |
| } => { | |
| // src_x is always 0 in all known producers. | |
| DisplayGfx::draw_scaled_sprite_raw( | |
| display, x, y, bitmap, 0, src_y, src_w, src_h, flags, | |
| ); | |
| } | |
| RenderMessage::TextboxLocal { | |
| x, | |
| y, | |
| bitmap, | |
| src_w, | |
| src_h, | |
| flags, | |
| } => { | |
| // mode is always 0, so simple one-clip path. | |
| // ref_z is always 0. | |
| let mut ox = Fixed::ZERO; | |
| let mut oy = Fixed::ZERO; | |
| let mut scale = Fixed::ZERO; | |
| if !rq_clip_coordinates(clip, x, y, 0, &mut ox, &mut oy, &mut scale) { | |
| return; | |
| } | |
| // src_x and src_y are always 0. | |
| DisplayGfx::draw_scaled_sprite_raw(display, ox, oy, bitmap, 0, 0, src_w, src_h, flags); | |
| } | |
| RenderMessage::LineStrip { | |
| count, | |
| color, | |
| vertices, | |
| } => { | |
| if count <= 1 { | |
| return; | |
| } | |
| let verts = core::slice::from_raw_parts(vertices, count as usize); | |
| let mut x1 = Fixed::ZERO; | |
| let mut y1 = Fixed::ZERO; | |
| let mut scale = Fixed::ZERO; | |
| if !rq_clip_coordinates( | |
| clip, | |
| Fixed::from_raw(verts[0][0]), | |
| Fixed::from_raw(verts[0][1]), | |
| verts[0][2], | |
| &mut x1, | |
| &mut y1, | |
| &mut scale, | |
| ) { | |
| return; | |
| } | |
| for v in &verts[1..] { | |
| let mut x2 = Fixed::ZERO; | |
| let mut y2 = Fixed::ZERO; | |
| if !rq_clip_coordinates( | |
| clip, | |
| Fixed::from_raw(v[0]), | |
| Fixed::from_raw(v[1]), | |
| v[2], | |
| &mut x2, | |
| &mut y2, | |
| &mut scale, | |
| ) { | |
| break; | |
| } | |
| DisplayGfx::draw_line_clipped_raw(display, x1, y1, x2, y2, color); | |
| x1 = x2; | |
| y1 = y2; | |
| } | |
| } | |
| RenderMessage::Polygon { | |
| count, | |
| color1, | |
| color2, | |
| vertices, | |
| } => { | |
| if count <= 1 { | |
| return; | |
| } | |
| let verts = core::slice::from_raw_parts(vertices, count as usize); | |
| let mut x1 = Fixed::ZERO; | |
| let mut y1 = Fixed::ZERO; | |
| let mut scale = Fixed::ZERO; | |
| if !rq_clip_coordinates( | |
| clip, | |
| Fixed::from_raw(verts[0][0]), | |
| Fixed::from_raw(verts[0][1]), | |
| verts[0][2], | |
| &mut x1, | |
| &mut y1, | |
| &mut scale, | |
| ) { | |
| return; | |
| } | |
| for v in &verts[1..] { | |
| let mut x2 = Fixed::ZERO; | |
| let mut y2 = Fixed::ZERO; | |
| if !rq_clip_coordinates( | |
| clip, | |
| Fixed::from_raw(v[0]), | |
| Fixed::from_raw(v[1]), | |
| v[2], | |
| &mut x2, | |
| &mut y2, | |
| &mut scale, | |
| ) { | |
| break; | |
| } | |
| DisplayGfx::draw_line_raw(display, x1, y1, x2, y2, color1, color2); | |
| x1 = x2; | |
| y1 = y2; | |
| } | |
| } | |
| } | |
| } | |
| // ============================================================================= | |
| // Unit tests for the helpers | |
| // ============================================================================= | |
| mod tests { | |
| use super::*; | |
| fn zero_clip() -> ClipContext { | |
| ClipContext { | |
| cam_x: Fixed::ZERO, | |
| cam_y: Fixed::ZERO, | |
| pivot_x: Fixed::ZERO, | |
| pivot_y: Fixed::ZERO, | |
| } | |
| } | |
| fn perspective_scale_at_zero_z() { | |
| // 0x100_0000 / 0x100_0000 = 0x10000 (Fixed16 1.0) | |
| assert_eq!(perspective_scale(0x0100_0000), Fixed::ONE); | |
| } | |
| fn clip_sub_saturate_no_overflow() { | |
| // 5.0 - 2.0 = 3.0 in fixed16 | |
| assert_eq!( | |
| clip_sub_saturate(Fixed::from_int(5), Fixed::from_int(2)), | |
| Fixed::from_int(3) | |
| ); | |
| } | |
| fn clip_sub_saturate_underflow_clamps_to_min() { | |
| // x_in negative, cam positive, delta wraps positive → MIN | |
| assert_eq!( | |
| clip_sub_saturate(Fixed(i32::MIN), Fixed::from_raw(0x4000_0000)), | |
| Fixed(i32::MIN) | |
| ); | |
| } | |
| fn clip_sub_saturate_overflow_clamps_to_max() { | |
| // x_in positive, cam negative, delta wraps negative → MAX | |
| assert_eq!( | |
| clip_sub_saturate( | |
| Fixed::from_raw(0x4000_0000), | |
| Fixed::from_raw(i32::MIN + 0x10000) | |
| ), | |
| Fixed(i32::MAX) | |
| ); | |
| } | |
| fn clip_coordinates_at_z_zero_is_identity() { | |
| let clip = zero_clip(); | |
| let mut ox = Fixed::ZERO; | |
| let mut oy = Fixed::ZERO; | |
| let mut scale = Fixed::ZERO; | |
| let ok = rq_clip_coordinates( | |
| &clip, | |
| Fixed::from_int(5), | |
| Fixed::from_int(7), | |
| 0, | |
| &mut ox, | |
| &mut oy, | |
| &mut scale, | |
| ); | |
| assert!(ok); | |
| // scale = 0x100_0000 / 0x100_0000 = 0x10000 (1.0 in fixed16) | |
| assert_eq!(scale, Fixed::ONE); | |
| assert_eq!(ox, Fixed::from_int(5)); | |
| assert_eq!(oy, Fixed::from_int(7)); | |
| } | |
| fn clip_coordinates_negative_z_returns_false() { | |
| let clip = zero_clip(); | |
| let mut ox = Fixed::ZERO; | |
| let mut oy = Fixed::ZERO; | |
| let mut scale = Fixed::ZERO; | |
| let ok = rq_clip_coordinates( | |
| &clip, | |
| Fixed::from_int(5), | |
| Fixed::from_int(7), | |
| -0x100_0001, | |
| &mut ox, | |
| &mut oy, | |
| &mut scale, | |
| ); | |
| assert!(!ok); | |
| } | |
| fn translate_coordinates_camera_offset() { | |
| let clip = ClipContext { | |
| cam_x: Fixed::from_int(10), | |
| cam_y: Fixed::from_int(20), | |
| pivot_x: Fixed::ZERO, | |
| pivot_y: Fixed::ZERO, | |
| }; | |
| let mut ox = Fixed::ZERO; | |
| let mut oy = Fixed::ZERO; | |
| rq_translate_coordinates( | |
| &clip, | |
| Fixed::from_int(30), | |
| Fixed::from_int(50), | |
| &mut ox, | |
| &mut oy, | |
| ); | |
| assert_eq!(ox, Fixed::from_int(20)); | |
| assert_eq!(oy, Fixed::from_int(30)); | |
| } | |
| } | |