summaryrefslogtreecommitdiffstats
path: root/rsnix/src/gop.rs
diff options
context:
space:
mode:
Diffstat (limited to 'rsnix/src/gop.rs')
-rw-r--r--rsnix/src/gop.rs268
1 files changed, 261 insertions, 7 deletions
diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs
index 0d24d7d..9dedcb8 100644
--- a/rsnix/src/gop.rs
+++ b/rsnix/src/gop.rs
@@ -10,11 +10,11 @@ use alloc::vec;
10use alloc::vec::Vec; 10use alloc::vec::Vec;
11use core::fmt; 11use core::fmt;
12use core::ptr; 12use core::ptr;
13use core::sync::atomic::{AtomicU8, AtomicU64, Ordering};
13 14
14/// Framebuffer state captured before ExitBootServices. 15/// Framebuffer state captured before ExitBootServices.
15pub struct Framebuffer { 16pub struct Framebuffer {
16 base: *mut u8, 17 base: *mut u8,
17 #[allow(dead_code)]
18 width: usize, 18 width: usize,
19 height: usize, 19 height: usize,
20 pitch: usize, // bytes per scan line 20 pitch: usize, // bytes per scan line
@@ -119,6 +119,242 @@ pub fn dims() -> (usize, usize) {
119 } 119 }
120} 120}
121 121
122/// (width, height) of the framebuffer in pixels.
123pub fn pixel_dims() -> (usize, usize) {
124 match fb().as_ref() {
125 Some(f) => (f.width, f.height),
126 None => (0, 0),
127 }
128}
129
130// ---------------------------------------------------------------------------
131// Rect primitives + the push path
132// ---------------------------------------------------------------------------
133
134/// Copy a shadow rect to the display and composite the mouse cursor on
135/// top. Every display write goes through here (glyph blits, fills,
136/// scroll shifts), so the cursor — which lives *only* in this overlay,
137/// never in the shadow — stays on top of whatever the content is without
138/// any save/restore bookkeeping. The display is never read.
139pub(crate) fn push_rect(x: usize, y: usize, w: usize, h: usize) {
140 let Some(fb) = fb().as_ref() else {
141 return;
142 };
143 if unsafe { SHADOW.is_null() } || x >= fb.width || y >= fb.height {
144 return;
145 }
146 let w = w.min(fb.width - x);
147 let h = h.min(fb.height - y);
148 let row_words = fb.pitch / 4;
149 unsafe {
150 for dy in 0..h {
151 let off = (y + dy) * row_words + x;
152 ptr::copy_nonoverlapping(SHADOW.add(off), fb.base.add(off * 4) as *mut u32, w);
153 }
154 }
155 overlay_cursor(x, y, w, h);
156}
157
158/// Fill a rectangle in the shadow and push it (with cursor composited).
159pub fn fill_rect(x: usize, y: usize, w: usize, h: usize, color: u32) {
160 let Some(fb) = fb().as_ref() else {
161 return;
162 };
163 if x >= fb.width || y >= fb.height {
164 return;
165 }
166 let w = w.min(fb.width - x);
167 let h = h.min(fb.height - y);
168 if unsafe { SHADOW.is_null() } {
169 // Degraded: no backbuffer — write the display directly.
170 let row_words = fb.pitch / 4;
171 unsafe {
172 for dy in 0..h {
173 let off = (y + dy) * row_words + x;
174 for dx in 0..w {
175 ptr::write_volatile(fb.base.add((off + dx) * 4) as *mut u32, color);
176 }
177 }
178 }
179 return;
180 }
181 let row_words = fb.pitch / 4;
182 unsafe {
183 for dy in 0..h {
184 let off = (y + dy) * row_words + x;
185 for dx in 0..w {
186 *SHADOW.add(off + dx) = color;
187 }
188 }
189 }
190 push_rect(x, y, w, h);
191}
192
193/// Read one pixel back from the shadow (RAM; cheap). Test/demo use only.
194pub fn shadow_pixel(x: usize, y: usize) -> Option<u32> {
195 let fb = fb().as_ref()?;
196 if x >= fb.width || y >= fb.height || unsafe { SHADOW.is_null() } {
197 return None;
198 }
199 Some(unsafe { *SHADOW.add(y * (fb.pitch / 4) + x) })
200}
201
202/// Read one pixel back from the display (volatile; uncached on real
203/// hardware — test/demo use only, never on a hot path).
204pub fn display_pixel(x: usize, y: usize) -> Option<u32> {
205 let fb = fb().as_ref()?;
206 if x >= fb.width || y >= fb.height {
207 return None;
208 }
209 Some(unsafe { ptr::read_volatile(fb.base.add((y * (fb.pitch / 4) + x) * 4) as *mut u32) })
210}
211
212// ---------------------------------------------------------------------------
213// Mouse cursor — a display overlay, composited by `push_rect`.
214// ---------------------------------------------------------------------------
215
216/// Cursor bitmap: 12×16 classic pointer, 1 = opaque. The hotspot is the
217/// top-left corner of the bitmap; the arrow tip sits at (2, 0). Built by
218/// a const fn from explicit column lists (pixel column `dx` = bit `11-dx`)
219/// so the shape is auditable instead of hand-mangled bit literals.
220const CURSOR_W: usize = 12;
221const CURSOR_H: usize = 16;
222
223const fn bmp_row(cols: &[u8]) -> u16 {
224 let mut v: u16 = 0;
225 let mut i = 0;
226 while i < cols.len() {
227 v |= 1u16 << (11 - cols[i] as u16);
228 i += 1;
229 }
230 v
231}
232
233/// Head: triangle from a 1-px tip at (2,0) widening right to col 11.
234/// Tail: 2-px diagonal dropping down-right from the head's base.
235const CURSOR_BMP: [u16; CURSOR_H] = [
236 bmp_row(&[2]),
237 bmp_row(&[2, 3]),
238 bmp_row(&[2, 3, 4]),
239 bmp_row(&[2, 3, 4, 5]),
240 bmp_row(&[2, 3, 4, 5, 6]),
241 bmp_row(&[2, 3, 4, 5, 6, 7]),
242 bmp_row(&[2, 3, 4, 5, 6, 7, 8]),
243 bmp_row(&[2, 3, 4, 5, 6, 7, 8, 9]),
244 bmp_row(&[2, 3, 4, 5, 6, 7, 8, 9, 10]),
245 bmp_row(&[2, 3, 4, 5, 6, 7, 8, 9, 10, 11]),
246 bmp_row(&[8, 9]),
247 bmp_row(&[9, 10]),
248 bmp_row(&[9, 10]),
249 bmp_row(&[10, 11]),
250 bmp_row(&[10, 11]),
251 bmp_row(&[10, 11]),
252];
253
254/// Packed cursor position: x in the high 32 bits, y in the low 32, so a
255/// single relaxed load never sees a torn pair. Written by the cursor
256/// task, read by every push (any task).
257static CURSOR_POS: AtomicU64 = AtomicU64::new(0);
258static CURSOR_BUTTONS: AtomicU8 = AtomicU8::new(0);
259
260fn pack(x: usize, y: usize) -> u64 {
261 ((x as u64) << 32) | (y as u64)
262}
263
264/// Current cursor position (top-left of the bitmap).
265pub fn cursor_pos() -> (usize, usize) {
266 let v = CURSOR_POS.load(Ordering::Relaxed);
267 ((v >> 32) as usize, (v & 0xFFFF_FFFF) as usize)
268}
269
270/// Current button state: bit 0 = left, bit 1 = right, bit 2 = middle.
271pub fn cursor_buttons() -> u8 {
272 CURSOR_BUTTONS.load(Ordering::Relaxed)
273}
274
275pub fn cursor_set_buttons(b: u8) {
276 CURSOR_BUTTONS.store(b, Ordering::Relaxed);
277}
278
279/// Place the cursor (used at init; centre of the screen).
280pub fn cursor_set(x: usize, y: usize) {
281 CURSOR_POS.store(pack(x, y), Ordering::Relaxed);
282}
283
284/// Move the cursor by a relative delta, clamped to the screen, erasing
285/// the old position (a shadow push — the shadow never held the cursor)
286/// and drawing the new one. Returns the new position.
287pub fn cursor_move(dx: i16, dy: i16) -> (usize, usize) {
288 let Some(fb) = fb().as_ref() else {
289 return cursor_pos();
290 };
291 let (cx, cy) = cursor_pos();
292 let max_x = fb.width.saturating_sub(CURSOR_W);
293 let max_y = fb.height.saturating_sub(CURSOR_H);
294 let nx = (cx as i64 + dx as i64).clamp(0, max_x as i64) as usize;
295 let ny = (cy as i64 + dy as i64).clamp(0, max_y as i64) as usize;
296 if (nx, ny) == (cx, cy) {
297 return (cx, cy);
298 }
299 CURSOR_POS.store(pack(nx, ny), Ordering::Relaxed);
300 // Restore the old rect (overlay now composites at the new position).
301 push_rect(cx, cy, CURSOR_W, CURSOR_H);
302 // Draw at the new position.
303 push_rect(nx, ny, CURSOR_W, CURSOR_H);
304 (nx, ny)
305}
306
307/// If the cursor overlaps the given rect, composite it (black outline
308/// offset +1,+1, white body on top). Called by `push_rect`.
309fn overlay_cursor(x: usize, y: usize, w: usize, h: usize) {
310 let (cx, cy) = cursor_pos();
311 if x >= cx + CURSOR_W || y >= cy + CURSOR_H {
312 return;
313 }
314 if cx >= x + w || cy >= y + h {
315 return;
316 }
317 draw_bmp(cx + 1, cy + 1, rgb(0, 0, 0));
318 draw_bmp(cx, cy, rgb(255, 255, 255));
319}
320
321/// Re-composite the cursor where it is (after a scroll shift, which
322/// bypasses `push_rect`, and once at init so it is visible over idle
323/// cells the diff never re-blits).
324pub(crate) fn cursor_redraw() {
325 let (cx, cy) = cursor_pos();
326 draw_bmp(cx + 1, cy + 1, rgb(0, 0, 0));
327 draw_bmp(cx, cy, rgb(255, 255, 255));
328}
329
330/// Write the cursor bitmap at (x, y) in one colour — display only, the
331/// shadow is never touched (that is what makes the overlay race-free).
332fn draw_bmp(x: usize, y: usize, color: u32) {
333 let Some(fb) = fb().as_ref() else {
334 return;
335 };
336 let row_words = fb.pitch / 4;
337 unsafe {
338 for dy in 0..CURSOR_H {
339 let yy = y + dy;
340 if yy >= fb.height {
341 break;
342 }
343 let row = CURSOR_BMP[dy];
344 for dx in 0..CURSOR_W {
345 let xx = x + dx;
346 if xx >= fb.width {
347 break;
348 }
349 if row & (1u16 << (11 - dx)) != 0 {
350 let off = yy * row_words + xx;
351 ptr::write_volatile(fb.base.add(off * 4) as *mut u32, color);
352 }
353 }
354 }
355 }
356}
357
122/// Pack an (r, g, b) colour for the framebuffer's pixel format. 358/// Pack an (r, g, b) colour for the framebuffer's pixel format.
123pub fn rgb(r: u8, g: u8, b: u8) -> u32 { 359pub fn rgb(r: u8, g: u8, b: u8) -> u32 {
124 match fb().as_ref() { 360 match fb().as_ref() {
@@ -191,6 +427,9 @@ impl Framebuffer {
191 // coalesce well on WC framebuffers — far fewer bus 427 // coalesce well on WC framebuffers — far fewer bus
192 // transactions than per-pixel stores. 428 // transactions than per-pixel stores.
193 ptr::copy_nonoverlapping(SHADOW, self.base as *mut u32, words); 429 ptr::copy_nonoverlapping(SHADOW, self.base as *mut u32, words);
430 // The scroll copied the shadow (which never contains the
431 // cursor); re-composite it on top of the pushed frame.
432 crate::gop::cursor_redraw();
194 } 433 }
195 } 434 }
196} 435}
@@ -363,20 +602,35 @@ pub(crate) fn blit_glyph(col: usize, row: usize, ch: u16, fg: (u8, u8, u8), bg:
363 let x = col * 8; 602 let x = col * 8;
364 let y = row * 16; 603 let y = row * 16;
365 604
605 if unsafe { SHADOW.is_null() } {
606 // Degraded: no backbuffer — write the display directly, no
607 // cursor overlay.
608 for cy in 0..16 {
609 let row_bits = data[cy];
610 for cx in 0..8 {
611 let offset = (y + cy) * fb.pitch + (x + cx) * fb.bytes_per_pixel;
612 let px = if row_bits & (0x80 >> cx) != 0 { fg } else { bg };
613 unsafe {
614 ptr::write_volatile(fb.base.add(offset) as *mut u32, px);
615 }
616 }
617 }
618 return;
619 }
620
621 // Normal path: rasterise into the RAM shadow, then one push
622 // copies the cell to the display and composites the cursor.
366 for cy in 0..16 { 623 for cy in 0..16 {
367 let row_bits = data[cy]; 624 let row_bits = data[cy];
368 for cx in 0..8 { 625 for cx in 0..8 {
369 let offset = (y + cy) * fb.pitch + (x + cx) * fb.bytes_per_pixel; 626 let offset = (y + cy) * fb.pitch + (x + cx) * fb.bytes_per_pixel;
627 let px = if row_bits & (0x80 >> cx) != 0 { fg } else { bg };
370 unsafe { 628 unsafe {
371 let px = if row_bits & (0x80 >> cx) != 0 { fg } else { bg }; 629 *SHADOW.add(offset / 4) = px;
372 // Display (volatile; the fb is MMIO-ish) + RAM shadow.
373 ptr::write_volatile(fb.base.add(offset) as *mut u32, px);
374 if !SHADOW.is_null() {
375 *SHADOW.add(offset / 4) = px;
376 }
377 } 630 }
378 } 631 }
379 } 632 }
633 push_rect(x, y, 8, 16);
380 } 634 }
381} 635}
382 636