roxlap_core/raster_target.rs
1//! `RasterTarget` — a `Copy` borrowed view of the framebuffer +
2//! zbuffer as raw pointers, the compositing primitive shared by the
3//! DDA terrain renderer ([`crate::dda`]) and the DDA sprite raycaster
4//! ([`crate::dda_sprite`]).
5//!
6//! Holding `&'a mut [u32]` / `&'a mut [f32]` directly would force an
7//! exclusive borrow per instance, blocking the renderer's tile bands
8//! from running on multiple threads even though their pixel writes are
9//! disjoint. `RasterTarget` is constructed safely from exclusive slice
10//! borrows, then re-exposes the memory as raw pointers tied to lifetime
11//! `'a` via `PhantomData`. Once it exists it is the sole path to the
12//! underlying memory for the duration of `'a`.
13//!
14//! # Safety contract for parallel use
15//! Callers that copy a `RasterTarget` and pass copies to multiple
16//! threads MUST guarantee the threads collectively write to
17//! pairwise-disjoint pixel indices. The parallel DDA driver enforces
18//! this via per-band row ranges; single-threaded callers hold one copy
19//! and trivially satisfy the invariant.
20
21use std::marker::PhantomData;
22
23#[derive(Clone, Copy, Debug)]
24pub struct RasterTarget<'a> {
25 fb_ptr: *mut u32,
26 fb_len: usize,
27 zb_ptr: *mut f32,
28 zb_len: usize,
29 _marker: PhantomData<&'a mut [u32]>,
30}
31
32// SAFETY: `RasterTarget` is morally a borrowed mutable slice pair —
33// the same shape `&'a mut [u32]` / `&'a mut [f32]` would have, both of
34// which are `Send` when `T: Send`. Multi-thread safety is enforced
35// by the wedge / strip-disjoint write invariant (see struct doc).
36unsafe impl Send for RasterTarget<'_> {}
37
38// SAFETY: sharing `&RasterTarget` across threads exposes only the
39// raw pointers + lengths. Reading a pointer field is itself free of
40// data races; concurrent writes through the pointer are gated by
41// the disjoint-write invariant the caller upholds. Required so
42// `ScalarRasterizer: Sync`, which `rayon::par_iter_mut` needs to
43// share `&rasterizer` across the strip-parallel closures (R12.3.1).
44unsafe impl Sync for RasterTarget<'_> {}
45
46impl<'a> RasterTarget<'a> {
47 /// Build a target from exclusive slice borrows. The slices are
48 /// consumed (their `&'a mut` reborrow is the load-bearing thing —
49 /// this constructor is the only way to mint a `RasterTarget`
50 /// from safe code).
51 #[must_use]
52 pub fn new(framebuffer: &'a mut [u32], zbuffer: &'a mut [f32]) -> Self {
53 Self {
54 fb_ptr: framebuffer.as_mut_ptr(),
55 fb_len: framebuffer.len(),
56 zb_ptr: zbuffer.as_mut_ptr(),
57 zb_len: zbuffer.len(),
58 _marker: PhantomData,
59 }
60 }
61
62 /// Framebuffer length in `u32` elements.
63 #[must_use]
64 pub fn fb_len(self) -> usize {
65 self.fb_len
66 }
67
68 /// Raw mutable framebuffer pointer. Used by SSE blocks that do
69 /// their own arithmetic + bounds reasoning.
70 ///
71 /// # Safety
72 /// Callers must respect `fb_len` and the parallel-use invariant.
73 #[must_use]
74 pub fn fb_ptr(self) -> *mut u32 {
75 self.fb_ptr
76 }
77
78 /// Raw mutable zbuffer pointer. Same contract as
79 /// [`Self::fb_ptr`].
80 #[must_use]
81 pub fn zb_ptr(self) -> *mut f32 {
82 self.zb_ptr
83 }
84
85 /// Write one ARGB pixel.
86 ///
87 /// # Safety
88 /// `idx < self.fb_len()`, plus the parallel-use invariant.
89 pub unsafe fn write_color(self, idx: usize, color: u32) {
90 debug_assert!(idx < self.fb_len, "fb idx {} >= len {}", idx, self.fb_len);
91 // SAFETY: caller asserts in-bounds + disjoint-from-other-threads.
92 unsafe { self.fb_ptr.add(idx).write(color) };
93 }
94
95 /// Write one z-buffer entry.
96 ///
97 /// # Safety
98 /// `idx < self.fb_len()` (zbuffer length matches fb), plus the
99 /// parallel-use invariant.
100 pub unsafe fn write_depth(self, idx: usize, z: f32) {
101 debug_assert!(idx < self.zb_len, "zb idx {} >= len {}", idx, self.zb_len);
102 // SAFETY: caller asserts in-bounds + disjoint-from-other-threads.
103 unsafe { self.zb_ptr.add(idx).write(z) };
104 }
105
106 /// Depth-tested write: store `(color, z)` only if `z` is strictly
107 /// closer (smaller) than the current z-buffer entry. Returns whether
108 /// the pixel was written. The compositing primitive the DDA sprite
109 /// raycaster uses to occlude sprites against terrain.
110 ///
111 /// # Safety
112 /// `idx < self.fb_len()`, plus the parallel-use invariant.
113 #[must_use]
114 pub unsafe fn z_test_write(self, idx: usize, color: u32, z: f32) -> bool {
115 debug_assert!(idx < self.zb_len, "zb idx {} >= len {}", idx, self.zb_len);
116 // SAFETY: caller asserts in-bounds + disjoint-from-other-threads.
117 unsafe {
118 if z < *self.zb_ptr.add(idx) {
119 self.fb_ptr.add(idx).write(color);
120 self.zb_ptr.add(idx).write(z);
121 true
122 } else {
123 false
124 }
125 }
126 }
127}