ling-lang
↓ 0 下载Ling - The Omniglot Systems Language
关键词 systems, polyglot, ai, crypto, game
lingfu install ling-lang代码 / 文件 (995)
crates/ling-kernel/src/drivers/framebuffer.rs
//! Linear framebuffer support across x86_64 (Multiboot2) and aarch64 (BCM2835 mailbox).
use core::ptr;
#[cfg(target_arch = "x86_64")]
extern "C" {
static mb2_info_ptr: u32;
}
#[derive(Clone, Copy)]
pub struct FbInfo {
pub addr: u64,
pub pitch: u32,
pub width: u32,
pub height: u32,
pub bpp: u8,
}
static mut FB: Option<FbInfo> = None;
// Overscan safe-area margin. The logical origin is inset by (OX, OY) from the
// real top-left, and width()/height() report the reduced drawable area, so UI
// laid out against those bounds lands fully inside a display that crops the
// signal edges (common on HDMI panels/TVs — on the Pi 5 the visible area can
// start a couple % in, so content at a true (0,0) gets clipped off the top-left
// corner). aarch64/Pi sets this after framebuffer init; it stays 0 on x86_64
// (VMs present the whole surface, no overscan), so set_pixel/fill_rect/width/
// height are unchanged there.
static mut OX: u32 = 0;
static mut OY: u32 = 0;
const BACKBUFFER_MAX: usize = 1920 * 1080 * 4;
static mut BACKBUFFER: [u8; BACKBUFFER_MAX] = [0u8; BACKBUFFER_MAX];
fn back_buf() -> &'static mut [u8; BACKBUFFER_MAX] {
unsafe { &mut *&raw mut BACKBUFFER }
}
/// Physical address + magic of the VBE handoff block the disk-boot
/// stage2 leaves behind (bootloader/stage2.asm's LFB_INFO_ADDR/_MAGIC --
/// keep in sync). Layout: magic, physbase, pitch, width, height, bpp,
/// each a u32.
#[cfg(target_arch = "x86_64")]
const LFB_INFO_ADDR: u64 = 0x6000;
#[cfg(target_arch = "x86_64")]
const LFB_INFO_MAGIC: u32 = 0x4942_464C; // "LFBI"
/// True when this kernel was loaded by the disk-boot path (stage1/stage2,
/// no GRUB): there is no Multiboot2 info structure at all. The desktop
/// uses this to decide between the Live auto-login flow and the installed
/// system's login greeter.
#[cfg(target_arch = "x86_64")]
pub fn booted_from_disk() -> bool {
unsafe { ptr::read_volatile(&raw const mb2_info_ptr) == 0 }
}
pub fn init() {
#[cfg(target_arch = "x86_64")]
unsafe {
let info_ptr = ptr::read_volatile(&raw const mb2_info_ptr);
if info_ptr == 0 {
// Disk boot: no Multiboot2 info exists. stage2 may have set a
// VBE mode and left its tagged handoff block instead.
if ptr::read_volatile(LFB_INFO_ADDR as *const u32) == LFB_INFO_MAGIC {
let addr = ptr::read_volatile((LFB_INFO_ADDR + 4) as *const u32) as u64;
let pitch = ptr::read_volatile((LFB_INFO_ADDR + 8) as *const u32);
let width = ptr::read_volatile((LFB_INFO_ADDR + 12) as *const u32);
let height = ptr::read_volatile((LFB_INFO_ADDR + 16) as *const u32);
let bpp = ptr::read_volatile((LFB_INFO_ADDR + 20) as *const u32) as u8;
if addr != 0 && width > 0 && height > 0 && (bpp == 24 || bpp == 32) {
ptr::write(&raw mut FB, Some(FbInfo { addr, pitch, width, height, bpp }));
}
}
return;
}
let total_size = ptr::read_unaligned(info_ptr as *const u32);
let mut offset: u32 = 8;
while offset + 8 <= total_size {
let tag_ptr = (info_ptr + offset) as *const u32;
let tag_type = ptr::read_unaligned(tag_ptr);
let tag_size = ptr::read_unaligned(tag_ptr.add(1));
if tag_type == 0 {
break;
}
if tag_type == 8 && tag_size >= 29 {
let base = info_ptr + offset;
let addr = ptr::read_unaligned((base + 8) as *const u64);
let pitch = ptr::read_unaligned((base + 16) as *const u32);
let width = ptr::read_unaligned((base + 20) as *const u32);
let height = ptr::read_unaligned((base + 24) as *const u32);
let bpp = ptr::read_volatile((base + 28) as *const u8);
ptr::write(
&raw mut FB,
Some(FbInfo { addr, pitch, width, height, bpp }),
);
return;
}
offset += (tag_size + 7) & !7;
}
}
#[cfg(target_arch = "aarch64")]
{
if let Some((addr, pitch, width, height, bpp)) =
// 1280x720 (720p): a standard HDMI/TV mode. TVs often won't cleanly
// scan out the 4:3 1024x768 computer mode, and the firmware may give
// an odd surface for it. The firmware clamps to the display's real
// mode anyway, and we read the actual geometry back.
crate::arch::mailbox::allocate_framebuffer(1280, 720, 32)
{
unsafe {
ptr::write(&raw mut FB, Some(FbInfo { addr, pitch, width, height, bpp }));
}
// Overscan safe area: pull all rendering in from the edges so a
// monitor that crops the HDMI signal (reported ~10px off the top-
// left on real Pi 5 hardware) doesn't clip the UI. Proportional
// (~2%) so it scales with resolution; the config screen can nudge it.
set_safe_margin(width / 48, height / 48);
}
}
}
// -- Live resolution switching (bochs/QEMU/VirtualBox DISPI) -----------------
// VBE's mode-set is a real-mode BIOS call we can't make in long mode -- but the
// bochs VBE "DISPI" register interface (used by QEMU's std VGA and VirtualBox's
// VBoxVGA) reprograms width/height/bpp at runtime with plain port I/O, and the
// linear framebuffer stays at the same aperture. So on those adapters we CAN
// change resolution live; elsewhere `set_mode` reports false and the caller
// falls back to persist-and-reboot.
#[cfg(target_arch = "x86_64")]
mod dispi {
use crate::arch::io::{inw, outw};
const INDEX: u16 = 0x01CE;
const DATA: u16 = 0x01CF;
const I_ID: u16 = 0;
const I_XRES: u16 = 1;
const I_YRES: u16 = 2;
const I_BPP: u16 = 3;
const I_ENABLE: u16 = 4;
const I_VIRT_WIDTH: u16 = 6;
const ENABLED: u16 = 0x01;
const GETCAPS: u16 = 0x02;
const LFB: u16 = 0x40;
unsafe fn read(idx: u16) -> u16 {
outw(INDEX, idx);
inw(DATA)
}
unsafe fn write(idx: u16, val: u16) {
outw(INDEX, idx);
outw(DATA, val);
}
/// A bochs-DISPI-capable adapter reports an id of 0xB0C0..=0xB0CF.
pub fn present() -> bool {
let id = unsafe { read(I_ID) };
(0xB0C0..=0xB0CF).contains(&id)
}
/// Query the adapter's maximum supported width/height. The DISPI spec:
/// with the GETCAPS bit set in ENABLE, reading XRES/YRES returns the max
/// the card can do (bounded by its VRAM), then GETCAPS is cleared. This is
/// how we offer only the modes the actual card supports instead of a
/// hardcoded guess.
pub fn max_caps() -> Option<(u32, u32)> {
if !present() {
return None;
}
unsafe {
let prev = read(I_ENABLE);
write(I_ENABLE, GETCAPS);
let mw = read(I_XRES) as u32;
let mh = read(I_YRES) as u32;
write(I_ENABLE, prev); // restore whatever mode was live
if mw == 0 || mh == 0 {
None
} else {
Some((mw, mh))
}
}
}
pub fn set(w: u16, h: u16) {
unsafe {
write(I_ENABLE, 0); // must disable before changing geometry
write(I_XRES, w);
write(I_YRES, h);
write(I_BPP, 32);
write(I_VIRT_WIDTH, w); // pitch = w * 4
write(I_ENABLE, ENABLED | LFB);
}
}
}
/// Change the display resolution live (32bpp), reusing the existing linear
/// framebuffer aperture. Returns false if the adapter isn't DISPI-capable
/// (real hardware / a mode too big for the back buffer) -- the caller then
/// persists a next-boot preference instead. Keeps `FB.addr`; updates
/// width/height/pitch/bpp and clears the new surface.
#[cfg(target_arch = "x86_64")]
pub fn set_mode(w: u32, h: u32) -> bool {
if w == 0 || h == 0 || (w as usize) * (h as usize) * 4 > BACKBUFFER_MAX {
return false;
}
let Some(mut fb) = get() else { return false };
if !dispi::present() {
return false;
}
dispi::set(w as u16, h as u16);
fb.width = w;
fb.height = h;
fb.bpp = 32;
fb.pitch = w * 4;
unsafe { ptr::write(&raw mut FB, Some(fb)) };
back_clear(0x10_10_18);
present();
true
}
/// The Pi: ask the firmware for a new surface (physical + virtual size), take
/// whatever geometry it really gave (the Pi 5 quirks are handled in the
/// mailbox code) and re-derive the overscan margin. The window layer re-sizes
/// itself from `width()`/`height()` afterwards.
#[cfg(target_arch = "aarch64")]
pub fn set_mode(w: u32, h: u32) -> bool {
if w == 0 || h == 0 || (w as usize) * (h as usize) * 4 > BACKBUFFER_MAX {
return false;
}
let Some((addr, pitch, width, height, bpp)) = crate::arch::mailbox::allocate_framebuffer(w, h, 32) else {
return false;
};
unsafe {
ptr::write(&raw mut FB, Some(FbInfo { addr, pitch, width, height, bpp }));
}
set_safe_margin(width / 48, height / 48);
clear_physical(0);
// what the firmware really gave (its geometry / pitch / depth answers
// are not always what was asked): on the serial log for the test runs
let d = unsafe { crate::arch::mailbox::FB_DIAG };
crate::console_write(b"display: ");
crate::print_decimal(width as u64);
crate::console_write(b"x");
crate::print_decimal(height as u64);
crate::console_write(b" pitch ");
crate::print_decimal(pitch as u64);
crate::console_write(b" bpp ");
crate::print_decimal(bpp as u64);
crate::console_write(b" (phys ");
crate::print_decimal(d[0] as u64);
crate::console_write(b"x");
crate::print_decimal(d[1] as u64);
crate::console_write(b" virt ");
crate::print_decimal(d[2] as u64);
crate::console_write(b"x");
crate::print_decimal(d[3] as u64);
crate::console_write(b" size ");
crate::print_decimal(d[5] as u64);
crate::console_write(b")
");
true
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
pub fn set_mode(_w: u32, _h: u32) -> bool {
false
}
/// The surface's full size (before the overscan margin), for the mode row.
pub fn mode_dims() -> (u32, u32) {
get().map(|f| (f.width, f.height)).unwrap_or((0, 0))
}
pub fn get() -> Option<FbInfo> {
unsafe { ptr::read(&raw const FB) }
}
pub fn available() -> bool {
get().is_some()
}
pub fn width() -> u32 {
get().map(|f| f.width).unwrap_or(0).saturating_sub(unsafe { OX } * 2)
}
pub fn height() -> u32 {
get().map(|f| f.height).unwrap_or(0).saturating_sub(unsafe { OY } * 2)
}
/// Set the overscan safe-area margin (pixels inset on each edge). All direct
/// framebuffer drawing shifts in by this and width()/height() shrink by twice
/// it, so UI laid out against those bounds stays within a display that crops
/// its edges. The Pi config screen exposes this as a live alignment nudge.
pub fn set_safe_margin(ox: u32, oy: u32) {
unsafe {
OX = ox;
OY = oy;
}
}
/// Current overscan safe-area margin (pixels inset per edge).
pub fn safe_margin() -> (u32, u32) {
unsafe { (OX, OY) }
}
pub fn set_pixel(x: u32, y: u32, color: u32) {
unsafe {
let Some(fb) = get() else { return };
let x = x + OX;
let y = y + OY;
if x >= fb.width || y >= fb.height {
return;
}
let bypp = fb.bpp as u32 / 8;
let byte_offset = y as u64 * fb.pitch as u64 + x as u64 * bypp as u64;
let p = (fb.addr + byte_offset) as *mut u8;
match bypp {
4 => ptr::write_volatile(p as *mut u32, color),
3 => {
ptr::write_volatile(p, (color & 0xFF) as u8);
ptr::write_volatile(p.add(1), ((color >> 8) & 0xFF) as u8);
ptr::write_volatile(p.add(2), ((color >> 16) & 0xFF) as u8);
},
2 => ptr::write_volatile(p as *mut u16, rgb565(color)),
_ => {},
}
}
}
pub fn fill_rect(x: u32, y: u32, w: u32, h: u32, color: u32) {
let Some(fb) = get() else { return };
let (x, y) = unsafe { (x + OX, y + OY) };
if x >= fb.width || y >= fb.height {
return;
}
let w = w.min(fb.width - x);
let h = h.min(fb.height - y);
if w == 0 || h == 0 {
return;
}
let bypp = fb.bpp as u32 / 8;
if bypp == 4 {
for row in y..y + h {
let row_start = (fb.addr + row as u64 * fb.pitch as u64 + x as u64 * 4) as *mut u32;
for col in 0..w as usize {
unsafe { ptr::write_volatile(row_start.add(col), color) };
}
}
} else if bypp == 3 {
let b0 = (color & 0xFF) as u8;
let b1 = ((color >> 8) & 0xFF) as u8;
let b2 = ((color >> 16) & 0xFF) as u8;
for row in y..y + h {
let mut p = (fb.addr + row as u64 * fb.pitch as u64 + x as u64 * 3) as *mut u8;
for _ in 0..w {
unsafe {
ptr::write_volatile(p, b0);
ptr::write_volatile(p.add(1), b1);
ptr::write_volatile(p.add(2), b2);
p = p.add(3);
}
}
}
} else if bypp == 2 {
let p16 = rgb565(color);
for row in y..y + h {
let row_start = (fb.addr + row as u64 * fb.pitch as u64 + x as u64 * 2) as *mut u16;
for col in 0..w as usize {
unsafe { ptr::write_volatile(row_start.add(col), p16) };
}
}
}
}
/// Fill the WHOLE physical framebuffer, margin included, with one 0xRRGGBB
/// color — so nothing drawn earlier (the boot banner) lingers in the overscan
/// margin around a full-screen app.
pub fn clear_physical(color: u32) {
let Some(fb) = get() else { return };
for y in 0..fb.height as usize {
let base = fb.addr as usize + y * fb.pitch as usize;
unsafe {
match fb.bpp {
32 => {
let row = base as *mut u32;
for x in 0..fb.width as usize {
ptr::write_volatile(row.add(x), color);
}
},
16 => {
let row = base as *mut u16;
for x in 0..fb.width as usize {
ptr::write_volatile(row.add(x), rgb565(color));
}
},
_ => {
for x in 0..fb.width as usize * (fb.bpp as usize / 8) {
ptr::write_volatile((base + x) as *mut u8, 0);
}
},
}
}
}
}
/// Copy a whole `sw`×`sh` frame of 0xRRGGBB pixels to the live framebuffer at
/// the safe-area origin, converting to the panel's format (32/24bpp, or the
/// RGB565 the Pi 5 firmware scans out). One pass, row by row — the Ling app
/// runtime's `present()`; far cheaper than per-pixel `set_pixel`.
pub fn blit_rgb(src: &[u32], sw: usize, sh: usize) {
let Some(fb) = get() else { return };
let (ox, oy) = unsafe { (OX as usize, OY as usize) };
let (fw, fh) = (fb.width as usize, fb.height as usize);
if ox >= fw || oy >= fh || sw == 0 {
return;
}
let w = sw.min(fw - ox);
let h = sh.min(fh - oy).min(src.len() / sw);
let pitch = fb.pitch as usize;
for y in 0..h {
let row = &src[y * sw..y * sw + w];
let base = fb.addr as usize + (y + oy) * pitch;
unsafe {
match fb.bpp {
32 => {
let dst = (base + ox * 4) as *mut u32;
ptr::copy_nonoverlapping(row.as_ptr(), dst, w);
},
16 => {
// Two pixels per 32-bit store: the Pi's screen is write-combined
// memory, so wide plain stores go several times faster than
// one volatile 16-bit write per pixel.
let dst = (base + ox * 2) as *mut u16;
let mut x = 0usize;
if (dst as usize) & 3 == 0 {
let d32 = dst as *mut u32;
while x + 1 < w {
let v = rgb565(row[x]) as u32 | ((rgb565(row[x + 1]) as u32) << 16);
ptr::write(d32.add(x / 2), v);
x += 2;
}
}
while x < w {
ptr::write(dst.add(x), rgb565(row[x]));
x += 1;
}
},
24 => {
let mut p = (base + ox * 3) as *mut u8;
for &c in row {
ptr::write_volatile(p, (c & 0xFF) as u8);
ptr::write_volatile(p.add(1), ((c >> 8) & 0xFF) as u8);
ptr::write_volatile(p.add(2), ((c >> 16) & 0xFF) as u8);
p = p.add(3);
}
},
_ => {},
}
}
}
}
/// Pack a 0xRRGGBB color into 16-bit RGB565, for the 16bpp framebuffer the
/// Pi 5 firmware sometimes hands back regardless of the requested depth.
#[inline]
fn rgb565(color: u32) -> u16 {
let r = (color >> 16) & 0xFF;
let g = (color >> 8) & 0xFF;
let b = color & 0xFF;
((((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)) & 0xFFFF) as u16
}
pub fn clear(color: u32) {
let (w, h) = (width(), height());
fill_rect(0, 0, w, h, color);
}
/// Is the display a bochs-DISPI adapter we can resize live? (QEMU std VGA /
/// VirtualBox VBoxVGA.) Settings uses this to label the Display row.
pub fn dispi_capable() -> bool {
#[cfg(target_arch = "x86_64")]
{
dispi::present()
}
#[cfg(not(target_arch = "x86_64"))]
{
false
}
}
/// The adapter's maximum supported (width, height), queried from the card via
/// DISPI GETCAPS -- `None` if not a DISPI adapter. Settings uses this to list
/// only the modes the card can actually do.
pub fn dispi_max() -> Option<(u32, u32)> {
#[cfg(target_arch = "x86_64")]
{
dispi::max_caps()
}
#[cfg(not(target_arch = "x86_64"))]
{
None
}
}
/// How many 32bpp pixels the back buffer can hold -- the other ceiling on a
/// live mode (a mode must fit both the card's caps and this buffer).
pub fn max_pixels() -> usize {
BACKBUFFER_MAX / 4
}
// -- Clip rectangle (scissor) ------------------------------------------------
// Every primitive that funnels through back_set_pixel / back_fill_rect /
// back_blend_pixel respects this. A window sets it to its body rect while
// drawing content, so text/widgets can't spill past the window edges (or over
// neighbouring windows). `None` = draw to the whole screen.
static mut CLIP: Option<(u32, u32, u32, u32)> = None;
pub fn set_clip(x: u32, y: u32, w: u32, h: u32) {
unsafe { CLIP = Some((x, y, w, h)) };
}
pub fn clear_clip() {
unsafe { CLIP = None };
}
#[inline]
fn clip_ok(x: u32, y: u32) -> bool {
match unsafe { CLIP } {
Some((cx, cy, cw, ch)) => x >= cx && y >= cy && x < cx + cw && y < cy + ch,
None => true,
}
}
/// Intersect a rect with the active clip; None if fully clipped out.
#[inline]
fn clip_rect(x: u32, y: u32, w: u32, h: u32) -> Option<(u32, u32, u32, u32)> {
match unsafe { CLIP } {
None => Some((x, y, w, h)),
Some((cx, cy, cw, ch)) => {
let x0 = x.max(cx);
let y0 = y.max(cy);
let x1 = (x + w).min(cx + cw);
let y1 = (y + h).min(cy + ch);
if x1 <= x0 || y1 <= y0 {
None
} else {
Some((x0, y0, x1 - x0, y1 - y0))
}
}
}
}
pub fn back_set_pixel(x: u32, y: u32, color: u32) {
let Some(fb) = get() else { return };
if x >= fb.width || y >= fb.height {
return;
}
if !clip_ok(x, y) {
return;
}
let bypp = fb.bpp as u32 / 8;
let offset = y as usize * fb.pitch as usize + x as usize * bypp as usize;
if offset + bypp as usize > BACKBUFFER_MAX {
return;
}
let buf = back_buf();
match bypp {
4 => buf[offset..offset + 4].copy_from_slice(&color.to_le_bytes()),
3 => {
buf[offset] = (color & 0xFF) as u8;
buf[offset + 1] = ((color >> 8) & 0xFF) as u8;
buf[offset + 2] = ((color >> 16) & 0xFF) as u8;
},
_ => {},
}
}
pub fn back_fill_rect(x: u32, y: u32, w: u32, h: u32, color: u32) {
let Some(fb) = get() else { return };
if x >= fb.width || y >= fb.height {
return;
}
let w = w.min(fb.width - x);
let h = h.min(fb.height - y);
if w == 0 || h == 0 {
return;
}
let (x, y, w, h) = match clip_rect(x, y, w, h) {
Some(r) => r,
None => return,
};
let bypp = fb.bpp as u32 / 8;
let buf = back_buf();
if bypp == 4 {
let col_bytes = color.to_le_bytes();
for row in y..y + h {
let row_offset = row as usize * fb.pitch as usize + x as usize * 4;
if row_offset + (w as usize) * 4 <= BACKBUFFER_MAX {
for col in 0..w as usize {
let off = row_offset + col * 4;
buf[off..off + 4].copy_from_slice(&col_bytes);
}
}
}
} else if bypp == 3 {
let b0 = (color & 0xFF) as u8;
let b1 = ((color >> 8) & 0xFF) as u8;
let b2 = ((color >> 16) & 0xFF) as u8;
for row in y..y + h {
let row_offset = row as usize * fb.pitch as usize + x as usize * 3;
if row_offset + (w as usize) * 3 <= BACKBUFFER_MAX {
for col in 0..w as usize {
let off = row_offset + col * 3;
buf[off] = b0;
buf[off + 1] = b1;
buf[off + 2] = b2;
}
}
}
}
}
pub fn back_clear(color: u32) {
let (w, h) = (width(), height());
back_fill_rect(0, 0, w, h, color);
}
/// Blit one full row of 0xRRGGBB pixels at line `y` -- the wallpaper
/// cache's fast path (per-pixel `back_set_pixel` over a whole frame is
/// visibly slow under TCG).
pub fn back_blit_row(y: u32, colors: &[u32]) {
let Some(fb) = get() else { return };
if y >= fb.height {
return;
}
let w = (colors.len() as u32).min(fb.width) as usize;
let bypp = fb.bpp as usize / 8;
let row_off = y as usize * fb.pitch as usize;
if row_off + w * bypp > BACKBUFFER_MAX || (bypp != 3 && bypp != 4) {
return;
}
let buf = back_buf();
if bypp == 4 {
for (x, &c) in colors[..w].iter().enumerate() {
buf[row_off + x * 4..row_off + x * 4 + 4].copy_from_slice(&c.to_le_bytes());
}
} else {
for (x, &c) in colors[..w].iter().enumerate() {
let o = row_off + x * 3;
buf[o] = (c & 0xFF) as u8;
buf[o + 1] = ((c >> 8) & 0xFF) as u8;
buf[o + 2] = ((c >> 16) & 0xFF) as u8;
}
}
}
/// Integer square root (largest `r` with `r*r <= v`) -- the corner-arc code
/// below needs sqrt but this crate is no_std with no libm (same constraint
/// `wm_liquid::scale_w_pct` documents), and Newton over u32 converges in a
/// handful of iterations at these magnitudes (radii are tens of pixels).
fn isqrt(v: u32) -> u32 {
if v < 2 {
return v;
}
let mut x = v;
let mut y = (x + 1) / 2;
while y < x {
x = y;
y = (x + v / x) / 2;
}
x
}
/// Horizontal inset of a rounded rect's row `dy` (0-based from the top)
/// for corner radius `r` in a rect of height `h`: 0 for the straight middle
/// rows, quarter-circle-shaped for the top/bottom `r` rows. Shared by the
/// fill and blend variants so the two can never disagree on shape.
pub fn rounded_row_inset(dy: u32, h: u32, r: u32) -> u32 {
let r = r.min(h / 2);
if r == 0 {
return 0;
}
let d = if dy < r {
r - dy
} else if dy >= h - r {
dy - (h - 1 - r)
} else {
return 0;
};
if d > r {
return r;
}
r - isqrt(r * r - d * d)
}
/// Filled rounded rectangle into the back buffer: plain row spans with
/// quarter-circle insets on the top/bottom `radius` rows. No antialiasing
/// -- honest hard-edged spans, same as every other primitive here.
pub fn back_fill_rounded_rect(x: u32, y: u32, w: u32, h: u32, radius: u32, color: u32) {
if h == 0 || w == 0 {
return;
}
for dy in 0..h {
let inset = rounded_row_inset(dy, h, radius).min(w / 2);
back_fill_rect(x + inset, y + dy, w - inset * 2, 1, color);
}
}
/// Filled circle into the back buffer -- row spans via `isqrt`, the same
/// arc math as `rounded_row_inset` without the straight middle section.
pub fn back_fill_circle(cx: u32, cy: u32, radius: u32, color: u32) {
if radius == 0 {
return;
}
let r = radius as i64;
for dy in -r..=r {
let e = isqrt((r * r - dy * dy) as u32) as i64;
let row_y = cy as i64 + dy;
let row_x = cx as i64 - e;
if row_y < 0 || row_x + 2 * e < 0 {
continue;
}
back_fill_rect(row_x.max(0) as u32, row_y as u32, (2 * e + 1) as u32, 1, color);
}
}
/// Alpha-blend one back-buffer pixel: `out = src*a + dst*(255-a)` per
/// channel, `alpha` in 0..=255. Reads the back buffer (not the live
/// framebuffer) so blending composes correctly with everything drawn
/// earlier this frame, before the single `present()` blit.
fn back_blend_pixel(x: u32, y: u32, color: u32, alpha: u32) {
let Some(fb) = get() else { return };
if x >= fb.width || y >= fb.height {
return;
}
if !clip_ok(x, y) {
return;
}
let bypp = fb.bpp as u32 / 8;
let offset = y as usize * fb.pitch as usize + x as usize * bypp as usize;
if offset + bypp as usize > BACKBUFFER_MAX || (bypp != 3 && bypp != 4) {
return;
}
let buf = back_buf();
let inv = 255 - alpha;
for ch in 0..3usize {
let src = (color >> (ch * 8)) & 0xFF;
let dst = buf[offset + ch] as u32;
buf[offset + ch] = ((src * alpha + dst * inv) / 255) as u8;
}
}
/// Alpha-blended rectangle into the back buffer, `alpha` 0..=255 (255 =
/// opaque, use `back_fill_rect` instead there -- this is per-pixel
/// read-modify-write and costs accordingly). This is what makes real
/// drop shadows and glass effects possible; before it existed every
/// "shadow" would have been a fake solid grey.
pub fn back_blend_rect(x: u32, y: u32, w: u32, h: u32, color: u32, alpha: u32) {
let alpha = alpha.min(255);
if alpha == 0 {
return;
}
for dy in 0..h {
for dx in 0..w {
back_blend_pixel(x + dx, y + dy, color, alpha);
}
}
}
/// Alpha-blended rounded rectangle -- the drop-shadow primitive (a dark
/// blended rounded rect offset a few pixels under a window reads as soft
/// shadow even without a real gaussian falloff).
pub fn back_blend_rounded_rect(x: u32, y: u32, w: u32, h: u32, radius: u32, color: u32, alpha: u32) {
let alpha = alpha.min(255);
if alpha == 0 || h == 0 {
return;
}
for dy in 0..h {
let inset = rounded_row_inset(dy, h, radius).min(w / 2);
for dx in inset..w - inset {
back_blend_pixel(x + dx, y + dy, color, alpha);
}
}
}
/// Target present rate ("Hz" in Settings). In a VM the guest can't drive the
/// host's real refresh, so this caps how often the desktop copies the back
/// buffer to the screen -- a genuine, honest frame-rate limit (and it saves
/// CPU). 0 = uncapped.
static mut PRESENT_HZ: u32 = 60;
static mut LAST_PRESENT_US: u64 = 0;
pub fn set_present_hz(hz: u32) {
unsafe { PRESENT_HZ = hz };
}
pub fn present_hz() -> u32 {
unsafe { PRESENT_HZ }
}
pub fn present() {
#[cfg(target_arch = "x86_64")]
unsafe {
let hz = PRESENT_HZ;
if hz > 0 {
let interval = 1_000_000u64 / hz as u64;
let next = LAST_PRESENT_US.wrapping_add(interval);
// Yield to other cooperative tasks until this frame's slot; also
// guard against a backwards/zero clock so we can't spin forever.
let mut spins = 0u32;
while crate::arch::timer::now_us() < next && spins < 100_000 {
crate::proc::sched::yield_now();
spins += 1;
}
LAST_PRESENT_US = crate::arch::timer::now_us();
}
}
unsafe {
let Some(fb) = get() else { return };
let total = (fb.pitch as usize * fb.height as usize).min(BACKBUFFER_MAX);
let dst = fb.addr as *mut u8;
let buf = back_buf();
ptr::copy_nonoverlapping(buf.as_ptr(), dst, total);
}
}