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