pico_rendering/render_pipeline/
_sprite_renderer.rs

1use pico_engine_hardware::Display;
2
3/// Draw sprite into frame buffer
4/// Dispatches to optimized rendering path based on sprite type
5#[inline]
6pub fn draw_sprite(
7    display: &mut impl Display,
8    data: &[u16],
9    x: i16,
10    y: i16,
11    width: usize,
12    height: usize,
13    is_opaque: bool,
14) {
15    // Early exit for completely off-screen sprites
16    if x >= display.get_screen_width() as i16
17        || y >= display.get_screen_height() as i16
18        || x + width as i16 <= 0
19        || y + height as i16 <= 0
20    {
21        return;
22    }
23
24    if is_opaque {
25        draw_sprite_opaque(display, data, x, y, width, height);
26    } else {
27        draw_sprite_strip_encoded(display, data, x, y, width, height);
28    }
29}
30
31/// Optimized opaque sprite rendering with aligned memory copy
32#[inline]
33fn draw_sprite_opaque(
34    display: &mut impl Display,
35    data: &[u16],
36    x: i16,
37    y: i16,
38    width: usize,
39    height: usize,
40) {
41    // Calculate clipping boundaries once
42    let start_y = if y < 0 { -y } else { 0 };
43    let end_y = ((y + height as i16).min(display.get_screen_height() as i16) - y).max(0) as usize;
44    let start_x = if x < 0 { -x } else { 0 };
45    let end_x = ((x + width as i16).min(display.get_screen_width() as i16) - x).max(0) as usize;
46
47    // Fast path: fully visible sprite with aligned copy
48    if start_y == 0 && end_y == height && start_x == 0 && end_x == width {
49        for row in 0..height {
50            let screen_y = (y + row as i16) as usize;
51            let dst_start = screen_y * display.get_screen_width() + x as usize;
52            let src_start = row * width;
53            unsafe {
54                core::ptr::copy_nonoverlapping(
55                    data.as_ptr().add(src_start),
56                    display.frame_buffer().as_mut_ptr().add(dst_start),
57                    width,
58                );
59            }
60        }
61    } else {
62        // Clipped sprite - row by row copy
63        for row in start_y as usize..end_y {
64            let screen_y = (y + row as i16) as usize;
65            let dst_start = screen_y * display.get_screen_width() + (x + start_x) as usize;
66            let src_start = row * width + start_x as usize;
67            let copy_len = end_x - start_x as usize;
68
69            unsafe {
70                core::ptr::copy_nonoverlapping(
71                    data.as_ptr().add(src_start),
72                    display.frame_buffer().as_mut_ptr().add(dst_start),
73                    copy_len,
74                );
75            }
76        }
77    }
78}
79
80/// Optimized strip-encoded sprite decoder with memcpy for contiguous runs
81/// Strip encoding format: u16 header (low byte = transparent count, high byte = opaque count)
82#[inline(never)]
83fn draw_sprite_strip_encoded(
84    display: &mut impl Display,
85    data: &[u16],
86    x: i16,
87    y: i16,
88    width: usize,
89    height: usize,
90) {
91    let mut data_idx = 0;
92    let mut row = 0;
93    let mut col = 0;
94    let data_len = data.len();
95    let fb_ptr = display.frame_buffer().as_mut_ptr();
96    let data_ptr = data.as_ptr();
97
98    unsafe {
99        while data_idx < data_len {
100            // Read strip header
101            let header = *data_ptr.add(data_idx);
102            data_idx += 1;
103
104            let transparent_count = (header & 0xFF) as usize;
105            let opaque_count = ((header >> 8) & 0xFF) as usize;
106
107            // Skip transparent pixels - update row/col without division
108            let mut skip = transparent_count;
109            while skip > 0 {
110                let available = width - col;
111                if skip >= available {
112                    skip -= available;
113                    col = 0;
114                    row += 1;
115                    if row >= height {
116                        return;
117                    }
118                } else {
119                    col += skip;
120                    break;
121                }
122            }
123
124            // Process opaque pixels in row-aligned batches
125            let mut remaining = opaque_count;
126
127            while remaining > 0 && row < height {
128                let pixels_in_row = width - col;
129                let batch_size = remaining.min(pixels_in_row);
130
131                let screen_x = x + col as i16;
132                let screen_y = y + row as i16;
133
134                // Check if entire batch is on screen
135                if screen_y >= 0
136                    && screen_y < display.get_screen_height() as i16
137                    && screen_x >= 0
138                    && screen_x + batch_size as i16 <= display.get_screen_width() as i16
139                {
140                    // Fast path: memcpy entire batch
141                    let fb_idx = screen_y as usize * display.get_screen_width() + screen_x as usize;
142                    core::ptr::copy_nonoverlapping(
143                        data_ptr.add(data_idx),
144                        fb_ptr.add(fb_idx),
145                        batch_size,
146                    );
147                } else if screen_y >= 0 && screen_y < display.get_screen_height() as i16 {
148                    // Partial on-screen: per-pixel bounds check
149                    for i in 0..batch_size {
150                        let px = screen_x + i as i16;
151                        if px >= 0 && px < display.get_screen_width() as i16 {
152                            let fb_idx =
153                                screen_y as usize * display.get_screen_width() + px as usize;
154                            *fb_ptr.add(fb_idx) = *data_ptr.add(data_idx + i);
155                        }
156                    }
157                }
158
159                data_idx += batch_size;
160                col += batch_size;
161                remaining -= batch_size;
162
163                if col >= width {
164                    col = 0;
165                    row += 1;
166                }
167            }
168        }
169    }
170}
171
172/// Shift frame buffer for small camera movements
173/// 
174/// Delta represents camera movement, buffer shifts in opposite direction
175/// For example, when camera moves right (delta_x > 0), buffer shifts left
176#[inline(never)]
177pub fn shift_buffer(display: &mut impl Display, delta_x: i16, delta_y: i16) {
178    if delta_x == 0 && delta_y == 0 {
179        return;
180    }
181
182    let fb_ptr = display.frame_buffer().as_mut_ptr();
183
184    unsafe {
185        if delta_y != 0 {
186            // Camera moves down -> buffer shifts up (content moves up on screen)
187            // Camera moves up -> buffer shifts down (content moves down on screen)
188            let shift_amount = delta_y.abs() as usize;
189            if delta_y > 0 {
190                // Camera down: shift buffer UP
191                for y in 0..(display.get_screen_height() - shift_amount) {
192                    let src_idx = (y + shift_amount) * display.get_screen_width();
193                    let dst_idx = y * display.get_screen_width();
194                    core::ptr::copy(
195                        fb_ptr.add(src_idx),
196                        fb_ptr.add(dst_idx),
197                        display.get_screen_width(),
198                    );
199                }
200            } else {
201                // Camera up: shift buffer DOWN
202                for y in (shift_amount..display.get_screen_height()).rev() {
203                    let src_idx = (y - shift_amount) * display.get_screen_width();
204                    let dst_idx = y * display.get_screen_width();
205                    core::ptr::copy(
206                        fb_ptr.add(src_idx),
207                        fb_ptr.add(dst_idx),
208                        display.get_screen_width(),
209                    );
210                }
211            }
212        }
213
214        if delta_x != 0 {
215            // Camera moves right -> buffer shifts left (content moves left on screen)
216            // Camera moves left -> buffer shifts right (content moves right on screen)
217            let shift_amount = delta_x.abs() as usize;
218            if delta_x > 0 {
219                // Camera right: shift buffer LEFT
220                for y in 0..display.get_screen_height() {
221                    let row_start = y * display.get_screen_width();
222                    core::ptr::copy(
223                        fb_ptr.add(row_start + shift_amount),
224                        fb_ptr.add(row_start),
225                        display.get_screen_width() - shift_amount,
226                    );
227                }
228            } else {
229                // Camera left: shift buffer RIGHT
230                for y in 0..display.get_screen_height() {
231                    let row_start = y * display.get_screen_width();
232                    core::ptr::copy(
233                        fb_ptr.add(row_start),
234                        fb_ptr.add(row_start + shift_amount),
235                        display.get_screen_width() - shift_amount,
236                    );
237                }
238            }
239        }
240    }
241}