pico_rendering/
_render_pipeline.rs

1use core::cmp::min;
2use pico_ecs::_component_storage::ComponentStorage;
3use pico_ecs::_world::World;
4use pico_transform::_pivot::Pivot;
5use pico_transform::_position::Position;
6use pico_transform::_size::Size;
7
8// ============================================================================
9// STRIP-BASED RENDERER SYSTEM
10// ============================================================================
11// Renders in horizontal strips to manage memory constraints
12use pico_engine_hardware::Display;
13
14use crate::components::_render_component::RendererComponent;
15use crate::types::_cell::Cell;
16use crate::types::_tile::Tile;
17use crate::types::_tile_map::TileMap;
18
19/// Render system - draws all entities
20pub struct RenderPipeline {
21    prev_cam_x: u16,
22    prev_cam_y: u16,
23    first_frame: bool,
24}
25
26impl RenderPipeline {
27    pub fn new() -> Self {
28        Self {
29            prev_cam_x: 0,
30            prev_cam_y: 0,
31            first_frame: true,
32        }
33    }
34
35    /// Run the render system - renders full screen
36    #[inline]
37    pub fn render_direct(
38        &mut self,
39        world: &World,
40        position_storage: &ComponentStorage<Position>,
41        renderer_storage: &ComponentStorage<RendererComponent>,
42        size_storage: &ComponentStorage<Size>,
43        pivot_storage: &ComponentStorage<Pivot>,
44        background_tilemap: &'static TileMap,
45        foreground_tilemap: &'static TileMap,
46        display: &mut impl Display,
47    ) {
48        // Draw background tiles first
49        self.draw_background(
50            world,
51            position_storage,
52            size_storage,
53            pivot_storage,
54            background_tilemap,
55            display,
56        );
57
58        // Then render entities
59        for (_entity, transform, renderer, size, pivot) in world.query4(
60            position_storage,
61            renderer_storage,
62            size_storage,
63            pivot_storage,
64        ) {
65            // Calculate sprite screen position (accounting for pivot and camera)
66            let sprite_x = transform.x as i16 - pivot.x as i16 - world.camera.x as i16;
67            let sprite_y = transform.y as i16 - pivot.y as i16 - world.camera.y as i16;
68
69            // Inline sprite drawing to avoid function call overhead
70            draw_sprite(
71                display,
72                &renderer.texture[renderer.sprite.data_start..renderer.sprite.data_end],
73                sprite_x,
74                sprite_y,
75                size.width as usize,
76                size.height as usize,
77                renderer.sprite.is_opaque,
78            );
79        }
80
81        // Finally draw foreground tiles (above entities)
82        self.draw_foreground(
83            world,
84            position_storage,
85            size_storage,
86            pivot_storage,
87            foreground_tilemap,
88            display,
89        );
90
91        // Update camera tracking after all layers are drawn
92        self.prev_cam_x = world.camera.x;
93        self.prev_cam_y = world.camera.y;
94        self.first_frame = false;
95
96        #[cfg(feature = "is_debug")]
97        {
98            let mut fps = 0;
99            if delta_time > 0 {
100                fps = 1000 / delta_time;
101            }
102            // Draw FPS counter on top
103            draw_fps(display, fps);
104        }
105        // Flush to display
106        display.display_buffer();
107    }
108
109    // Optimized background rendering with buffer shifting for small camera movements
110    #[inline(never)]
111    fn draw_background(
112        &mut self,
113        world: &World,
114        position_storage: &ComponentStorage<Position>,
115        size_storage: &ComponentStorage<Size>,
116        pivot_storage: &ComponentStorage<Pivot>,
117        tilemap: &'static TileMap,
118        display: &mut impl Display,
119    ) {
120        self.draw_tilemap(
121            world,
122            position_storage,
123            size_storage,
124            pivot_storage,
125            display,
126            tilemap,
127            true,  // Allow buffer shifting for background layer
128            false, // Background has no offset
129        );
130    }
131
132    // Optimized foreground rendering with buffer shifting for small camera movements
133    #[inline(never)]
134    fn draw_foreground(
135        &mut self,
136        world: &World,
137        position_storage: &ComponentStorage<Position>,
138        size_storage: &ComponentStorage<Size>,
139        pivot_storage: &ComponentStorage<Pivot>,
140        tilemap: &'static TileMap,
141        display: &mut impl Display,
142    ) {
143        self.draw_tilemap(
144            world,
145            position_storage,
146            size_storage,
147            pivot_storage,
148            display,
149            tilemap,
150            false, // Don't shift buffer for foreground (already shifted by background)
151            true,  // Foreground has -32 pixel offset
152        );
153    }
154
155    // Generic tilemap rendering with buffer shifting for small camera movements
156    #[inline(never)]
157    fn draw_tilemap(
158        &mut self,
159        world: &World,
160        position_storage: &ComponentStorage<Position>,
161        size_storage: &ComponentStorage<Size>,
162        pivot_storage: &ComponentStorage<Pivot>,
163        display: &mut impl Display,
164        tilemap: &'static TileMap,
165        allow_buffer_shift: bool,
166        is_foreground: bool,
167    ) {
168        const TILE_SIZE: u16 = 16;
169        const HORIZONTAL_TILES: u16 = 20;
170        const VERTICAL_TILES: u16 = 15;
171
172        let cam_x = world.camera.x;
173        let cam_y = world.camera.y;
174        let delta_x = cam_x as i16 - self.prev_cam_x as i16;
175        let delta_y = cam_y as i16 - self.prev_cam_y as i16;
176
177        // Camera not moving: only redraw tiles under entities
178        if delta_x == 0 && delta_y == 0 && !self.first_frame {
179            // Redraw tiles that intersect with entities to clear trails from movement
180            for (_entity, transform, size, pivot) in
181                world.query3(position_storage, size_storage, pivot_storage)
182            {
183                // Calculate entity bounding box in world coordinates
184                let entity_left = transform.x - pivot.x as u16;
185                let entity_top = transform.y - pivot.y as u16;
186                let entity_right = entity_left + size.width;
187                let entity_bottom = entity_top + size.height;
188
189                // Find tiles whose rendered area overlaps with entity
190                // Foreground tiles render from (y*16-16) to (y*16), so adjust calculation
191                let tile_start_x = entity_left / TILE_SIZE;
192                let tile_start_y = if is_foreground {
193                    if entity_top >= TILE_SIZE {
194                        (entity_top + TILE_SIZE - 1) / TILE_SIZE
195                    } else {
196                        0
197                    }
198                } else {
199                    entity_top / TILE_SIZE
200                };
201                let tile_end_x = min((entity_right + TILE_SIZE - 1) / TILE_SIZE, 30);
202                let tile_end_y = min(
203                    (entity_bottom + if is_foreground { TILE_SIZE } else { 0 } + TILE_SIZE - 1)
204                        / TILE_SIZE,
205                    30,
206                );
207
208                // Redraw overlapping tiles
209                for ty in tile_start_y..tile_end_y {
210                    for tx in tile_start_x..tile_end_x {
211                        self.draw_tile_column(
212                            tx,
213                            ty,
214                            cam_x as i16,
215                            cam_y as i16,
216                            display,
217                            tilemap.cells,
218                            tilemap.tiles,
219                            tilemap.texture,
220                            is_foreground,
221                        );
222                    }
223                }
224            }
225            return;
226        }
227
228        // Small scroll optimization: shift buffer and only draw new tiles
229        if allow_buffer_shift
230            && delta_x.abs() <= 2
231            && delta_y.abs() <= 2
232            && (delta_x != 0 || delta_y != 0)
233        {
234            shift_buffer(display, delta_x, delta_y);
235
236            // Calculate visible tile range - tiles that overlap with the screen
237            // Account for camera offset within tiles by expanding range by 1 in each direction
238            let start_x = cam_x / TILE_SIZE;
239            let start_y = cam_y / TILE_SIZE;
240            let end_x = min((cam_x + 320) / TILE_SIZE + 1, 30);
241            let end_y = min((cam_y + 240) / TILE_SIZE + 1, 30);
242
243            // Always redraw edge tiles after shifting to fill gaps from sub-tile positioning
244            if delta_x > 0 {
245                // Camera moved right -> redraw RIGHT edge (2 columns)
246                for y in start_y..end_y {
247                    if end_x > 0 {
248                        self.draw_tile_column(
249                            end_x - 1,
250                            y,
251                            cam_x as i16,
252                            cam_y as i16,
253                            display,
254                            tilemap.cells,
255                            tilemap.tiles,
256                            tilemap.texture,
257                            is_foreground,
258                        );
259                    }
260                    if end_x > 1 {
261                        self.draw_tile_column(
262                            end_x - 2,
263                            y,
264                            cam_x as i16,
265                            cam_y as i16,
266                            display,
267                            tilemap.cells,
268                            tilemap.tiles,
269                            tilemap.texture,
270                            is_foreground,
271                        );
272                    }
273                }
274            } else if delta_x < 0 {
275                // Camera moved left -> redraw LEFT edge (2 columns)
276                for y in start_y..end_y {
277                    self.draw_tile_column(
278                        start_x,
279                        y,
280                        cam_x as i16,
281                        cam_y as i16,
282                        display,
283                        tilemap.cells,
284                        tilemap.tiles,
285                        tilemap.texture,
286                        is_foreground,
287                    );
288                    if start_x + 1 < 30 {
289                        self.draw_tile_column(
290                            start_x + 1,
291                            y,
292                            cam_x as i16,
293                            cam_y as i16,
294                            display,
295                            tilemap.cells,
296                            tilemap.tiles,
297                            tilemap.texture,
298                            is_foreground,
299                        );
300                    }
301                }
302            }
303
304            // Always redraw edge tiles after shifting
305            if delta_y > 0 {
306                // Camera moved down -> redraw BOTTOM edge (2 rows)
307                for x in start_x..end_x {
308                    if end_y > 0 {
309                        self.draw_tile_column(
310                            x,
311                            end_y - 1,
312                            cam_x as i16,
313                            cam_y as i16,
314                            display,
315                            tilemap.cells,
316                            tilemap.tiles,
317                            tilemap.texture,
318                            is_foreground,
319                        );
320                    }
321                    if end_y > 1 {
322                        self.draw_tile_column(
323                            x,
324                            end_y - 2,
325                            cam_x as i16,
326                            cam_y as i16,
327                            display,
328                            tilemap.cells,
329                            tilemap.tiles,
330                            tilemap.texture,
331                            is_foreground,
332                        );
333                    }
334                }
335            } else if delta_y < 0 {
336                // Camera moved up -> redraw TOP edge (2 rows)
337                for x in start_x..end_x {
338                    self.draw_tile_column(
339                        x,
340                        start_y,
341                        cam_x as i16,
342                        cam_y as i16,
343                        display,
344                        tilemap.cells,
345                        tilemap.tiles,
346                        tilemap.texture,
347                        is_foreground,
348                    );
349                    if start_y + 1 < 30 {
350                        self.draw_tile_column(
351                            x,
352                            start_y + 1,
353                            cam_x as i16,
354                            cam_y as i16,
355                            display,
356                            tilemap.cells,
357                            tilemap.tiles,
358                            tilemap.texture,
359                            is_foreground,
360                        );
361                    }
362                }
363            }
364
365            // Redraw tiles that intersect with entities to clear trails
366            for (_entity, transform, size, pivot) in
367                world.query3(position_storage, size_storage, pivot_storage)
368            {
369                // Calculate entity bounding box in world coordinates
370                let entity_left = transform.x - pivot.x as u16;
371                let entity_top = transform.y - pivot.y as u16;
372                let entity_right = entity_left + size.width;
373                let entity_bottom = entity_top + size.height;
374
375                // Find tiles whose rendered area overlaps with entity
376                // Foreground tiles render from (y*16-16) to (y*16), so adjust calculation
377                let tile_start_x = entity_left / TILE_SIZE;
378                let tile_start_y = if is_foreground {
379                    if entity_top >= TILE_SIZE {
380                        (entity_top + TILE_SIZE - 1) / TILE_SIZE
381                    } else {
382                        0
383                    }
384                } else {
385                    entity_top / TILE_SIZE
386                };
387                let tile_end_x = min((entity_right + TILE_SIZE - 1) / TILE_SIZE, 30);
388                let tile_end_y = min(
389                    (entity_bottom + if is_foreground { TILE_SIZE } else { 0 } + TILE_SIZE - 1)
390                        / TILE_SIZE,
391                    30,
392                );
393
394                // Redraw overlapping tiles
395                for ty in tile_start_y..tile_end_y {
396                    for tx in tile_start_x..tile_end_x {
397                        self.draw_tile_column(
398                            tx,
399                            ty,
400                            cam_x as i16,
401                            cam_y as i16,
402                            display,
403                            tilemap.cells,
404                            tilemap.tiles,
405                            tilemap.texture,
406                            is_foreground,
407                        );
408                    }
409                }
410            }
411        } else {
412            // Large movement or first frame - draw everything
413            let start_x = cam_x / TILE_SIZE;
414            let start_y = cam_y / TILE_SIZE;
415            let end_x = min(start_x + HORIZONTAL_TILES + 1, 30);
416            let end_y = min(start_y + VERTICAL_TILES + 2, 30);
417
418            for y in start_y..end_y {
419                for x in start_x..end_x {
420                    self.draw_tile_column(
421                        x,
422                        y,
423                        cam_x as i16,
424                        cam_y as i16,
425                        display,
426                        tilemap.cells,
427                        tilemap.tiles,
428                        tilemap.texture,
429                        is_foreground,
430                    );
431                }
432            }
433        }
434    }
435
436    #[inline]
437    fn draw_tile_column(
438        &self,
439        x: u16,
440        y: u16,
441        cam_x: i16,
442        cam_y: i16,
443        display: &mut impl Display,
444        cells: &'static [Cell],
445        tiles: &'static [Tile],
446        texture: &'static [u16],
447        is_foreground: bool,
448    ) {
449        let cell_idx = (y as usize * 30) + x as usize;
450        if cell_idx >= cells.len() {
451            return;
452        }
453        let cell = &cells[cell_idx];
454        let cell_tiles = &tiles[cell.start..cell.end];
455
456        for tile in cell_tiles {
457            let sprite = tile.sprite;
458            // Tiles use grid positioning
459            // For foreground, adjust Y by -16 to compensate for data generation coordinate inversion
460            let pos_x = (x * 16) as i16 - cam_x;
461            let pos_y = (y * 16) as i16 - cam_y - if is_foreground { 16 } else { 0 };
462
463            draw_sprite(
464                display,
465                &texture[sprite.data_start..sprite.data_end],
466                pos_x,
467                pos_y,
468                16,
469                16,
470                sprite.is_opaque,
471            );
472        }
473    }
474}
475
476// Draw sprite into frame buffer
477#[inline]
478pub fn draw_sprite(
479    display: &mut impl Display,
480    data: &[u16],
481    x: i16,
482    y: i16,
483    width: usize,
484    height: usize,
485    is_opaque: bool,
486) {
487    // Early exit for completely off-screen sprites
488    if x >= display.get_screen_width() as i16
489        || y >= display.get_screen_height() as i16
490        || x + width as i16 <= 0
491        || y + height as i16 <= 0
492    {
493        return;
494    }
495
496    if is_opaque {
497        draw_sprite_opaque(display, data, x, y, width, height);
498    } else {
499        draw_sprite_strip_encoded(display, data, x, y, width, height);
500    }
501}
502
503// Optimized opaque sprite rendering
504#[inline]
505fn draw_sprite_opaque(
506    display: &mut impl Display,
507    data: &[u16],
508    x: i16,
509    y: i16,
510    width: usize,
511    height: usize,
512) {
513    // Calculate clipping boundaries once
514    let start_y = if y < 0 { -y } else { 0 };
515    let end_y = ((y + height as i16).min(display.get_screen_height() as i16) - y).max(0) as usize;
516    let start_x = if x < 0 { -x } else { 0 };
517    let end_x = ((x + width as i16).min(display.get_screen_width() as i16) - x).max(0) as usize;
518
519    // Fast path: fully visible sprite with aligned copy
520    if start_y == 0 && end_y == height && start_x == 0 && end_x == width {
521        for row in 0..height {
522            let screen_y = (y + row as i16) as usize;
523            let dst_start = screen_y * display.get_screen_width() + x as usize;
524            let src_start = row * width;
525            unsafe {
526                core::ptr::copy_nonoverlapping(
527                    data.as_ptr().add(src_start),
528                    display.frame_buffer().as_mut_ptr().add(dst_start),
529                    width,
530                );
531            }
532        }
533    } else {
534        // Clipped sprite - row by row copy
535        for row in start_y as usize..end_y {
536            let screen_y = (y + row as i16) as usize;
537            let dst_start = screen_y * display.get_screen_width() + (x + start_x) as usize;
538            let src_start = row * width + start_x as usize;
539            let copy_len = end_x - start_x as usize;
540
541            unsafe {
542                core::ptr::copy_nonoverlapping(
543                    data.as_ptr().add(src_start),
544                    display.frame_buffer().as_mut_ptr().add(dst_start),
545                    copy_len,
546                );
547            }
548        }
549    }
550}
551
552// Optimized strip-encoded sprite decoder with memcpy for contiguous runs
553#[inline(never)]
554fn draw_sprite_strip_encoded(
555    display: &mut impl Display,
556    data: &[u16],
557    x: i16,
558    y: i16,
559    width: usize,
560    height: usize,
561) {
562    let mut data_idx = 0;
563    let mut row = 0;
564    let mut col = 0;
565    let data_len = data.len();
566    let fb_ptr = display.frame_buffer().as_mut_ptr();
567    let data_ptr = data.as_ptr();
568
569    unsafe {
570        while data_idx < data_len {
571            // Read strip header
572            let header = *data_ptr.add(data_idx);
573            data_idx += 1;
574
575            let transparent_count = (header & 0xFF) as usize;
576            let opaque_count = ((header >> 8) & 0xFF) as usize;
577
578            // Skip transparent pixels - update row/col without division
579            let mut skip = transparent_count;
580            while skip > 0 {
581                let available = width - col;
582                if skip >= available {
583                    skip -= available;
584                    col = 0;
585                    row += 1;
586                    if row >= height {
587                        return;
588                    }
589                } else {
590                    col += skip;
591                    break;
592                }
593            }
594
595            // Process opaque pixels in row-aligned batches
596            let mut remaining = opaque_count;
597
598            while remaining > 0 && row < height {
599                let pixels_in_row = width - col;
600                let batch_size = remaining.min(pixels_in_row);
601
602                let screen_x = x + col as i16;
603                let screen_y = y + row as i16;
604
605                // Check if entire batch is on screen
606                if screen_y >= 0
607                    && screen_y < display.get_screen_height() as i16
608                    && screen_x >= 0
609                    && screen_x + batch_size as i16 <= display.get_screen_width() as i16
610                {
611                    // Fast path: memcpy entire batch
612                    let fb_idx = screen_y as usize * display.get_screen_width() + screen_x as usize;
613                    core::ptr::copy_nonoverlapping(
614                        data_ptr.add(data_idx),
615                        fb_ptr.add(fb_idx),
616                        batch_size,
617                    );
618                } else if screen_y >= 0 && screen_y < display.get_screen_height() as i16 {
619                    // Partial on-screen: per-pixel bounds check
620                    for i in 0..batch_size {
621                        let px = screen_x + i as i16;
622                        if px >= 0 && px < display.get_screen_width() as i16 {
623                            let fb_idx =
624                                screen_y as usize * display.get_screen_width() + px as usize;
625                            *fb_ptr.add(fb_idx) = *data_ptr.add(data_idx + i);
626                        }
627                    }
628                }
629
630                data_idx += batch_size;
631                col += batch_size;
632                remaining -= batch_size;
633
634                if col >= width {
635                    col = 0;
636                    row += 1;
637                }
638            }
639        }
640    }
641}
642
643// Draw FPS counter in top-left corner
644pub fn draw_fps(display: &mut impl Display, fps: u32) {
645    // Simple 3x5 digit font data (0-9)
646    const DIGIT_WIDTH: usize = 4;
647    const DIGIT_HEIGHT: usize = 7;
648    const DIGITS: [[u8; DIGIT_HEIGHT]; 10] = [
649        [0b1110, 0b1010, 0b1010, 0b1010, 0b1010, 0b1010, 0b1110], // 0
650        [0b0100, 0b1100, 0b0100, 0b0100, 0b0100, 0b0100, 0b1110], // 1
651        [0b1110, 0b0010, 0b0010, 0b1110, 0b1000, 0b1000, 0b1110], // 2
652        [0b1110, 0b0010, 0b0010, 0b1110, 0b0010, 0b0010, 0b1110], // 3
653        [0b1010, 0b1010, 0b1010, 0b1110, 0b0010, 0b0010, 0b0010], // 4
654        [0b1110, 0b1000, 0b1000, 0b1110, 0b0010, 0b0010, 0b1110], // 5
655        [0b1110, 0b1000, 0b1000, 0b1110, 0b1010, 0b1010, 0b1110], // 6
656        [0b1110, 0b0010, 0b0010, 0b0010, 0b0010, 0b0010, 0b0010], // 7
657        [0b1110, 0b1010, 0b1010, 0b1110, 0b1010, 0b1010, 0b1110], // 8
658        [0b1110, 0b1010, 0b1010, 0b1110, 0b0010, 0b0010, 0b1110], // 9
659    ];
660
661    const FG_COLOR: u16 = 0xFFFF; // White
662    const BG_COLOR: u16 = 0x0000; // Black
663    const START_X: usize = 4;
664    const START_Y: usize = 4;
665    const PADDING: usize = 2;
666
667    // Calculate background dimensions
668    const BG_WIDTH: usize = 3 * DIGIT_WIDTH + 2 + PADDING * 2; // 3 digits + 2 gaps + padding
669    const BG_HEIGHT: usize = DIGIT_HEIGHT + PADDING * 2;
670
671    // Draw dark background
672    for y in 0..BG_HEIGHT {
673        for x in 0..BG_WIDTH {
674            let fb_x = START_X + x;
675            let fb_y = START_Y + y;
676            if fb_x < display.get_screen_width() && fb_y < display.get_screen_height() {
677                let fb_idx = fb_y * display.get_screen_width() + fb_x;
678                display.frame_buffer()[fb_idx] = BG_COLOR;
679            }
680        }
681    }
682
683    // Extract digits from FPS value
684    let mut digits = [0u8; 3];
685    let mut temp = fps.min(999); // Cap at 999
686    digits[2] = (temp % 10) as u8;
687    temp /= 10;
688    digits[1] = (temp % 10) as u8;
689    temp /= 10;
690    digits[0] = (temp % 10) as u8;
691
692    // Draw each digit
693    for (digit_idx, &digit) in digits.iter().enumerate() {
694        let x_offset = START_X + PADDING + digit_idx * (DIGIT_WIDTH + 1);
695
696        for y in 0..DIGIT_HEIGHT {
697            let row_data = DIGITS[digit as usize][y];
698            for x in 0..DIGIT_WIDTH {
699                if (row_data >> (DIGIT_WIDTH - 1 - x)) & 1 == 1 {
700                    let fb_x = x_offset + x;
701                    let fb_y = START_Y + PADDING + y;
702                    if fb_x < display.get_screen_width() && fb_y < display.get_screen_height() {
703                        let fb_idx = fb_y * display.get_screen_width() + fb_x;
704                        display.frame_buffer()[fb_idx] = FG_COLOR;
705                    }
706                }
707            }
708        }
709    }
710}
711// Shift frame buffer for small camera movements
712// delta represents camera movement, buffer shifts in opposite direction
713#[inline(never)]
714pub fn shift_buffer(display: &mut impl Display, delta_x: i16, delta_y: i16) {
715    if delta_x == 0 && delta_y == 0 {
716        return;
717    }
718
719    let fb_ptr = display.frame_buffer().as_mut_ptr();
720
721    unsafe {
722        if delta_y != 0 {
723            // Camera moves down -> buffer shifts up (content moves up on screen)
724            // Camera moves up -> buffer shifts down (content moves down on screen)
725            let shift_amount = delta_y.abs() as usize;
726            if delta_y > 0 {
727                // Camera down: shift buffer UP
728                for y in 0..(display.get_screen_height() - shift_amount) {
729                    let src_idx = (y + shift_amount) * display.get_screen_width();
730                    let dst_idx = y * display.get_screen_width();
731                    core::ptr::copy(
732                        fb_ptr.add(src_idx),
733                        fb_ptr.add(dst_idx),
734                        display.get_screen_width(),
735                    );
736                }
737            } else {
738                // Camera up: shift buffer DOWN
739                for y in (shift_amount..display.get_screen_height()).rev() {
740                    let src_idx = (y - shift_amount) * display.get_screen_width();
741                    let dst_idx = y * display.get_screen_width();
742                    core::ptr::copy(
743                        fb_ptr.add(src_idx),
744                        fb_ptr.add(dst_idx),
745                        display.get_screen_width(),
746                    );
747                }
748            }
749        }
750
751        if delta_x != 0 {
752            // Camera moves right -> buffer shifts left (content moves left on screen)
753            // Camera moves left -> buffer shifts right (content moves right on screen)
754            let shift_amount = delta_x.abs() as usize;
755            if delta_x > 0 {
756                // Camera right: shift buffer LEFT
757                for y in 0..display.get_screen_height() {
758                    let row_start = y * display.get_screen_width();
759                    core::ptr::copy(
760                        fb_ptr.add(row_start + shift_amount),
761                        fb_ptr.add(row_start),
762                        display.get_screen_width() - shift_amount,
763                    );
764                }
765            } else {
766                // Camera left: shift buffer RIGHT
767                for y in 0..display.get_screen_height() {
768                    let row_start = y * display.get_screen_width();
769                    core::ptr::copy(
770                        fb_ptr.add(row_start),
771                        fb_ptr.add(row_start + shift_amount),
772                        display.get_screen_width() - shift_amount,
773                    );
774                }
775            }
776        }
777    }
778}