Files
MontaukOS/template/sysroot/include/gui/framebuffer.hpp
T

369 lines
14 KiB
C++

/*
* framebuffer.hpp
* MontaukOS double-buffered framebuffer abstraction
* Copyright (c) 2025 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <montauk/config.h>
#include "gui/gui.hpp"
namespace gui {
class Framebuffer {
uint32_t* hw_fb;
uint32_t* hw_fb2; // second scanout buffer (page flip), nullptr if unsupported
uint32_t* hw_direct; // buffer known to be live after page-flip fallback
uint32_t* hw_mirror; // second possible live buffer when SURFLIVE is unavailable
uint32_t* back_buf;
int fb_width;
int fb_height;
int fb_pitch; // in bytes
int hw_next; // scanout buffer the next flip() will present (page flip only)
static inline void fill_pixels(uint32_t* dst, int count, uint32_t pixel) {
if (!dst || count <= 0) return;
uint64_t pixel64 = ((uint64_t)pixel << 32) | pixel;
if (((uint64_t)dst & 4) && count > 0) {
*dst++ = pixel;
count--;
}
uint64_t* dst64 = (uint64_t*)dst;
int pairs = count / 2;
for (int i = 0; i < pairs; i++) {
dst64[i] = pixel64;
}
if (count & 1) {
((uint32_t*)(dst64 + pairs))[0] = pixel;
}
}
static inline uint32_t blend_pixel(uint32_t dst, uint32_t src, uint32_t alpha) {
uint32_t inv_a = 255 - alpha;
uint32_t sr = (src >> 16) & 0xFF;
uint32_t sg = (src >> 8) & 0xFF;
uint32_t sb = src & 0xFF;
uint32_t dr = (dst >> 16) & 0xFF;
uint32_t dg = (dst >> 8) & 0xFF;
uint32_t db = dst & 0xFF;
uint32_t rr = alpha * sr + inv_a * dr;
uint32_t gg = alpha * sg + inv_a * dg;
uint32_t bb = alpha * sb + inv_a * db;
rr = (rr + 1 + (rr >> 8)) >> 8;
gg = (gg + 1 + (gg >> 8)) >> 8;
bb = (bb + 1 + (bb >> 8)) >> 8;
return 0xFF000000 | (rr << 16) | (gg << 8) | bb;
}
static bool prefer_hardware_page_flip() {
char user[64] = {};
if (montauk::getuser(user, sizeof(user)) > 0 && user[0]) {
auto doc = montauk::config::load_user(user, "display");
bool enabled = doc.get_bool("graphics.tear_free", false);
doc.destroy();
return enabled;
}
auto doc = montauk::config::load("display");
bool enabled = doc.get_bool("graphics.tear_free", false);
doc.destroy();
return enabled;
}
public:
Framebuffer() : hw_fb(nullptr), hw_fb2(nullptr), hw_direct(nullptr), hw_mirror(nullptr),
back_buf(nullptr),
fb_width(0), fb_height(0), fb_pitch(0), hw_next(1) {
montauk::abi::FbInfo info;
montauk::fb_info(&info);
fb_width = (int)info.width;
fb_height = (int)info.height;
fb_pitch = (int)info.pitch;
hw_fb = (uint32_t*)montauk::fb_map();
hw_direct = hw_fb;
back_buf = (uint32_t*)montauk::alloc((uint64_t)fb_height * fb_pitch);
// Hardware page flipping: fb_map() maps the second scanout buffer
// directly after the first when the kernel supports flipping. Acquire
// the scanout before choosing an off-screen buffer: another fullscreen
// process (notably login.elf) may have left either buffer live.
bool flip_supported = hw_fb && montauk::fb_flip(-1, 0) == 1;
uint64_t pages = ((uint64_t)fb_height * fb_pitch + 0xFFF) / 0x1000;
uint32_t* second = flip_supported
? (uint32_t*)((uint8_t*)hw_fb + pages * 0x1000) : nullptr;
if (flip_supported && prefer_hardware_page_flip()) {
int64_t front = montauk::fb_flip(-2, 0);
if (front == 0 || front == 1) {
hw_fb2 = second;
hw_next = (int)front ^ 1;
} else {
// If the live register cannot be trusted even at startup,
// avoid flipping and update both possible scanout surfaces.
hw_mirror = second;
}
} else if (flip_supported) {
// A direct-copy client must make buffer 0 live before it starts
// drawing there; the previous fullscreen client may have left
// buffer 1 scanning out.
if (montauk::fb_flip(0, 1) != 0) {
int64_t front = montauk::fb_flip(-2, 0);
if (front == 1) {
hw_direct = second;
} else if (front != 0) {
hw_mirror = second;
}
}
}
}
int width() const { return fb_width; }
int height() const { return fb_height; }
int pitch() const { return fb_pitch; }
uint32_t* buffer() { return back_buf; }
inline void put_pixel(int x, int y, Color c) {
if (x < 0 || x >= fb_width || y < 0 || y >= fb_height) return;
uint32_t* row = (uint32_t*)((uint8_t*)back_buf + y * fb_pitch);
row[x] = c.to_pixel();
}
inline void put_pixel_alpha(int x, int y, Color c) {
if (x < 0 || x >= fb_width || y < 0 || y >= fb_height) return;
if (c.a == 0) return;
if (c.a == 255) {
put_pixel(x, y, c);
return;
}
uint32_t* row = (uint32_t*)((uint8_t*)back_buf + y * fb_pitch);
uint32_t dst = row[x];
uint8_t dr = (dst >> 16) & 0xFF;
uint8_t dg = (dst >> 8) & 0xFF;
uint8_t db = dst & 0xFF;
uint32_t a = c.a;
uint32_t inv_a = 255 - a;
// Fast alpha blend: out = (src * alpha + dst * (255 - alpha) + 128) / 255
// Approximation: (x + 1 + (x >> 8)) >> 8 for division by 255
uint32_t rr = a * c.r + inv_a * dr;
uint32_t gg = a * c.g + inv_a * dg;
uint32_t bb = a * c.b + inv_a * db;
rr = (rr + 1 + (rr >> 8)) >> 8;
gg = (gg + 1 + (gg >> 8)) >> 8;
bb = (bb + 1 + (bb >> 8)) >> 8;
row[x] = (0xFF000000) | (rr << 16) | (gg << 8) | bb;
}
inline void fill_rect(int x, int y, int w, int h, Color c) {
if (!back_buf) return;
// Clip to screen bounds
int x0 = x < 0 ? 0 : x;
int y0 = y < 0 ? 0 : y;
int x1 = (x + w) > fb_width ? fb_width : (x + w);
int y1 = (y + h) > fb_height ? fb_height : (y + h);
if (x0 >= x1 || y0 >= y1) return;
uint32_t pixel = c.to_pixel();
int clipped_w = x1 - x0;
for (int row = y0; row < y1; row++) {
uint32_t* dst = (uint32_t*)((uint8_t*)back_buf + row * fb_pitch) + x0;
fill_pixels(dst, clipped_w, pixel);
}
}
inline void fill_rect_alpha(int x, int y, int w, int h, Color c) {
if (c.a == 0) return;
if (c.a == 255) {
fill_rect(x, y, w, h, c);
return;
}
int x0 = x < 0 ? 0 : x;
int y0 = y < 0 ? 0 : y;
int x1 = (x + w) > fb_width ? fb_width : (x + w);
int y1 = (y + h) > fb_height ? fb_height : (y + h);
if (x0 >= x1 || y0 >= y1) return;
uint32_t a = c.a;
uint32_t inv_a = 255 - a;
uint32_t src_r = a * c.r;
uint32_t src_g = a * c.g;
uint32_t src_b = a * c.b;
for (int row = y0; row < y1; row++) {
uint32_t* dst = (uint32_t*)((uint8_t*)back_buf + row * fb_pitch) + x0;
for (int col = 0; col < x1 - x0; col++) {
uint32_t d = dst[col];
uint32_t dr = (d >> 16) & 0xFF;
uint32_t dg = (d >> 8) & 0xFF;
uint32_t db = d & 0xFF;
uint32_t rr = src_r + inv_a * dr;
uint32_t gg = src_g + inv_a * dg;
uint32_t bb = src_b + inv_a * db;
rr = (rr + 1 + (rr >> 8)) >> 8;
gg = (gg + 1 + (gg >> 8)) >> 8;
bb = (bb + 1 + (bb >> 8)) >> 8;
dst[col] = (0xFF000000) | (rr << 16) | (gg << 8) | bb;
}
}
}
inline void blit(int x, int y, int w, int h, const uint32_t* pixels) {
if (!back_buf || !pixels || w <= 0 || h <= 0) return;
int src_x = 0;
int src_y = 0;
int dst_x = x;
int dst_y = y;
int copy_w = w;
int copy_h = h;
if (dst_x < 0) { src_x = -dst_x; copy_w += dst_x; dst_x = 0; }
if (dst_y < 0) { src_y = -dst_y; copy_h += dst_y; dst_y = 0; }
if (dst_x + copy_w > fb_width) copy_w = fb_width - dst_x;
if (dst_y + copy_h > fb_height) copy_h = fb_height - dst_y;
if (copy_w <= 0 || copy_h <= 0) return;
uint64_t row_bytes = (uint64_t)copy_w * sizeof(uint32_t);
for (int row = 0; row < copy_h; row++) {
uint32_t* dst = (uint32_t*)((uint8_t*)back_buf + (dst_y + row) * fb_pitch) + dst_x;
const uint32_t* src = pixels + (src_y + row) * w + src_x;
montauk::memcpy(dst, src, row_bytes);
}
}
inline void blit_alpha(int x, int y, int w, int h, const uint32_t* pixels) {
if (!back_buf || !pixels || w <= 0 || h <= 0) return;
int src_x = 0;
int src_y = 0;
int dst_x = x;
int dst_y = y;
int copy_w = w;
int copy_h = h;
if (dst_x < 0) { src_x = -dst_x; copy_w += dst_x; dst_x = 0; }
if (dst_y < 0) { src_y = -dst_y; copy_h += dst_y; dst_y = 0; }
if (dst_x + copy_w > fb_width) copy_w = fb_width - dst_x;
if (dst_y + copy_h > fb_height) copy_h = fb_height - dst_y;
if (copy_w <= 0 || copy_h <= 0) return;
for (int row = 0; row < copy_h; row++) {
uint32_t* dst = (uint32_t*)((uint8_t*)back_buf + (dst_y + row) * fb_pitch) + dst_x;
const uint32_t* src = pixels + (src_y + row) * w + src_x;
for (int col = 0; col < copy_w; col++) {
uint32_t s = src[col];
uint8_t sa = (s >> 24) & 0xFF;
if (sa == 0) continue;
if (sa == 255) {
dst[col] = s;
continue;
}
dst[col] = blend_pixel(dst[col], s, sa);
}
}
}
inline void copy_from(const uint32_t* pixels, int src_pitch_bytes) {
if (!back_buf || !pixels || src_pitch_bytes <= 0) return;
uint64_t row_bytes = (uint64_t)fb_width * sizeof(uint32_t);
for (int row = 0; row < fb_height; row++) {
uint32_t* dst = (uint32_t*)((uint8_t*)back_buf + row * fb_pitch);
const uint32_t* src = (const uint32_t*)((const uint8_t*)pixels + row * src_pitch_bytes);
montauk::memcpy(dst, src, row_bytes);
}
}
inline void clear(Color c) {
fill_rect(0, 0, fb_width, fb_height, c);
}
inline void flip() {
if (!hw_fb || !back_buf) return;
// With hardware page flipping, copy into the scanout buffer that is
// currently OFF screen, then ask the display engine to present it at
// the next vblank (tear-free). Without it, fall back to copying
// straight into the visible framebuffer.
uint32_t* dst_fb = hw_direct;
if (hw_fb2) dst_fb = (hw_next == 1) ? hw_fb2 : hw_fb;
// Copy back buffer to hardware framebuffer, row by row (pitch may differ)
uint64_t row_bytes = (uint64_t)fb_width * sizeof(uint32_t);
for (int y = 0; y < fb_height; y++) {
uint32_t* src = (uint32_t*)((uint8_t*)back_buf + y * fb_pitch);
uint32_t* dst = (uint32_t*)((uint8_t*)dst_fb + y * fb_pitch);
montauk::memcpy(dst, src, row_bytes);
if (hw_mirror) {
uint32_t* mirror = (uint32_t*)((uint8_t*)hw_mirror + y * fb_pitch);
montauk::memcpy(mirror, src, row_bytes);
}
}
if (hw_mirror) asm volatile("sfence" ::: "memory");
if (hw_fb2) {
// Wait for the latch (vsync): the buffer we just left becomes
// safe to draw into only once the new one is actually scanning.
asm volatile("sfence" ::: "memory");
int64_t presented = montauk::fb_flip(hw_next, 1);
if (presented == hw_next) {
hw_next ^= 1;
} else {
// Do not retry a faulty page-flip path forever: that turns a
// transient latch failure into an apparently frozen desktop.
// Keep drawing directly into whichever buffer hardware says
// is live for the rest of this process. If SURFLIVE itself is
// unavailable, request buffer 0 without another blocking wait.
int64_t front = montauk::fb_flip(-2, 0);
if (front == 0 || front == 1) {
hw_direct = (front == 1) ? hw_fb2 : hw_fb;
} else {
(void)montauk::fb_flip(0, 0);
hw_direct = hw_fb;
hw_mirror = hw_fb2;
}
hw_fb2 = nullptr;
// The frame above was copied to the requested (possibly
// off-screen) buffer. Publish it once more to the live direct
// buffer so the cursor and desktop resume immediately.
for (int y = 0; y < fb_height; y++) {
uint32_t* src = (uint32_t*)((uint8_t*)back_buf + y * fb_pitch);
uint32_t* dst = (uint32_t*)((uint8_t*)hw_direct + y * fb_pitch);
montauk::memcpy(dst, src, row_bytes);
if (hw_mirror) {
uint32_t* mirror = (uint32_t*)((uint8_t*)hw_mirror + y * fb_pitch);
montauk::memcpy(mirror, src, row_bytes);
}
}
asm volatile("sfence" ::: "memory");
}
}
}
};
} // namespace gui