From 554711507ae16d9a7fca9452459c4ba5db6e00eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Sun, 6 Sep 2026 19:48:09 +0200 Subject: [PATCH 01/17] Adds stub GPU server tool project --- src/tools/gpu-server/build.zig | 32 +++++++++++++++++++++++++ src/tools/gpu-server/build.zig.zon | 18 ++++++++++++++ src/tools/gpu-server/src/gpu-server.zig | 22 +++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 src/tools/gpu-server/build.zig create mode 100644 src/tools/gpu-server/build.zig.zon create mode 100644 src/tools/gpu-server/src/gpu-server.zig diff --git a/src/tools/gpu-server/build.zig b/src/tools/gpu-server/build.zig new file mode 100644 index 00000000..47d3fbe5 --- /dev/null +++ b/src/tools/gpu-server/build.zig @@ -0,0 +1,32 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const abi_dep = b.dependency("abi", .{}); + const agp_dep = b.dependency("agp", .{}); + const agp_swrast_dep = b.dependency("agp_swrast", .{}); + + const abi_mod = abi_dep.module("ashet-abi"); + const agp_mod = agp_dep.module("agp"); + const agp_swrast_mod = agp_swrast_dep.module("agp-swrast"); + + const server_mod = b.createModule(.{ + .root_source_file = b.path("src/gpu-server.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "agp", .module = agp_mod }, + .{ .name = "agp-swrast", .module = agp_swrast_mod }, + .{ .name = "ashet", .module = abi_mod }, + }, + }); + + const server_exe = b.addExecutable(.{ + .name = "gpu-server", + .root_module = server_mod, + }); + + b.installArtifact(server_exe); +} diff --git a/src/tools/gpu-server/build.zig.zon b/src/tools/gpu-server/build.zig.zon new file mode 100644 index 00000000..c41c78ae --- /dev/null +++ b/src/tools/gpu-server/build.zig.zon @@ -0,0 +1,18 @@ +.{ + .name = .gpu_server, + .version = "0.1.0", + .fingerprint = 0xfa90c655f6289a63, + .paths = .{""}, + + .dependencies = .{ + .abi = .{ + .path = "../../abi", + }, + .agp = .{ + .path = "../../userland/libs/agp", + }, + .agp_swrast = .{ + .path = "../../userland/libs/agp-swrast", + }, + }, +} diff --git a/src/tools/gpu-server/src/gpu-server.zig b/src/tools/gpu-server/src/gpu-server.zig new file mode 100644 index 00000000..e6861c2e --- /dev/null +++ b/src/tools/gpu-server/src/gpu-server.zig @@ -0,0 +1,22 @@ +const std = @import("std"); +const ashet = @import("ashet"); +const agp = @import("agp"); + +const Size = ashet.Size; + +pub fn main() !void { + + // + +} + +const GpuOptions = struct { + /// Number of bytes of video memory + vmem: usize, + + outputs: std.ArrayListUnmanaged(Output), + + const Output = struct { + size: Size, + }; +}; From 2adbb270b24e4e261ff30d39edadc9840a9b5687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Sun, 6 Sep 2026 21:45:30 +0200 Subject: [PATCH 02/17] First draft of shitty AVAP support in the kernel --- justfile | 8 + src/kernel/drivers/drivers.zig | 2 + .../drivers/video/AVAPv1_Framebuffer.zig | 347 ++++++++++++++++++ src/kernel/drivers/video/avap/AVAPv1.zig | 0 src/kernel/port/hosted/initialize.zig | 18 + 5 files changed, 375 insertions(+) create mode 100644 src/kernel/drivers/video/AVAPv1_Framebuffer.zig create mode 100644 src/kernel/drivers/video/avap/AVAPv1.zig diff --git a/justfile b/justfile index fd79be6b..21a0d920 100644 --- a/justfile +++ b/justfile @@ -12,6 +12,14 @@ build: {{zig}} build {{default_params}} --summary none -Doptimize-kernel={{optimize_kernel}} -Doptimize-apps={{optimize_apps}} rv32-qemu-virt {{zig}} build {{default_params}} --summary none -Doptimize-kernel={{optimize_kernel}} -Doptimize-apps={{optimize_apps}} +run-avap-simulation: + {{zig}} build {{default_params}} --summary none -Doptimize-kernel={{optimize_kernel}} -Doptimize-apps={{optimize_apps}} tools x86-hosted-linux + + zig-out/bin/debug-filter --elf kernel=./zig-out/x86-hosted-linux/kernel.elf \ + ./zig-out/x86-hosted-linux/kernel.elf \ + "drive;zig-out/x86-hosted-linux/disk.img" \ + "video;avap-v1;640;400;/dev/serial/by-id/usb-Ashet_Technologies_Fast_Bridge_AT-FB-00001-if00-port0" + [working-directory: 'src/kernel'] build-kernel: {{zig}} build {{default_params}} -Dmachine=arm-ashet-hc -Dno-emit-bin diff --git a/src/kernel/drivers/drivers.zig b/src/kernel/drivers/drivers.zig index 2a084fca..5d509667 100644 --- a/src/kernel/drivers/drivers.zig +++ b/src/kernel/drivers/drivers.zig @@ -52,6 +52,8 @@ pub const video = struct { pub const Multiboot_Framebuffer = @import("video/Multiboot_Framebuffer.zig"); pub const Memory_Mapped_Framebuffer = @import("video/Memory_Mapped_Framebuffer.zig"); pub const Ashet_Framebuffer = @import("video/Ashet_Framebuffer.zig"); + + pub const AVAPv1_Framebuffer = @import("video/AVAPv1_Framebuffer.zig"); }; pub const network = struct { diff --git a/src/kernel/drivers/video/AVAPv1_Framebuffer.zig b/src/kernel/drivers/video/AVAPv1_Framebuffer.zig new file mode 100644 index 00000000..cd4855af --- /dev/null +++ b/src/kernel/drivers/video/AVAPv1_Framebuffer.zig @@ -0,0 +1,347 @@ +const std = @import("std"); +const ashet = @import("../../main.zig"); +const logger = std.log.scoped(.ashet_fb); +const machine = ashet.machine.peripherals; + +const AVAPv1_Framebuffer = @This(); +const Driver = ashet.drivers.Driver; +const Color = ashet.abi.Color; +const Resolution = ashet.abi.Size; + +pub const width = 640; +pub const height = 400; + +driver: Driver = .{ + .name = "AVAPv1 Framebuffer", + .class = .{ + .video = .{ + .get_properties_fn = get_properties, + .flush_fn = flush, + }, + }, +}, + +framebuffer: [256_000]Color align(ashet.memory.page_size), +device: std.fs.File, + +pub fn init( + file_name: []const u8, +) error{ FileNotFound, BadFile, DeviceUnresponsive, IoError }!AVAPv1_Framebuffer { + var fb: AVAPv1_Framebuffer = .{ + .framebuffer = @splat(.black), + .device = undefined, + }; + + fb.device = std.fs.cwd().openFile(file_name, .{ .mode = .read_write }) catch |err| switch (err) { + error.FileNotFound, + => return error.FileNotFound, + + error.BadPathName, + error.IsDir, + error.NoDevice, + => return error.BadFile, + + error.SystemResources, + error.WouldBlock, + error.AccessDenied, + error.ProcessNotFound, + error.Unexpected, + error.PermissionDenied, + error.SharingViolation, + error.PathAlreadyExists, + error.PipeBusy, + error.NameTooLong, + error.InvalidUtf8, + error.InvalidWtf8, + error.NetworkNotFound, + error.AntivirusInterference, + error.SymLinkLoop, + error.ProcessFdQuotaExceeded, + error.SystemFdQuotaExceeded, + error.FileTooBig, + error.NoSpaceLeft, + error.NotDir, + error.DeviceBusy, + error.FileLocksNotSupported, + error.FileBusy, + => return error.IoError, + }; + errdefer fb.device.close(); + + const ack = ping(fb.device) catch |err| switch (err) { + error.InputOutput, + error.SystemResources, + error.IsDir, + error.OperationAborted, + error.BrokenPipe, + error.ConnectionResetByPeer, + error.ConnectionTimedOut, + error.NotOpenForReading, + error.SocketNotConnected, + error.WouldBlock, + error.Canceled, + error.AccessDenied, + error.ProcessNotFound, + error.LockViolation, + error.Unexpected, + error.PermissionDenied, + error.Overflow, + error.NoDevice, + error.FileTooBig, + error.NoSpaceLeft, + error.DeviceBusy, + error.DiskQuota, + error.InvalidArgument, + error.NotOpenForWriting, + error.MessageTooBig, + => return error.IoError, + + error.Timeout, + => return error.DeviceUnresponsive, + }; + if (!ack) { + logger.err("device did not respond to ping", .{}); + return error.DeviceUnresponsive; + } + + ashet.video.load_splash_screen(.{ + .base = &fb.framebuffer, + .width = 640, + .height = 400, + .stride = 640, + }); + + fb.flush_with_error() catch |err| switch (err) { + error.InputOutput, + error.SystemResources, + error.IsDir, + error.OperationAborted, + error.BrokenPipe, + error.ConnectionResetByPeer, + error.ConnectionTimedOut, + error.NotOpenForReading, + error.SocketNotConnected, + error.WouldBlock, + error.Canceled, + error.AccessDenied, + error.ProcessNotFound, + error.LockViolation, + error.Unexpected, + error.PermissionDenied, + error.Overflow, + error.NoDevice, + error.FileTooBig, + error.NoSpaceLeft, + error.DeviceBusy, + error.DiskQuota, + error.InvalidArgument, + error.NotOpenForWriting, + error.MessageTooBig, + => return error.IoError, + + error.Timeout, + => return error.DeviceUnresponsive, + + error.WriteBufferFailed => { + logger.err("write buffers failed", .{}); + return error.DeviceUnresponsive; + }, + + error.SwapBuffersFailed => { + logger.err("swap buffers failed", .{}); + return error.DeviceUnresponsive; + }, + }; + + return fb; +} + +fn get_properties(driver: *Driver) ashet.video.DeviceProperties { + const vd = driver.resolve(AVAPv1_Framebuffer, "driver"); + return .{ + .video_memory = &vd.framebuffer, + .video_memory_mapping = .buffered, + .stride = width, + .resolution = .{ + .width = width, + .height = height, + }, + }; +} + +fn flush(driver: *Driver) void { + const vd = driver.resolve(AVAPv1_Framebuffer, "driver"); + + vd.flush_with_error() catch |err| { + logger.err("video driver failure: {t}", .{err}); + }; +} + +fn flush_with_error(vd: *AVAPv1_Framebuffer) !void { + logger.debug("write buffer", .{}); + try write_buffer(vd.device, 0, @ptrCast(&vd.framebuffer)); + + logger.debug("swap buffers", .{}); + try swap_buffers(vd.device); +} + +fn ping(port: std.fs.File) !bool { + try write_command(port, .ping, ""); + + const deadline: Deadline = .from_ms(100); + + const header = try read_header(port, deadline); + try read_discarding(port, header.length, deadline); + try read_footer(port, header, deadline); + + return header.ack; +} + +pub fn write_buffer(port: std.fs.File, offset: u32, buffer: []const u8) !void { + const length: u32 = std.math.cast(u32, buffer.len +| 4) orelse return error.Overflow; + + try write_header(port, length, .ping); + + try write_all(port, std.mem.asBytes(&std.mem.nativeToLittle(u32, offset))); + try write_all(port, buffer); + + try write_footer(port, length); + + const deadline: Deadline = .from_ms(100); + + const header = try read_header(port, deadline); + try read_discarding(port, header.length, deadline); + try read_footer(port, header, deadline); + + if (header.ack == false) + return error.WriteBufferFailed; +} + +pub fn swap_buffers(port: std.fs.File) !void { + try write_command(port, .swap_buffer, ""); + + const deadline: Deadline = .from_ms(100); + + const header = try read_header(port, deadline); + try read_discarding(port, header.length, deadline); + try read_footer(port, header, deadline); + + if (header.ack == false) + return error.SwapBuffersFailed; +} + +fn write_command(port: std.fs.File, cmd: Command, buffer: []const u8) !void { + try write_header(port, buffer.len, cmd); + if (buffer.len > 0) { + try write_all(port, buffer); + } + try write_footer(port, buffer.len); +} + +const Command = enum(u7) { + ping = 0, + write_buffer = 1, + swap_buffer = 2, + update_palette = 3, + await_vblank = 4, + write_rectangle = 5, +}; +const Header = packed struct(u32) { + length: u24, + cmd: Command, + ack: bool, +}; + +fn write_header(port: std.fs.File, length: usize, cmd: Command) !void { + const enc = Header{ + .length = std.math.cast(u24, length) orelse return error.Overflow, + .cmd = cmd, + .ack = false, + }; + + var cmd_buf: [4]u8 = undefined; + std.mem.writeInt(u32, &cmd_buf, @bitCast(enc), .little); + try write_all(port, &cmd_buf); +} + +fn write_footer(port: std.fs.File, total_length: usize) !void { + const overhead = compute_padding(total_length); + if (overhead > 0) { + const padding: [4]u8 = @splat(0); + try write_all(port, padding[0..overhead]); + } +} + +fn read_header(port: std.fs.File, deadline: Deadline) !Header { + var buffer: [4]u8 = undefined; + try read_all(port, &buffer, deadline); + return @bitCast(std.mem.readInt(u32, &buffer, .little)); +} + +fn read_footer(port: std.fs.File, response: Header, deadline: Deadline) !void { + const overhead = compute_padding(response.length); + try read_discarding(port, overhead, deadline); +} + +fn read_discarding(port: std.fs.File, length: usize, deadline: Deadline) !void { + var buffer: [8192]u8 = undefined; + + var count: usize = 0; + while (count < length) { + try deadline.check(); + + const limit = @min(buffer.len, length - count); + const len = try port.read(buffer[0..limit]); + count += len; + } +} + +fn compute_padding(total_length: usize) usize { + const aligned = std.mem.alignForward(usize, total_length, 4); + return aligned - total_length; +} + +fn write_all(port: std.fs.File, buffer: []const u8) !void { + logger.debug("write {d} bytes", .{buffer.len}); + + try port.writeAll(buffer); +} + +fn read_all(port: std.fs.File, buffer: []u8, deadline: Deadline) !void { + logger.debug("read {d} bytes", .{buffer.len}); + + var offset: usize = 0; + while (offset < buffer.len) { + try deadline.check(); + + const len = try port.read(buffer[offset..]); + + if (len > 0) { + logger.debug(" .. {x}", .{buffer[offset .. offset + len]}); + } + + offset += len; + } +} + +const Deadline = struct { + pub const infinite: Deadline = .{ .end = null, .duration = 0 }; + + start: ?std.time.Instant, + duration: u64, + + pub fn from_ms(ms: u64) Deadline { + return .{ + .start = std.time.Instant.now() catch @panic("unsupported system"), + .duration = std.time.ns_per_ms * ms, + }; + } + + pub fn check(deadline: Deadline) !void { + const start = deadline.start orelse return; + + const now = std.time.Instant.now() catch unreachable; + if (now.since(start) >= deadline.duration) + return error.Timeout; + } +}; diff --git a/src/kernel/drivers/video/avap/AVAPv1.zig b/src/kernel/drivers/video/avap/AVAPv1.zig new file mode 100644 index 00000000..e69de29b diff --git a/src/kernel/port/hosted/initialize.zig b/src/kernel/port/hosted/initialize.zig index 8bbb1a74..8b20b83e 100644 --- a/src/kernel/port/hosted/initialize.zig +++ b/src/kernel/port/hosted/initialize.zig @@ -60,6 +60,7 @@ pub fn initialize(comptime video_drivers: std.StaticStringMap(VideoDriverCtor)) "dummy", "vnc", "sdl", + "avap-v1", }; comptime for (shared_video_drivers) |dri| { @@ -158,6 +159,19 @@ pub fn initialize(comptime video_drivers: std.StaticStringMap(VideoDriverCtor)) const driver = try global_memory.create(ashet.drivers.video.Virtual_Video_Output); driver.* = ashet.drivers.video.Virtual_Video_Output.init(); ashet.drivers.install(&driver.driver); + } else if (std.mem.eql(u8, device_type, "avap-v1")) { + if (res_x != 640 or res_y != 400) badKernelOption("video", "AVAPv1 resolution must be 640x400!", .{}); + + const serial_device = iter.next() orelse badKernelOption("video", "missing AVAPv1 serial port", .{}); + + const driver = try global_memory.create(ashet.drivers.video.AVAPv1_Framebuffer); + driver.* = ashet.drivers.video.AVAPv1_Framebuffer.init(serial_device) catch |err| switch (err) { + error.BadFile => badKernelOption("video", "bad file: {s}", .{serial_device}), + error.DeviceUnresponsive => badKernelOption("video", "AVAP device not responsive", .{}), + error.FileNotFound => badKernelOption("video", "missing file: {s}", .{serial_device}), + error.IoError => badKernelOption("video", "io error on {s}", .{serial_device}), + }; + ashet.drivers.install(&driver.driver); } else if (video_drivers.get(device_type)) |video_driver_ctor| { try video_driver_ctor(.{ .video_out_index = video_out_index, @@ -168,6 +182,10 @@ pub fn initialize(comptime video_drivers: std.StaticStringMap(VideoDriverCtor)) badKernelOption("video", "bad video device type '{s}'", .{device_type}); } + if (iter.next()) |option| badKernelOption("video", "unexpected option \"{f}\"", .{ + std.zig.fmtString(option), + }); + video_out_index += 1; } else { badKernelOption(component, "does not exist", .{}); From 9c5f3e3a9cce0962623a7c8d397a3a3cc08a49e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Sun, 6 Sep 2026 21:54:45 +0200 Subject: [PATCH 03/17] Fixes stupid typo in AVAP driver. --- .../drivers/video/AVAPv1_Framebuffer.zig | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/kernel/drivers/video/AVAPv1_Framebuffer.zig b/src/kernel/drivers/video/AVAPv1_Framebuffer.zig index cd4855af..ada465c4 100644 --- a/src/kernel/drivers/video/AVAPv1_Framebuffer.zig +++ b/src/kernel/drivers/video/AVAPv1_Framebuffer.zig @@ -191,7 +191,7 @@ fn ping(port: std.fs.File) !bool { const deadline: Deadline = .from_ms(100); const header = try read_header(port, deadline); - try read_discarding(port, header.length, deadline); + try read_discarding(port, header.length, deadline, .log); try read_footer(port, header, deadline); return header.ack; @@ -200,7 +200,7 @@ fn ping(port: std.fs.File) !bool { pub fn write_buffer(port: std.fs.File, offset: u32, buffer: []const u8) !void { const length: u32 = std.math.cast(u32, buffer.len +| 4) orelse return error.Overflow; - try write_header(port, length, .ping); + try write_header(port, length, .write_buffer); try write_all(port, std.mem.asBytes(&std.mem.nativeToLittle(u32, offset))); try write_all(port, buffer); @@ -210,7 +210,7 @@ pub fn write_buffer(port: std.fs.File, offset: u32, buffer: []const u8) !void { const deadline: Deadline = .from_ms(100); const header = try read_header(port, deadline); - try read_discarding(port, header.length, deadline); + try read_discarding(port, header.length, deadline, .log); try read_footer(port, header, deadline); if (header.ack == false) @@ -223,7 +223,7 @@ pub fn swap_buffers(port: std.fs.File) !void { const deadline: Deadline = .from_ms(100); const header = try read_header(port, deadline); - try read_discarding(port, header.length, deadline); + try read_discarding(port, header.length, deadline, .log); try read_footer(port, header, deadline); if (header.ack == false) @@ -280,10 +280,10 @@ fn read_header(port: std.fs.File, deadline: Deadline) !Header { fn read_footer(port: std.fs.File, response: Header, deadline: Deadline) !void { const overhead = compute_padding(response.length); - try read_discarding(port, overhead, deadline); + try read_discarding(port, overhead, deadline, .ignore); } -fn read_discarding(port: std.fs.File, length: usize, deadline: Deadline) !void { +fn read_discarding(port: std.fs.File, length: usize, deadline: Deadline, output: enum { ignore, log }) !void { var buffer: [8192]u8 = undefined; var count: usize = 0; @@ -292,6 +292,11 @@ fn read_discarding(port: std.fs.File, length: usize, deadline: Deadline) !void { const limit = @min(buffer.len, length - count); const len = try port.read(buffer[0..limit]); + + if (len > 0 and output == .log) { + logger.err("unexpected data from device: {x}", .{buffer[0..len]}); + } + count += len; } } @@ -302,13 +307,13 @@ fn compute_padding(total_length: usize) usize { } fn write_all(port: std.fs.File, buffer: []const u8) !void { - logger.debug("write {d} bytes", .{buffer.len}); + // logger.debug("write {d} bytes", .{buffer.len}); try port.writeAll(buffer); } fn read_all(port: std.fs.File, buffer: []u8, deadline: Deadline) !void { - logger.debug("read {d} bytes", .{buffer.len}); + // logger.debug("read {d} bytes", .{buffer.len}); var offset: usize = 0; while (offset < buffer.len) { @@ -316,9 +321,9 @@ fn read_all(port: std.fs.File, buffer: []u8, deadline: Deadline) !void { const len = try port.read(buffer[offset..]); - if (len > 0) { - logger.debug(" .. {x}", .{buffer[offset .. offset + len]}); - } + // if (len > 0) { + // logger.debug(" .. {x}", .{buffer[offset .. offset + len]}); + // } offset += len; } From 21210790727838cecc91b45fb8e188ec4c716176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Sun, 6 Sep 2026 21:56:52 +0200 Subject: [PATCH 04/17] Makes AVAP driver not miserably fail --- src/kernel/components/video.zig | 2 +- src/kernel/drivers/video/AVAPv1_Framebuffer.zig | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/kernel/components/video.zig b/src/kernel/components/video.zig index 1d8e068d..9badb3ad 100644 --- a/src/kernel/components/video.zig +++ b/src/kernel/components/video.zig @@ -53,7 +53,7 @@ pub const Output = struct { system_resource: ashet.resources.SystemResource = .{ .type = .video_output }, /// If true, the kernel will automatically flush the screen in a background process. - auto_flush: bool = false, + auto_flush: bool = true, // TODO: Fix this flush_required: bool = false, video_driver: *ashet.drivers.VideoDevice, diff --git a/src/kernel/drivers/video/AVAPv1_Framebuffer.zig b/src/kernel/drivers/video/AVAPv1_Framebuffer.zig index ada465c4..badef1ea 100644 --- a/src/kernel/drivers/video/AVAPv1_Framebuffer.zig +++ b/src/kernel/drivers/video/AVAPv1_Framebuffer.zig @@ -178,10 +178,10 @@ fn flush(driver: *Driver) void { } fn flush_with_error(vd: *AVAPv1_Framebuffer) !void { - logger.debug("write buffer", .{}); + // logger.debug("write buffer", .{}); try write_buffer(vd.device, 0, @ptrCast(&vd.framebuffer)); - logger.debug("swap buffers", .{}); + // logger.debug("swap buffers", .{}); try swap_buffers(vd.device); } From 820bfb7c142773dd391741f1fda8c519e9d7d21b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Mon, 7 Sep 2026 20:25:21 +0200 Subject: [PATCH 05/17] Introduces new video kernel API, invalidates old implicit-buffer-backed design. Does not change kernel internals. --- src/abi/db/abi-id-db.json | 84 ++++++++ src/abi/src/ashet.abi | 306 ++++++++++++++++++++++++--- src/abi/src/ports/zig.abi.zpatch | 6 +- src/kernel/components/overlapped.zig | 2 + src/kernel/components/resources.zig | 3 +- src/kernel/components/syscalls.zig | 31 ++- src/kernel/components/video.zig | 34 ++- 7 files changed, 412 insertions(+), 54 deletions(-) diff --git a/src/abi/db/abi-id-db.json b/src/abi/db/abi-id-db.json index a12c0545..63bff4d5 100644 --- a/src/abi/db/abi-id-db.json +++ b/src/abi/db/abi-id-db.json @@ -2443,6 +2443,90 @@ { "fqn": "Rectangle.everything", "uid": 611 + }, + { + "fqn": "video.VideoOutputID", + "uid": 612 + }, + { + "fqn": "video.VideoOutput", + "uid": 613 + }, + { + "fqn": "video.acquire.AlreadyExists", + "uid": 614 + }, + { + "fqn": "video.acquire.InvalidId", + "uid": 615 + }, + { + "fqn": "video.PresentMode", + "uid": 616 + }, + { + "fqn": "video.WritePixels.InvalidHandle", + "uid": 617 + }, + { + "fqn": "video.WritePixels.BufferSize", + "uid": 618 + }, + { + "fqn": "video.WritePixels.InvalidStride", + "uid": 619 + }, + { + "fqn": "video.WritePixels.InvalidRegion", + "uid": 620 + }, + { + "fqn": "video.WritePixels", + "uid": 621 + }, + { + "fqn": "video.BufferMapping", + "uid": 622 + }, + { + "fqn": "video.BufferKind", + "uid": 623 + }, + { + "fqn": "video.create_buffer_mapping.InvalidHandle", + "uid": 624 + }, + { + "fqn": "video.create_buffer_mapping.Unsupported", + "uid": 625 + }, + { + "fqn": "video.create_buffer_mapping.AlreadyExists", + "uid": 626 + }, + { + "fqn": "video.create_buffer_mapping.SystemResources", + "uid": 627 + }, + { + "fqn": "video.create_buffer_mapping", + "uid": 628 + }, + { + "fqn": "video.Present.InvalidHandle", + "uid": 629 + }, + { + "fqn": "video.Present", + "uid": 630 + }, + { + "fqn": "video.VideoMemory", + "uid": 631 + }, + { + "fqn": "video.acquire.OutputInUse", + "uid": 632 } ] } \ No newline at end of file diff --git a/src/abi/src/ashet.abi b/src/abi/src/ashet.abi index cf79048e..ad98e3ea 100644 --- a/src/abi/src/ashet.abi +++ b/src/abi/src/ashet.abi @@ -381,52 +381,313 @@ namespace datetime { /// Earliest possible date time of when the alarm triggers. in when: DateTime; } - } +/// This namespace contains items related to presenting visual data over a video adapter. namespace video { - /// Returns a list of all video outputs. + /// Index of the systems video outputs. + enum VideoOutputID : u16 { + /// The primary video output + item primary = 0; + + ... + } + + /// Enumerates the list of available video outputs. /// - /// If @`ids` is `null`, the total number of available outputs is returned; - /// otherwise, up to `ids.len` elements are written into the provided array - /// and the number of written elements is returned. + /// NOTE: This list is ephemeral and may change. A video output ID + /// is a stable identifier for a video output, but is not necessarily + /// valid anymore when used later, as the video adapter could've been + /// unplugged. syscall enumerate { + /// A buffer that will receive the available video output IDs. + /// + /// If `null`, the kernel will not attempt to write this list. + /// + /// If not `null`, the kernel will fill the buffer up to `count` elements + /// or until the buffer is full. in ids: ?[]VideoOutputID; + + /// The total number of available video outputs. + /// + /// NOTE: This may exceed the length of @`ids`, but the kernel will + /// never write memory beyond the capacity of @`ids`. out count: usize; } + /// The video output resource is an exclusive access token to a + /// video output. + /// + /// It allows updating the displayed pixel data and waiting for the + /// vertical blanking interval of the display data. + resource VideoOutput { } + /// Acquire exclusive access to a video output. syscall acquire { in output_id: VideoOutputID; + + /// The resource created from `output_id`. out output: VideoOutput; - error NotAvailable; - error NotFound; + + /// Exclusive access is already held for the video output identified by `output_id`. + error OutputInUse; + + /// `output_id` is not a valid video output id. + error InvalidId; + error SystemResources; } - /// Returns the current resolution + /// Returns the resolution of `output` in pixels. + /// + /// NOTE: Currently Ashet OS auto-selects a resolution for each adapter + /// which cannot be changed by the user. The system guarantees that + /// the resolution stays that way for the lifetime of the `output` + /// object. + /// + /// This might change later, but requires some re-engineering of the + /// APIs as buffers would need re-allocation. syscall get_resolution { in output: VideoOutput; + out resolution: Size; - error InvalidHandle; - } - /// Returns a pointer to linear video memory, row-major. - /// Pixels rows will have a stride of the current video buffer width. - /// The first pixel in the memory is the top-left pixel. - syscall get_video_memory { - in output: VideoOutput; - out memory: VideoMemory; - error InvalidHandle; + /// `output` is not a valid video output resource. + error InvalidHandle; } /// Completes when the video output has fully scanned out an image and is now performing the v-blanking. /// /// This allows frame-synchronized presentation of video data. + /// + /// NOTE: All scheduled `WaitForVBlank` operations complete at the start of the next vertical blanking period. + /// + /// This means that a schedule during the current vertical blanking period does not immediately complete + /// the operation, but delays by nearly a full frame. + /// + /// NOTE: Depending on the transport method of the video output, the completion may have additional unknown latencies. async_call WaitForVBlank { in output: VideoOutput; + + /// `output` is not a valid video output resource. error InvalidHandle; } + + /// Specifies how `WritePixels` will upload the pixels. + enum PresentMode : u8 { + /// The pixel data is written immediately. + /// + /// NOTE: This mode will immediately upload the pixel data and + /// will not await a vertical blanking period. This means the + /// upload is likely to create visual glitches or tearing. + item immediate = 0; + + /// The kernel attempts a tearing free upload of the pixel data. + /// + /// This means the kernel attempts to align the upload with the + /// vertical blanking period. + /// + /// NOTE: This mode is best-effort, and does not guarantee the + /// video data is uploaded tearing-free. + item vblank = 1; + } + + /// Uploads pixels to a video output. + /// + /// NOTE: If `destination` would update a zero-sized area (`width` or `height` is zero), + /// the operation is a no-op and completes immediately. + /// + /// LORE: Originally, we had the ability to directly get a pointer + /// to the video outputs buffer. + /// As convenient as it is, it implicitly imposed the requirement + /// for the kernel to potentially allocate a pixel buffer if the + /// video output cannot actually provide the video memory inside + /// the systems main memory. + /// + /// This forced the kernel to periodically upload an allocated buffer + /// to external video devices, which is both inefficient and error prone. + /// + /// This syscall + `Buffer` sidestep this problem by making the access of a + /// memory-mapped video memory fallible without removing the ability for a generic + /// upload procedure. + async_call WritePixels { + /// The output which should receive the pixel data. + in output: VideoOutput; + + /// The portion of the video buffer that should be updated. + in destination: Rectangle; + + /// Pointer to the top-left pixel of `destination`. + /// + /// NOTE: The order inside this array is row-major. + /// This means that `pixels[1]` is the pixel at `(destination.x + 1, destination.y)` + /// and `pixels[stride]` is the pixel at `(destination.x, destination.y + 1)`. + /// + /// NOTE: Each scanline starts at `y * stride` elements apart and the buffer must contain + /// at least `destination.height` scanlines. + in pixels: []const Color; + + /// The length of a scanline in `pixels` in elements. + in stride: usize; + + /// Determines when to perform the pixel data write. + in mode: PresentMode; + + /// `output` is not a valid video output resource. + error InvalidHandle; + + /// Returned when `pixels` does not hold enough pixels to update `destination`. + /// + /// This means that `pixels.len` is less than `stride * max(0, destination.height - 1) + destination.width`. + /// + /// NOTE: This error is only returned if `destination.height > 0`. + error BufferSize; + + /// `stride` is less than `destination.width`. + error InvalidStride; + + /// `destination` is outside the actual video buffer resolution. + error InvalidRegion; + } + + /// A buffer mapping provides a memory-mapped view into a + /// front- or backbuffer of a video output. + /// + /// This allows uploading pixel data without the need for a `WritePixels` operation. + /// + /// NOTE: Not every `VideoOutput` supports a buffer mapping. + resource BufferMapping { } + + enum BufferKind : u8 { + /// A front buffer uses the same data as the scanout mechanism. + /// This means that any write to this buffer is *directly* visible + /// as soon as the video output scans out the written pixel locations. + /// + /// NOTE: This means that writes may produce tearing or other visual + /// glitches. + /// + /// NOTE: `Present` is not required to make the changes visible. + item front_buffer = 0; + + /// A back buffer is a second buffer that is not used for scanning out + /// pixel data. + /// + /// This means that writes to a back buffer will never appear on the + /// video output unless the buffer is swapped/copied to the front buffer. + /// + /// To perform this copy/swap, the `Present` operation shall be used. + /// + /// NOTE: It is possible, but not recommended to perform a manual copy + /// from a back buffer mapping to a front buffer mapping. + item back_buffer = 1; + } + + /// Creates a memory mapping for the front or the back buffer of a video output. + /// + /// NOTE: Not every video output supports memory mappings at all. Some video outputs + /// only support a single mode of memory mapping. + /// + /// The supported combinations are: + /// - No mapping support. + /// - Only front buffer. + /// - Only back buffer. + /// - Both front and back buffer. + /// + /// When a buffer type is not supported, `Unsupported` is returned. + /// + /// NOTE: There can be only a single mapping for the front and the back buffer. + /// This means for each video output, a maximum of two `BufferMapping` resources + /// can exist. + /// + /// NOTE: A buffer mapping is implicitly destroyed when its associated video output is + /// destroyed. This is necessary as the destruction of the video output resource + /// revokes access to the video device, and thus also revokes access through memory + /// mappings. + syscall create_buffer_mapping { + in output: VideoOutput; + + /// Which buffer should be mapped. + in requested_kind: BufferKind; + + out buffer: BufferMapping; + + /// `output` is not a valid video output resource. + error InvalidHandle; + + /// The requested buffer type is not supported by the `output` device. + error Unsupported; + + /// A buffer mapping for the `requested_kind` of the video output + /// already exists. + error AlreadyExists; + + error SystemResources; + } + + /// Applies the changes inside `buffer` and guarantees they + /// are visible afterwards. + /// + /// NOTE: For a front buffer, no data movement will happen, but + /// `mode` may still make `Present` await the next vertical blanking + /// period. + /// + /// NOTE: It is not specified if a `Present` for a back buffer is performing a + /// buffer swap operation or a buffer copy operation. + /// + /// NOTE: If `mode == PresentMode.immediate` and `buffer` is a front buffer, the + /// operation completes immediately. + async_call Present + { + /// The buffer mapping that shall be presented. + in buffer: BufferMapping; + + /// Determines when to perform the pixel data update. + in mode: PresentMode; + + /// `buffer` is not a valid buffer mapping resource. + error InvalidHandle; + } + + /// A descriptor of memory-accessible pixel buffer. + /// + /// It is laid out row-major and `base[0]` is the top-left pixel + /// of the mapped image. + struct VideoMemory { + /// Pointer to the first pixel of the first scanline. + /// + /// Each scanline is `.stride` elements separated from + /// each other and contains `width` valid elements. + /// + /// There are `height` total scanlines available. + field base: [*]align(4) Color; + + /// Length of a scanline in elements. + field stride: usize; + + /// Number of valid elements in a scanline + field width: u16; + + /// Number of valid scanlines. + field height: u16; + } + + /// Returns a pointer to linear video memory, row-major. + /// + /// NOTE: The pointer inside `memory` is only valid until the next `Present` operation + /// for any front or back buffer mapping for the associated video output or until + /// the buffer mapping is destroyed. + /// + /// This requires careful management and it is not recommended to share different + /// `BufferMapping` resources with other actors. + syscall get_video_memory { + in buffer: BufferMapping; + + /// The descriptor of the memory mapped video buffer. + out memory: VideoMemory; + + /// `buffer` is not a valid buffer mapping resource. + error InvalidHandle; + } } namespace random { @@ -1107,7 +1368,7 @@ namespace draw { /// Creates a new framebuffer based off a video output. Can be used to output pixels /// to the screen. syscall create_video_framebuffer { - in output: VideoOutput; + in output: video.VideoOutput; out handle: Framebuffer; error InvalidHandle; error SystemResources; @@ -1536,8 +1797,6 @@ resource File { } resource Directory { } -resource VideoOutput { } - resource Font { } /// A framebuffer is something that can be drawn on. @@ -1675,13 +1934,6 @@ struct Await_Options { } } -/// Index of the systems video outputs. -enum VideoOutputID : u8 { - /// The primary video output - item primary = 0; - ... -} - enum FontType : u32 { item bitmap = 0; item vector = 1; diff --git a/src/abi/src/ports/zig.abi.zpatch b/src/abi/src/ports/zig.abi.zpatch index 26e4a5a7..ddf6482a 100644 --- a/src/abi/src/ports/zig.abi.zpatch +++ b/src/abi/src/ports/zig.abi.zpatch @@ -44,7 +44,8 @@ UdpSocket => .udp_socket, File => .file, Directory => .directory, - VideoOutput => .video_output, + video.VideoOutput => .video_video_output, + video.BufferMapping => .video_buffer_mapping, Font => .font, Framebuffer => .framebuffer, Window => .window, @@ -68,7 +69,8 @@ .udp_socket => UdpSocket, .file => File, .directory => Directory, - .video_output => VideoOutput, + .video_video_output => video.VideoOutput, + .video_buffer_mapping => video.BufferMapping, .font => Font, .framebuffer => Framebuffer, .window => Window, diff --git a/src/kernel/components/overlapped.zig b/src/kernel/components/overlapped.zig index 74eb6656..0d35dfa3 100644 --- a/src/kernel/components/overlapped.zig +++ b/src/kernel/components/overlapped.zig @@ -142,6 +142,8 @@ const async_call_handlers = std.EnumArray(ashet.abi.overlapped.ARC.Type, AsyncHa .draw_render = AsyncHandler.wrap(ashet.graphics.render_async), .video_wait_for_v_blank = AsyncHandler.wrap(ashet.video.wait_for_vblank_async), + .video_write_pixels = AsyncHandler.wrap(ashet.video.write_pixels_async), + .video_present = AsyncHandler.wrap(ashet.video.present_async), .io_serial_configure = AsyncHandler.todo("io_serial_configure"), .io_serial_control = AsyncHandler.todo("io_serial_control"), diff --git a/src/kernel/components/resources.zig b/src/kernel/components/resources.zig index 7a7458cd..17ecff96 100644 --- a/src/kernel/components/resources.zig +++ b/src/kernel/components/resources.zig @@ -496,7 +496,8 @@ pub fn InstanceType(comptime type_enum: TypeId) type { .file => ashet.filesystem.File, .directory => ashet.filesystem.Directory, - .video_output => ashet.video.Output, + .video_video_output => ashet.video.Output, + .video_buffer_mapping => ashet.video.BufferMapping, .framebuffer => ashet.graphics.Framebuffer, .font => ashet.graphics.Font, diff --git a/src/kernel/components/syscalls.zig b/src/kernel/components/syscalls.zig index fdf132ca..a7a8b7f3 100644 --- a/src/kernel/components/syscalls.zig +++ b/src/kernel/components/syscalls.zig @@ -373,41 +373,40 @@ pub const syscalls = struct { }; pub const video = struct { - pub fn enumerate(ids: ?[]abi.VideoOutputID) usize { + pub fn enumerate(ids: ?[]abi.video.VideoOutputID) usize { return ashet.video.enumerate(ids); } - pub fn acquire(output: abi.VideoOutputID) error{ SystemResources, NotFound, NotAvailable }!abi.VideoOutput { + pub fn acquire(output_id: abi.video.VideoOutputID) error{ OutputInUse, InvalidId, SystemResources }!abi.video.VideoOutput { const proc = get_current_process(); - const video_output = try ashet.video.acquire_output(output); + const video_output = try ashet.video.acquire_output(output_id); const handle = try ashet.resources.add_to_process(proc, &video_output.system_resource); - return handle.unsafe_cast(.video_output); + return handle.unsafe_cast(.video_video_output); } - pub fn get_resolution(output_handle: abi.VideoOutput) error{InvalidHandle}!abi.Size { + pub fn get_resolution(output_handle: abi.video.VideoOutput) error{InvalidHandle}!abi.Size { _, const output = try resolve_typed_resource(ashet.video.Output, output_handle.as_resource()); return output.get_resolution(); } - pub fn get_video_memory(output_handle: abi.VideoOutput) error{InvalidHandle}!abi.VideoMemory { - _, const output = try resolve_typed_resource(ashet.video.Output, output_handle.as_resource()); - return output.get_video_memory(); - } - - pub fn get_palette(output: abi.VideoOutput, palette: *[abi.palette_size]abi.Color) error{InvalidHandle}!void { + pub fn create_buffer_mapping(output: abi.video.VideoOutput, requested_kind: abi.video.BufferKind) error{ InvalidHandle, Unsupported, AlreadyExists, SystemResources }!abi.video.BufferMapping { _ = output; - _ = palette; + _ = requested_kind; not_implemented_yet(@src()); } - pub fn set_palette(output: abi.VideoOutput, palette: *const [abi.palette_size]abi.Color) error{ InvalidHandle, Unsupported } { - _ = output; - _ = palette; + pub fn get_video_memory(buffer: abi.video.BufferMapping) error{InvalidHandle}!abi.video.VideoMemory { + _ = buffer; not_implemented_yet(@src()); } + + // pub fn get_video_memory(output_handle: abi.VideoOutput) error{InvalidHandle}!abi.VideoMemory { + // _, const output = try resolve_typed_resource(ashet.video.Output, output_handle.as_resource()); + // return output.get_video_memory(); + // } }; pub const overlapped = struct { @@ -493,7 +492,7 @@ pub const syscalls = struct { /// Creates a new framebuffer based off a video output. Can be used to output pixels /// to the screen. - pub fn create_video_framebuffer(video_output: abi.VideoOutput) error{ SystemResources, InvalidHandle }!abi.Framebuffer { + pub fn create_video_framebuffer(video_output: abi.video.VideoOutput) error{ SystemResources, InvalidHandle }!abi.Framebuffer { const proc, const output = try resolve_typed_resource(ashet.video.Output, video_output.as_resource()); const fb = try ashet.graphics.Framebuffer.create_video_output(output); diff --git a/src/kernel/components/video.zig b/src/kernel/components/video.zig index 9badb3ad..88ae9a61 100644 --- a/src/kernel/components/video.zig +++ b/src/kernel/components/video.zig @@ -4,7 +4,7 @@ const ashet = @import("../main.zig"); const logger = std.log.scoped(.video); pub const Color = ashet.abi.Color; -pub const OutputID = ashet.abi.VideoOutputID; +pub const OutputID = ashet.abi.video.VideoOutputID; pub const Resolution = ashet.abi.Size; pub const VideoMemory = ashet.abi.VideoMemory; @@ -50,7 +50,7 @@ pub const VideoDevice = struct { pub const Output = struct { pub const Destructor = ashet.resources.Destructor(@This(), _noop); - system_resource: ashet.resources.SystemResource = .{ .type = .video_output }, + system_resource: ashet.resources.SystemResource = .{ .type = .video_video_output }, /// If true, the kernel will automatically flush the screen in a background process. auto_flush: bool = true, // TODO: Fix this @@ -111,6 +111,14 @@ pub const Output = struct { } }; +pub const BufferMapping = struct { + pub const Destructor = ashet.resources.Destructor(@This(), _noop); + + system_resource: ashet.resources.SystemResource = .{ .type = .video_video_output }, + + fn _noop(_: *BufferMapping) void {} +}; + const frame_rate = 1000 / 30; // 30 Hz var video_outputs: []Output = &.{}; @@ -194,19 +202,17 @@ pub fn enumerate(maybe_ids: ?[]OutputID) usize { for (ids, 0..count) |*id, index| { id.* = @enumFromInt(@as(u8, @intCast(index))); } - return count; - } else { - return video_outputs.len; } + return video_outputs.len; } -pub fn acquire_output(output_id: OutputID) error{ NotFound, NotAvailable }!*Output { +pub fn acquire_output(output_id: OutputID) error{ InvalidId, OutputInUse }!*Output { const index = @intFromEnum(output_id); if (index >= video_outputs.len) - return error.NotFound; + return error.InvalidId; const output = &video_outputs[index]; if (output.system_resource.owners.len > 0) - return error.NotAvailable; + return error.OutputInUse; return output; } @@ -218,6 +224,18 @@ pub fn wait_for_vblank_async(call: *ashet.overlapped.AsyncCall, inputs: ashet.ab output.vsync_awaiters.enqueue(call, null); } +pub fn write_pixels_async(call: *ashet.overlapped.AsyncCall, inputs: ashet.abi.video.WritePixels.Inputs) void { + _ = call; + _ = inputs; + @panic("TODO: write_pixels_async!"); +} + +pub fn present_async(call: *ashet.overlapped.AsyncCall, inputs: ashet.abi.video.Present.Inputs) void { + _ = call; + _ = inputs; + @panic("TODO: present_async!"); +} + pub fn load_splash_screen(vmem: VideoMemory) void { const splash = defaults.splash_screen; const clamp_w = @min(vmem.width, splash.width); From 4501c5b3b8e1403d0a1baf585d260fac2144e9b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Mon, 7 Sep 2026 21:07:47 +0200 Subject: [PATCH 06/17] Starts implementing the video.WritePixels operation by setting up the driver interface and implementing the kernel-internal validation. --- src/abi/db/abi-id-db.json | 4 + src/abi/src/ashet.abi | 59 ++++++++------ src/kernel/components/graphics.zig | 57 ++++++++------ src/kernel/components/syscalls.zig | 15 ++-- src/kernel/components/video.zig | 122 +++++++++++++++++++++++++---- 5 files changed, 182 insertions(+), 75 deletions(-) diff --git a/src/abi/db/abi-id-db.json b/src/abi/db/abi-id-db.json index 63bff4d5..b29c933f 100644 --- a/src/abi/db/abi-id-db.json +++ b/src/abi/db/abi-id-db.json @@ -2527,6 +2527,10 @@ { "fqn": "video.acquire.OutputInUse", "uid": 632 + }, + { + "fqn": "video.WritePixels.InvalidOperation", + "uid": 633 } ] } \ No newline at end of file diff --git a/src/abi/src/ashet.abi b/src/abi/src/ashet.abi index ad98e3ea..db558529 100644 --- a/src/abi/src/ashet.abi +++ b/src/abi/src/ashet.abi @@ -415,6 +415,12 @@ namespace video { out count: usize; } + //? TODO: syscall query_output() -> capabilities, resolution, vid, pid, ... + //? + //? - supports_partial_update: bool + //? - requires_vsync_present: bool, ///< PresentMode.immediate is equivalent to PresentMode.vblank + //? - has_explicit_present: bool, ///< PresentMode.dont_care does not make the data visible + /// The video output resource is an exclusive access token to a /// video output. /// @@ -480,6 +486,11 @@ namespace video { /// NOTE: This mode will immediately upload the pixel data and /// will not await a vertical blanking period. This means the /// upload is likely to create visual glitches or tearing. + /// + /// NOTE: This mode is best-effort, and some video outputs may not support + /// immediate display upload, and require a vertical blanking interval + /// to display the new data. This means the upload operation *may* + /// be equivalent to the @`vblank` mode. item immediate = 0; /// The kernel attempts a tearing free upload of the pixel data. @@ -488,8 +499,28 @@ namespace video { /// vertical blanking period. /// /// NOTE: This mode is best-effort, and does not guarantee the - /// video data is uploaded tearing-free. + /// video data is uploaded tearing-free. This means this operation + /// *may* be equivalent to the @`immediate` mode. item vblank = 1; + + /// The kernel does not attempt to force-display the uploaded video + /// contents. + /// + /// This means that the kernel does not attempt to present the data to + /// the user, it just uploads it to the video output. + /// + /// NOTE: This mode is best-effort and may still be executed equivalently + /// to @`immediate` or @`vblank` if the video output performs automatic + /// self-refreshs. + /// + /// LORE: This is required to enable fast consecutive partial updates of + /// e.g. an AVAPv1 compatible display adapter, as these adapters + /// do a device-internal double-buffering and can only present data + /// with a buffer swap and an implicit vblank synchronization. + /// If we would force the @`WritePixels` operation to always present, + /// partial updates would each take exactly one frame, and it would be + /// impossible to upload more than one partial image per frame. + item dont_care = 2; } /// Uploads pixels to a video output. @@ -546,8 +577,11 @@ namespace video { /// `stride` is less than `destination.width`. error InvalidStride; - /// `destination` is outside the actual video buffer resolution. + /// `destination` is outside the actual video buffer resolution, error InvalidRegion; + + /// `output` does not support partial updates. + error InvalidOperation; } /// A buffer mapping provides a memory-mapped view into a @@ -1408,7 +1442,7 @@ namespace draw { /// Other framebuffer types are not allowed to be passed. syscall get_framebuffer_memory { in fb: Framebuffer; - out memory: VideoMemory; + out memory: video.VideoMemory; error InvalidHandle; error Unsupported; } @@ -3506,25 +3540,6 @@ struct Rectangle { field height: u16; } -struct VideoMemory { - /// Pointer to the first pixel of the first scanline. - /// - /// Each scanline is @`stride` elements separated from - /// each other and contains @`width` valid elements. - /// - /// There are @`height` total scanlines available. - field base: [*]align(4) Color; - - /// Length of a scanline. - field stride: usize; - - /// Number of valid elements in a scanline - field width: u16; - - /// Number of valid scanlines. - field height: u16; -} - struct FileSystemInfo { /// system-unique id of this file system field id: FileSystemId; diff --git a/src/kernel/components/graphics.zig b/src/kernel/components/graphics.zig index f4d21d82..f1e07886 100644 --- a/src/kernel/components/graphics.zig +++ b/src/kernel/components/graphics.zig @@ -195,7 +195,7 @@ pub const Framebuffer = struct { pub const Type = union(ashet.abi.FramebufferType) { memory: Bitmap, - video: VideoOut, + video: noreturn, // TODO(gpu_support): video: VideoOut, window: *ashet.gui.Window, widget: *ashet.gui.Widget, }; @@ -227,19 +227,23 @@ pub const Framebuffer = struct { } pub fn create_video_output(output: *ashet.video.Output) error{SystemResources}!*Framebuffer { - const fb = ashet.memory.type_pool(Framebuffer).alloc() catch return error.SystemResources; - errdefer ashet.memory.type_pool(Framebuffer).free(fb); - - fb.* = .{ - .type = .{ - .video = .{ - .output = output, - .memory = output.get_video_memory(), - }, - }, - }; - - return fb; + _ = output; + // TODO(gpu_support): + @panic("TODO: graphics.create_video_output!"); + + // const fb = ashet.memory.type_pool(Framebuffer).alloc() catch return error.SystemResources; + // errdefer ashet.memory.type_pool(Framebuffer).free(fb); + + // fb.* = .{ + // .type = .{ + // .video = .{ + // .output = output, + // .memory = output.get_video_memory(), + // }, + // }, + // }; + + // return fb; } pub fn create_window(window: *ashet.gui.Window) error{SystemResources}!*Framebuffer { @@ -272,7 +276,7 @@ pub const Framebuffer = struct { const back_buffer = bmp.pixels[0 .. @as(usize, bmp.width) * bmp.stride]; ashet.memory.allocator.free(back_buffer); }, - .video => {}, + .video => unreachable, // TODO(gpu_support) .window => {}, .widget => {}, } @@ -282,7 +286,7 @@ pub const Framebuffer = struct { fn invalidate(fb: *Framebuffer) void { switch (fb.type) { .memory => {}, // no-op, nothing to invalidate - .video => |video| video.output.flush(), + .video => unreachable, // TODO(gpu_support) .window => |win| win.invalidate_full(), .widget => |widget| widget.window.invalidate_region(widget.bounds), } @@ -291,7 +295,7 @@ pub const Framebuffer = struct { pub fn get_size(fb: Framebuffer) Size { return switch (fb.type) { .memory => |mem| .new(mem.width, mem.height), - .video => |video| video.output.get_resolution(), + .video => unreachable, // TODO(gpu_support) .window => |win| win.size, .widget => |widget| widget.bounds.size(), }; @@ -313,15 +317,16 @@ pub const Framebuffer = struct { .height = mem.height, .stride = mem.stride, }, - .video => |video| blk: { - const mem = video.output.get_video_memory(); - break :blk .{ - .pixels = mem.base, - .height = mem.height, - .width = mem.width, - .stride = mem.stride, - }; - }, + .video => unreachable, // TODO(gpu_support) + // TODO(gpu_support): .video => |video| blk: { + // const mem = video.output.get_video_memory(); + // break :blk .{ + // .pixels = mem.base, + // .height = mem.height, + // .width = mem.width, + // .stride = mem.stride, + // }; + // }, .window => |win| .{ .pixels = win.pixels.ptr, .width = win.size.width, diff --git a/src/kernel/components/syscalls.zig b/src/kernel/components/syscalls.zig index a7a8b7f3..20698722 100644 --- a/src/kernel/components/syscalls.zig +++ b/src/kernel/components/syscalls.zig @@ -395,18 +395,13 @@ pub const syscalls = struct { pub fn create_buffer_mapping(output: abi.video.VideoOutput, requested_kind: abi.video.BufferKind) error{ InvalidHandle, Unsupported, AlreadyExists, SystemResources }!abi.video.BufferMapping { _ = output; _ = requested_kind; - not_implemented_yet(@src()); + not_implemented_yet(@src()); // TODO(gpu_support) } - pub fn get_video_memory(buffer: abi.video.BufferMapping) error{InvalidHandle}!abi.video.VideoMemory { - _ = buffer; - not_implemented_yet(@src()); + pub fn get_video_memory(buffer_handle: abi.video.BufferMapping) error{InvalidHandle}!abi.video.VideoMemory { + _, const mapping = try resolve_typed_resource(ashet.video.BufferMapping, buffer_handle.as_resource()); + return mapping.get_video_memory(); } - - // pub fn get_video_memory(output_handle: abi.VideoOutput) error{InvalidHandle}!abi.VideoMemory { - // _, const output = try resolve_typed_resource(ashet.video.Output, output_handle.as_resource()); - // return output.get_video_memory(); - // } }; pub const overlapped = struct { @@ -541,7 +536,7 @@ pub const syscalls = struct { return fb.get_size(); } - pub fn get_framebuffer_memory(framebuffer: abi.Framebuffer) error{ InvalidHandle, Unsupported }!abi.VideoMemory { + pub fn get_framebuffer_memory(framebuffer: abi.Framebuffer) error{ InvalidHandle, Unsupported }!abi.video.VideoMemory { _, const fb = try resolve_typed_resource(ashet.graphics.Framebuffer, framebuffer.as_resource()); return switch (fb.type) { .memory => |mem| .{ diff --git a/src/kernel/components/video.zig b/src/kernel/components/video.zig index 88ae9a61..81f971f0 100644 --- a/src/kernel/components/video.zig +++ b/src/kernel/components/video.zig @@ -6,7 +6,10 @@ const logger = std.log.scoped(.video); pub const Color = ashet.abi.Color; pub const OutputID = ashet.abi.video.VideoOutputID; pub const Resolution = ashet.abi.Size; -pub const VideoMemory = ashet.abi.VideoMemory; +pub const VideoMemory = ashet.abi.video.VideoMemory; + +const Rectangle = ashet.abi.Rectangle; +const PresentMode = ashet.abi.video.PresentMode; pub const Buffering = enum { buffered, @@ -26,6 +29,14 @@ pub const VideoDevice = struct { get_properties_fn: *const fn (*ashet.drivers.Driver) DeviceProperties, get_one_vblank_event_fn: ?*const fn (*ashet.drivers.Driver) bool = null, // TODO: Go through all drivers and see which actually support this + begin_write_pixels_fn: ?*const fn ( + driver: *ashet.drivers.Driver, + call: *ashet.overlapped.AsyncCall, + rectangle: Rectangle, + pixels: []const Color, + mode: PresentMode, + ) void, + pub fn flush(vd: *VideoDevice) void { vd.flush_fn(ashet.drivers.resolveDriver(.video, vd)); } @@ -45,6 +56,22 @@ pub const VideoDevice = struct { @panic("invalid API use"); } } + + pub fn begin_write_pixels( + vd: *VideoDevice, + call: *ashet.overlapped.AsyncCall, + rectangle: Rectangle, + pixels: []const Color, + mode: PresentMode, + ) void { + vd.begin_write_pixels_fn( + ashet.drivers.resolveDriver(.video, vd), + call, + rectangle, + pixels, + mode, + ); + } }; pub const Output = struct { @@ -52,6 +79,10 @@ pub const Output = struct { system_resource: ashet.resources.SystemResource = .{ .type = .video_video_output }, + buffer_mappings: std.EnumArray(ashet.abi.video.BufferKind, ?*BufferMapping) = .initFill(null), + + supports_partial_update: bool = true, // TODO(gpu_support): Query this from the driver + /// If true, the kernel will automatically flush the screen in a background process. auto_flush: bool = true, // TODO: Fix this flush_required: bool = false, @@ -63,24 +94,53 @@ pub const Output = struct { fn _noop(_: *Output) void {} - pub fn get_resolution(output: Output) Resolution { + pub fn get_resolution(output: *const Output) Resolution { return output.video_driver.get_properties().resolution; } - /// The raw exposed video memory. Writing to this will change the content - /// on the screen. - /// Memory is interpreted with the current video mode to produce an image. - pub fn get_video_memory(output: Output) VideoMemory { - const props = output.video_driver.get_properties(); + pub fn begin_write_pixels(output: *const Output, call: *ashet.overlapped.AsyncCall, destination: Rectangle, pixels: []const Color, stride: usize, mode: PresentMode) error{ + BufferSize, + InvalidStride, + InvalidRegion, + InvalidOperation, + }!void { + const resolution = output.get_resolution(); - std.debug.assert(props.video_memory.len >= (props.stride * @as(usize, props.resolution.height))); + const screen_rect: Rectangle = .new(.zero, resolution); - return .{ - .base = props.video_memory.ptr, - .stride = props.stride, - .width = props.resolution.width, - .height = props.resolution.height, - }; + if (!screen_rect.containsRectangle(destination)) { + // No updates allowed outside the screen boundaries + return error.InvalidRegion; + } + + if (!output.supports_partial_update and !destination.eql(screen_rect)) { + // No partial updates allowed + return error.InvalidOperation; + } + + if (stride < destination.width) { + // Check if each row contains at least the actual row length of pixels. + return error.InvalidStride; + } + + const expected_pixel_count = stride * @max(0, destination.height -| 1) + destination.width; + if (pixels.len < expected_pixel_count) { + // Check if the buffer is big enough to be written + return error.BufferSize; + } + + if (destination.width == 0 or destination.height == 0) { + // Trivial case: Immediate completion when empty target. + return call.finalize(ashet.abi.video.WritePixels, .{}); + } + + return output.video_driver.begin_write_pixels( + call, + destination, + pixels, + stride, + mode, + ); } /// Requests that the driver shall flip front- and back buffers in the next @@ -116,7 +176,25 @@ pub const BufferMapping = struct { system_resource: ashet.resources.SystemResource = .{ .type = .video_video_output }, + output: *Output, + fn _noop(_: *BufferMapping) void {} + + /// The raw exposed video memory. Writing to this will change the content + /// on the screen. + /// Memory is interpreted with the current video mode to produce an image. + pub fn get_video_memory(mapping: *const BufferMapping) VideoMemory { + const props = mapping.output.video_driver.get_properties(); + + std.debug.assert(props.video_memory.len >= (props.stride * @as(usize, props.resolution.height))); + + return .{ + .base = props.video_memory.ptr, + .stride = props.stride, + .width = props.resolution.width, + .height = props.resolution.height, + }; + } }; const frame_rate = 1000 / 30; // 30 Hz @@ -225,9 +303,19 @@ pub fn wait_for_vblank_async(call: *ashet.overlapped.AsyncCall, inputs: ashet.ab } pub fn write_pixels_async(call: *ashet.overlapped.AsyncCall, inputs: ashet.abi.video.WritePixels.Inputs) void { - _ = call; - _ = inputs; - @panic("TODO: write_pixels_async!"); + const output: *Output = ashet.resources.resolve(Output, call.resource_owner, inputs.output.as_resource()) catch { + return call.finalize(ashet.abi.video.WritePixels, error.InvalidHandle); + }; + + output.begin_write_pixels( + call, + inputs.destination, + inputs.pixels_ptr[0..inputs.pixels_len], + inputs.stride, + inputs.mode, + ) catch |err| { + return call.finalize(ashet.abi.video.WritePixels, err); + }; } pub fn present_async(call: *ashet.overlapped.AsyncCall, inputs: ashet.abi.video.Present.Inputs) void { From 0d6b6b51a758ccbdb3b958e0ae100a44f1435cff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Tue, 8 Sep 2026 23:32:32 +0200 Subject: [PATCH 07/17] Refactors a lot of the video drivers to the new model. --- justfile | 2 +- src/abi/db/abi-id-db.json | 4 + src/abi/src/ashet.abi | 3 + src/kernel/components/video.zig | 150 +++++++------- .../drivers/video/AVAPv1_Framebuffer.zig | 184 ++++++++++++------ src/kernel/drivers/video/Host_VNC_Output.zig | 66 ++++--- .../drivers/video/Virtual_Video_Output.zig | 39 ++-- src/kernel/port/hosted/initialize.zig | 5 +- .../x86/hosted-linux/Wayland_Display.zig | 1 - .../machine/x86/hosted-linux/X11_Display.zig | 1 - 10 files changed, 279 insertions(+), 176 deletions(-) diff --git a/justfile b/justfile index 21a0d920..de428513 100644 --- a/justfile +++ b/justfile @@ -22,7 +22,7 @@ run-avap-simulation: [working-directory: 'src/kernel'] build-kernel: - {{zig}} build {{default_params}} -Dmachine=arm-ashet-hc -Dno-emit-bin + {{zig}} build {{default_params}} -Dmachine=x86-hosted-linux -Dno-emit-bin {{zig}} build {{default_params}} -Dmachine=arm-ashet-hc {{zig}} build {{default_params}} -Dmachine=arm-ashet-vhc diff --git a/src/abi/db/abi-id-db.json b/src/abi/db/abi-id-db.json index b29c933f..644601a9 100644 --- a/src/abi/db/abi-id-db.json +++ b/src/abi/db/abi-id-db.json @@ -2531,6 +2531,10 @@ { "fqn": "video.WritePixels.InvalidOperation", "uid": 633 + }, + { + "fqn": "video.WritePixels.IoError", + "uid": 634 } ] } \ No newline at end of file diff --git a/src/abi/src/ashet.abi b/src/abi/src/ashet.abi index db558529..617c4a50 100644 --- a/src/abi/src/ashet.abi +++ b/src/abi/src/ashet.abi @@ -582,6 +582,9 @@ namespace video { /// `output` does not support partial updates. error InvalidOperation; + + /// The underlying video adapter had an I/O failure. + error IoError; } /// A buffer mapping provides a memory-mapped view into a diff --git a/src/kernel/components/video.zig b/src/kernel/components/video.zig index 81f971f0..d18525a9 100644 --- a/src/kernel/components/video.zig +++ b/src/kernel/components/video.zig @@ -18,29 +18,25 @@ pub const Buffering = enum { pub const DeviceProperties = struct { resolution: Resolution, - stride: usize, + // stride: usize, - video_memory_mapping: Buffering, - video_memory: []align(ashet.memory.page_size) Color, + // video_memory_mapping: Buffering, + // video_memory: []align(ashet.memory.page_size) Color, }; pub const VideoDevice = struct { - flush_fn: *const fn (*ashet.drivers.Driver) void, get_properties_fn: *const fn (*ashet.drivers.Driver) DeviceProperties, get_one_vblank_event_fn: ?*const fn (*ashet.drivers.Driver) bool = null, // TODO: Go through all drivers and see which actually support this - begin_write_pixels_fn: ?*const fn ( + begin_write_pixels_fn: *const fn ( driver: *ashet.drivers.Driver, call: *ashet.overlapped.AsyncCall, rectangle: Rectangle, pixels: []const Color, + stride: usize, mode: PresentMode, ) void, - pub fn flush(vd: *VideoDevice) void { - vd.flush_fn(ashet.drivers.resolveDriver(.video, vd)); - } - pub fn get_properties(vd: *VideoDevice) DeviceProperties { return vd.get_properties_fn(ashet.drivers.resolveDriver(.video, vd)); } @@ -62,6 +58,7 @@ pub const VideoDevice = struct { call: *ashet.overlapped.AsyncCall, rectangle: Rectangle, pixels: []const Color, + stride: usize, mode: PresentMode, ) void { vd.begin_write_pixels_fn( @@ -69,6 +66,7 @@ pub const VideoDevice = struct { call, rectangle, pixels, + stride, mode, ); } @@ -143,25 +141,6 @@ pub const Output = struct { ); } - /// Requests that the driver shall flip front- and back buffers in the next - /// frame. - /// - /// NOTE: This is only a request, and might be called more often than necessary, - /// without impacting performance. - pub fn flush(output: *Output) void { - output.flush_required = true; - } - - /// Potentially synchronizes the video storage with the screen. - /// Without calling this, the screen might not be refreshed at all. - /// - /// NOTE: This function will forcefully flip the buffers and might cost - /// a good amount of time. - pub fn force_flush(output: *Output) void { - output.video_driver.flush(); - output.flush_required = false; - } - /// Notifies all overlapped events that wait for V-Blank on this output. pub fn notify_vblank_awaiters(output: *Output) void { while (output.vsync_awaiters.dequeue()) |tup| { @@ -184,23 +163,22 @@ pub const BufferMapping = struct { /// on the screen. /// Memory is interpreted with the current video mode to produce an image. pub fn get_video_memory(mapping: *const BufferMapping) VideoMemory { - const props = mapping.output.video_driver.get_properties(); - - std.debug.assert(props.video_memory.len >= (props.stride * @as(usize, props.resolution.height))); - - return .{ - .base = props.video_memory.ptr, - .stride = props.stride, - .width = props.resolution.width, - .height = props.resolution.height, - }; + _ = mapping; + @panic("TODO: Implement get_video_memory"); // TODO(gpu_support): Implement get_video_memory + // const props = mapping.output.video_driver.get_properties(); + + // std.debug.assert(props.video_memory.len >= (props.stride * @as(usize, props.resolution.height))); + + // return .{ + // .base = props.video_memory.ptr, + // .stride = props.stride, + // .width = props.resolution.width, + // .height = props.resolution.height, + // }; } }; -const frame_rate = 1000 / 30; // 30 Hz - var video_outputs: []Output = &.{}; -var video_flush_deadline = ashet.time.Deadline.init_abs(.system_start); pub fn initialize() !void { const count: usize = blk: { @@ -229,20 +207,6 @@ pub fn initialize() !void { }); } } - video_flush_deadline = ashet.time.Deadline.init_rel(frame_rate); -} - -fn flush_all() void { - // Go through all video outputs that no support vertical blanking - // notifications with a periodic interval and manually flush them: - for (video_outputs) |*video_output| { - if (video_output.video_driver.supports_vblank_event()) - continue; - if (video_output.auto_flush or video_output.flush_required) { - video_output.force_flush(); - } - video_output.notify_vblank_awaiters(); - } } ///Ticks the video subsystem @@ -254,24 +218,12 @@ pub fn tick() void { continue; if (video_output.video_driver.get_one_vblank_event()) { - video_output.force_flush(); + // video_output.force_flush(); video_output.notify_vblank_awaiters(); } } - if (video_flush_deadline.is_reached()) { - video_flush_deadline.move_forward(frame_rate); - flush_all(); - - var drop_count: usize = 0; - while (video_flush_deadline.is_reached()) { - drop_count += 1; - video_flush_deadline.move_forward(frame_rate); - } - if (drop_count > 0) { - logger.warn("dropping {} video frames!", .{drop_count}); - } - } + // TODO(gpu_support): How to implement non-vblanking video outputs with WaitForVSync? } pub fn enumerate(maybe_ids: ?[]OutputID) usize { @@ -356,3 +308,63 @@ pub const defaults = struct { /// The default border color if the screen is downscaled pub const border_color = splash_screen.base[0]; // just use the top-left pixel of the splash screen. }; + +pub const utils = struct { + pub fn PixelBuffer(comptime Pixel: type, mutability: enum { @"const", mut }) type { + return struct { + data: switch (mutability) { + .@"const" => [*]const Pixel, + .mut => [*]Pixel, + }, + width: usize, + height: usize, + stride: usize, + }; + } + + pub fn CopyPixelOptions(comptime DstPixel: type) type { + return struct { + dst_buffer: PixelBuffer(DstPixel, .mut), + dst_pos: struct { x: usize, y: usize }, + + src_buffer: PixelBuffer(Color, .@"const"), + + convert_ctx: ?*anyopaque = null, + }; + } + + /// Copies a rectangular portion from src_buffer to dst_buffer, + /// potentially converting the color data. + pub fn copy_pixels( + comptime DstPixel: type, + options: CopyPixelOptions(DstPixel), + comptime convert_fn: ?fn (?*anyopaque, Color) DstPixel, + ) void { + if (DstPixel != Color and convert_fn == null) + @compileError("If copying to a non-native target, you have to provide a convert function"); + + // Assert that we fit: + std.debug.assert(options.dst_pos.x +| options.src_buffer.width <= options.dst_buffer.width); + std.debug.assert(options.dst_pos.y +| options.src_buffer.height <= options.dst_buffer.height); + + var dst_iter = options.dst_buffer.data + options.dst_pos.y * options.dst_buffer.stride + options.dst_pos.x; + var src_iter = options.src_buffer.data; + + for (0..options.src_buffer.height) |_| { + const dst_row = dst_iter; + const src_row = src_iter; + + for (0..options.src_buffer.width) |x| { + const src = src_row[x]; + const dst = if (convert_fn) |convert| + convert(options.convert_ctx, src) + else + src; + dst_row[x] = dst; + } + + dst_iter += options.dst_buffer.stride; + src_iter += options.src_buffer.stride; + } + } +}; diff --git a/src/kernel/drivers/video/AVAPv1_Framebuffer.zig b/src/kernel/drivers/video/AVAPv1_Framebuffer.zig index badef1ea..4e1e58fa 100644 --- a/src/kernel/drivers/video/AVAPv1_Framebuffer.zig +++ b/src/kernel/drivers/video/AVAPv1_Framebuffer.zig @@ -16,19 +16,17 @@ driver: Driver = .{ .class = .{ .video = .{ .get_properties_fn = get_properties, - .flush_fn = flush, + .begin_write_pixels_fn = driver_begin_write_pixels, }, }, }, -framebuffer: [256_000]Color align(ashet.memory.page_size), device: std.fs.File, pub fn init( file_name: []const u8, ) error{ FileNotFound, BadFile, DeviceUnresponsive, IoError }!AVAPv1_Framebuffer { var fb: AVAPv1_Framebuffer = .{ - .framebuffer = @splat(.black), .device = undefined, }; @@ -104,64 +102,21 @@ pub fn init( return error.DeviceUnresponsive; } - ashet.video.load_splash_screen(.{ - .base = &fb.framebuffer, - .width = 640, - .height = 400, - .stride = 640, - }); - - fb.flush_with_error() catch |err| switch (err) { - error.InputOutput, - error.SystemResources, - error.IsDir, - error.OperationAborted, - error.BrokenPipe, - error.ConnectionResetByPeer, - error.ConnectionTimedOut, - error.NotOpenForReading, - error.SocketNotConnected, - error.WouldBlock, - error.Canceled, - error.AccessDenied, - error.ProcessNotFound, - error.LockViolation, - error.Unexpected, - error.PermissionDenied, - error.Overflow, - error.NoDevice, - error.FileTooBig, - error.NoSpaceLeft, - error.DeviceBusy, - error.DiskQuota, - error.InvalidArgument, - error.NotOpenForWriting, - error.MessageTooBig, - => return error.IoError, - - error.Timeout, - => return error.DeviceUnresponsive, - - error.WriteBufferFailed => { - logger.err("write buffers failed", .{}); - return error.DeviceUnresponsive; - }, - - error.SwapBuffersFailed => { - logger.err("swap buffers failed", .{}); - return error.DeviceUnresponsive; - }, - }; + // TODO(gpu_support): This needs to wander into the kernel: + // ashet.video.load_splash_screen(.{ + // .base = &fb.framebuffer, + // .width = 640, + // .height = 400, + // .stride = 640, + // }); return fb; } fn get_properties(driver: *Driver) ashet.video.DeviceProperties { - const vd = driver.resolve(AVAPv1_Framebuffer, "driver"); + _ = driver; + // const vd = driver.resolve(AVAPv1_Framebuffer, "driver"); return .{ - .video_memory = &vd.framebuffer, - .video_memory_mapping = .buffered, - .stride = width, .resolution = .{ .width = width, .height = height, @@ -169,20 +124,73 @@ fn get_properties(driver: *Driver) ashet.video.DeviceProperties { }; } -fn flush(driver: *Driver) void { +fn driver_begin_write_pixels( + driver: *Driver, + call: *ashet.overlapped.AsyncCall, + rectangle: ashet.abi.Rectangle, + pixels: []const Color, + stride: usize, + mode: ashet.abi.video.PresentMode, +) void { const vd = driver.resolve(AVAPv1_Framebuffer, "driver"); - vd.flush_with_error() catch |err| { + vd.begin_write_pixels(rectangle, pixels, stride, mode) catch |err| { logger.err("video driver failure: {t}", .{err}); + const call_err: ashet.abi.video.WritePixels.Error = switch (err) { + // OS I/O layer: + error.InputOutput, + error.SystemResources, + error.IsDir, + error.OperationAborted, + error.BrokenPipe, + error.ConnectionResetByPeer, + error.ConnectionTimedOut, + error.NotOpenForReading, + error.SocketNotConnected, + error.WouldBlock, + error.Canceled, + error.AccessDenied, + error.ProcessNotFound, + error.LockViolation, + error.Unexpected, + error.PermissionDenied, + error.NoDevice, + error.FileTooBig, + error.NoSpaceLeft, + error.DeviceBusy, + error.DiskQuota, + error.InvalidArgument, + error.NotOpenForWriting, + error.MessageTooBig, + + // our I/O layer + error.Timeout, + error.MissingAcknowledge, + => error.IoError, + + error.Overflow, + => @panic("Kernel did not sanitize inputs properly"), + }; + return call.finalize(ashet.abi.video.WritePixels, call_err); }; + return call.finalize(ashet.abi.video.WritePixels, .{}); } -fn flush_with_error(vd: *AVAPv1_Framebuffer) !void { +fn begin_write_pixels( + vd: *AVAPv1_Framebuffer, + rectangle: ashet.abi.Rectangle, + pixels: []const Color, + stride: usize, + mode: ashet.abi.video.PresentMode, +) !void { // logger.debug("write buffer", .{}); - try write_buffer(vd.device, 0, @ptrCast(&vd.framebuffer)); + try write_rectangle(vd.device, rectangle, pixels, stride); // logger.debug("swap buffers", .{}); - try swap_buffers(vd.device); + switch (mode) { + .dont_care => {}, + .immediate, .vblank => try swap_buffers(vd.device), + } } fn ping(port: std.fs.File) !bool { @@ -213,8 +221,54 @@ pub fn write_buffer(port: std.fs.File, offset: u32, buffer: []const u8) !void { try read_discarding(port, header.length, deadline, .log); try read_footer(port, header, deadline); - if (header.ack == false) - return error.WriteBufferFailed; + if (header.ack == false) { + logger.err("write_buffer did not ACK!", .{}); + return error.MissingAcknowledge; + } +} + +pub fn write_rectangle(port: std.fs.File, rectangle: ashet.abi.Rectangle, pixels: []const Color, stride: usize) !void { + const pixel_count = @as(u32, rectangle.width) * rectangle.height; + + // [ x: u16, y: u16, width: u32, pixel: u8, … ] + + const length: u32 = std.math.cast(u32, pixel_count +| 8) orelse return error.Overflow; + const x = std.math.cast(u16, rectangle.x).?; + const y = std.math.cast(u16, rectangle.y).?; + + try write_header(port, length, .write_buffer); + + try write_all(port, &int_slice(u16, x)); + try write_all(port, &int_slice(u16, y)); + try write_all(port, &int_slice(u32, rectangle.width)); + + { + comptime std.debug.assert(@sizeOf(Color) == @sizeOf(u8)); + var row: [*]const u8 = @ptrCast(pixels.ptr); + for (0..rectangle.height) |_| { + try write_all(port, row[0..rectangle.width]); + row += stride; + } + } + + try write_footer(port, length); + + const deadline: Deadline = .from_ms(100); + + const header = try read_header(port, deadline); + try read_discarding(port, header.length, deadline, .log); + try read_footer(port, header, deadline); + + if (header.ack == false) { + logger.err("write_rectangle did not ACK!", .{}); + return error.MissingAcknowledge; + } +} + +fn int_slice(comptime T: type, value: T) [@sizeOf(T)]u8 { + var buf: [@sizeOf(T)]u8 = undefined; + std.mem.writeInt(T, &buf, value, .little); + return buf; } pub fn swap_buffers(port: std.fs.File) !void { @@ -226,8 +280,10 @@ pub fn swap_buffers(port: std.fs.File) !void { try read_discarding(port, header.length, deadline, .log); try read_footer(port, header, deadline); - if (header.ack == false) - return error.SwapBuffersFailed; + if (header.ack == false) { + logger.err("swap_buffers did not ACK!", .{}); + return error.MissingAcknowledge; + } } fn write_command(port: std.fs.File, cmd: Command, buffer: []const u8) !void { diff --git a/src/kernel/drivers/video/Host_VNC_Output.zig b/src/kernel/drivers/video/Host_VNC_Output.zig index 1be5cce3..9d1decb8 100644 --- a/src/kernel/drivers/video/Host_VNC_Output.zig +++ b/src/kernel/drivers/video/Host_VNC_Output.zig @@ -12,17 +12,15 @@ const VNC_Server = @import("../../port/hosted/VNC_Server.zig"); backbuffer_lock: std.Thread.Mutex = .{}, backbuffer: []Color, -frontbuffer: []align(ashet.memory.page_size) Color, width: u16, height: u16, -backbuffer_dirty: bool, driver: Driver = .{ .name = "Host VNC Screen", .class = .{ .video = .{ .get_properties_fn = get_properties, - .flush_fn = flush, + .begin_write_pixels_fn = begin_write_pixels, }, }, }, @@ -31,28 +29,19 @@ pub fn init( width: u16, height: u16, ) !Host_VNC_Output { - const fb = try std.heap.page_allocator.alignedAlloc( - Color, - .fromByteUnits(ashet.memory.page_size), - 2 * @as(u32, width) * @as(u32, height), - ); + const fb = try std.heap.page_allocator.alloc(Color, @as(u32, width) * @as(u32, height)); errdefer std.heap.page_allocator.free(fb); return .{ .width = width, .height = height, - .frontbuffer = fb[0 .. fb.len / 2], - .backbuffer = fb[fb.len / 2 .. fb.len], - .backbuffer_dirty = false, + .backbuffer = fb, }; } fn get_properties(driver: *Driver) ashet.video.DeviceProperties { const vd: *Host_VNC_Output = @fieldParentPtr("driver", driver); return .{ - .stride = vd.width, - .video_memory = vd.frontbuffer, - .video_memory_mapping = .buffered, .resolution = .{ .width = vd.width, .height = vd.height, @@ -60,18 +49,47 @@ fn get_properties(driver: *Driver) ashet.video.DeviceProperties { }; } -fn flush(driver: *Driver) void { - const vd: *Host_VNC_Output = @fieldParentPtr("driver", driver); +fn vnc_server(output: *Host_VNC_Output) *VNC_Server { + return @fieldParentPtr("screen", output); +} - // vd.backbuffer_lock.lock(); - // defer vd.backbuffer_lock.unlock(); +fn begin_write_pixels( + driver: *Driver, + call: *ashet.overlapped.AsyncCall, + rectangle: ashet.abi.Rectangle, + pixels: []const Color, + stride: usize, + mode: ashet.abi.video.PresentMode, +) void { + const vd: *Host_VNC_Output = @fieldParentPtr("driver", driver); - @memcpy(vd.backbuffer, vd.frontbuffer); - vd.backbuffer_dirty = true; + ashet.video.utils.copy_pixels( + Color, + .{ + .dst_buffer = .{ + .data = vd.backbuffer.ptr, + .width = vd.width, + .height = vd.height, + .stride = vd.width, + }, + .dst_pos = .{ + .x = @intCast(rectangle.x), + .y = @intCast(rectangle.y), + }, + .src_buffer = .{ + .data = pixels.ptr, + .width = rectangle.width, + .height = rectangle.height, + .stride = stride, + }, + }, + null, + ); - vd.vnc_server().notify_flush(); -} + switch (mode) { + .dont_care => {}, + .immediate, .vblank => vd.vnc_server().notify_flush(), + } -fn vnc_server(output: *Host_VNC_Output) *VNC_Server { - return @fieldParentPtr("screen", output); + return call.finalize(ashet.abi.video.WritePixels, .{}); } diff --git a/src/kernel/drivers/video/Virtual_Video_Output.zig b/src/kernel/drivers/video/Virtual_Video_Output.zig index 51716636..50c5b9d1 100644 --- a/src/kernel/drivers/video/Virtual_Video_Output.zig +++ b/src/kernel/drivers/video/Virtual_Video_Output.zig @@ -7,38 +7,49 @@ const Driver = ashet.drivers.Driver; const Color = ashet.abi.Color; const Resolution = ashet.abi.Size; -pub const width = 320; -pub const height = 240; - -backbuffer: [width * height]Color align(ashet.memory.page_size) = undefined, +pub const width = 640; +pub const height = 400; driver: Driver = .{ .name = "Virtual Screen", .class = .{ .video = .{ .get_properties_fn = get_properties, - .flush_fn = flush, + .begin_write_pixels_fn = driver_begin_write_pixels, }, }, }, +resolution: Resolution, -pub fn init() Virtual_Video_Output { - return .{}; +pub fn init(resolution: Resolution) Virtual_Video_Output { + std.debug.assert(resolution.width > 0 and resolution.height > 0); + return .{ + .resolution = resolution, + }; } fn get_properties(driver: *Driver) ashet.video.DeviceProperties { - const vd = driver.resolve(Virtual_Video_Output, "driver"); + // const vd = driver.resolve(Virtual_Video_Output, "driver"); + _ = driver; return .{ - .video_memory = &vd.backbuffer, - .video_memory_mapping = .unbuffered, - .stride = width, .resolution = .{ .width = width, .height = height, }, }; } -fn flush(driver: *Driver) void { - const vd = driver.resolve(Virtual_Video_Output, "driver"); - _ = vd; +fn driver_begin_write_pixels( + driver: *Driver, + call: *ashet.overlapped.AsyncCall, + rectangle: ashet.abi.Rectangle, + pixels: []const Color, + stride: usize, + mode: ashet.abi.video.PresentMode, +) void { + _ = driver; + _ = rectangle; + _ = pixels; + _ = stride; + _ = mode; + return call.finalize(ashet.abi.video.WritePixels, .{}); } diff --git a/src/kernel/port/hosted/initialize.zig b/src/kernel/port/hosted/initialize.zig index 8b20b83e..849910a4 100644 --- a/src/kernel/port/hosted/initialize.zig +++ b/src/kernel/port/hosted/initialize.zig @@ -155,9 +155,10 @@ pub fn initialize(comptime video_drivers: std.StaticStringMap(VideoDriverCtor)) badKernelOption("sdl", "sdl video output disabled!", .{}); } } else if (std.mem.eql(u8, device_type, "dummy")) { - if (res_x != 320 or res_y != 240) badKernelOption("video", "resolution must be 320x240!", .{}); const driver = try global_memory.create(ashet.drivers.video.Virtual_Video_Output); - driver.* = ashet.drivers.video.Virtual_Video_Output.init(); + driver.* = ashet.drivers.video.Virtual_Video_Output.init( + .new(res_x, res_y), + ); ashet.drivers.install(&driver.driver); } else if (std.mem.eql(u8, device_type, "avap-v1")) { if (res_x != 640 or res_y != 400) badKernelOption("video", "AVAPv1 resolution must be 640x400!", .{}); diff --git a/src/kernel/port/machine/x86/hosted-linux/Wayland_Display.zig b/src/kernel/port/machine/x86/hosted-linux/Wayland_Display.zig index 12d37ff9..c7c99547 100644 --- a/src/kernel/port/machine/x86/hosted-linux/Wayland_Display.zig +++ b/src/kernel/port/machine/x86/hosted-linux/Wayland_Display.zig @@ -85,7 +85,6 @@ pub fn init( .swap_chain = undefined, }; - @memset(server.screen.frontbuffer, ashet.abi.Color.blue); @memset(server.screen.backbuffer, ashet.abi.Color.red); server.connection = shimizu.posix.Connection.open(allocator, .{}) catch |err| switch (err) { diff --git a/src/kernel/port/machine/x86/hosted-linux/X11_Display.zig b/src/kernel/port/machine/x86/hosted-linux/X11_Display.zig index 66754aa5..493b2fc9 100644 --- a/src/kernel/port/machine/x86/hosted-linux/X11_Display.zig +++ b/src/kernel/port/machine/x86/hosted-linux/X11_Display.zig @@ -123,7 +123,6 @@ pub fn init( server.source = x11.Source.initAfterSetup(server.socket_reader.interface()); server.sink = .{ .writer = &server.socket_writer.interface }; - @memset(server.screen.frontbuffer, ashet.abi.Color.blue); @memset(server.screen.backbuffer, ashet.abi.Color.red); const base_resource = server.setup.resource_id_base; From 1dc0442d912c84b16cb60732dccfc2b3ecfcb3c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Tue, 8 Sep 2026 23:43:11 +0200 Subject: [PATCH 08/17] Starts to rework Host_VNC_Output into Externally_Managed_Output which is a generalization of the concept. --- src/kernel/drivers/drivers.zig | 2 +- .../video/Externally_Managed_Output.zig | 131 ++++++++++++++++++ src/kernel/drivers/video/Host_VNC_Output.zig | 95 ------------- src/kernel/port/hosted/VNC_Server.zig | 4 +- .../x86/hosted-linux/Wayland_Display.zig | 2 +- .../machine/x86/hosted-linux/X11_Display.zig | 2 +- 6 files changed, 136 insertions(+), 100 deletions(-) create mode 100644 src/kernel/drivers/video/Externally_Managed_Output.zig delete mode 100644 src/kernel/drivers/video/Host_VNC_Output.zig diff --git a/src/kernel/drivers/drivers.zig b/src/kernel/drivers/drivers.zig index 5d509667..c08babd8 100644 --- a/src/kernel/drivers/drivers.zig +++ b/src/kernel/drivers/drivers.zig @@ -44,7 +44,7 @@ pub const video = struct { pub const VESA_BIOS_Extension = @import("video/VESA_BIOS_Extension.zig"); pub const VGA = @import("video/VGA.zig"); pub const Virtual_Video_Output = @import("video/Virtual_Video_Output.zig"); - pub const Host_VNC_Output = @import("video/Host_VNC_Output.zig"); + pub const Externally_Managed_Output = @import("video/Externally_Managed_Output.zig"); pub const Host_SDL_Output = @import("video/Host_SDL_Output.zig"); pub const ILI9488 = @import("video/ILI9488.zig"); pub const HSTX_DVI = @import("video/HSTX_DVI.zig"); diff --git a/src/kernel/drivers/video/Externally_Managed_Output.zig b/src/kernel/drivers/video/Externally_Managed_Output.zig new file mode 100644 index 00000000..bf8bcb6a --- /dev/null +++ b/src/kernel/drivers/video/Externally_Managed_Output.zig @@ -0,0 +1,131 @@ +//! +//! This video driver is a thin layer inserted between another subsystem +//! and the kernel. +//! +//! Primarily intended for the "hosted" targets, this video output basically +//! converts a video output instance into a callback + parameter, allowing +//! generic use and embedding in other components. +//! +//! This driver implements an optional retaining native-color format framebuffer +//! which allows fullscreen conversion. +//! + +const std = @import("std"); +const ashet = @import("../../main.zig"); +const logger = std.log.scoped(.virtual_screen); + +const Host_VNC_Output = @This(); +const Driver = ashet.drivers.Driver; +const Color = ashet.abi.Color; +const Resolution = ashet.abi.Size; + +pub const WritePixelsSyncFn = fn ( + context: ?*anyopaque, + rectangle: ashet.abi.Rectangle, + pixels: []const Color, + stride: usize, + mode: ashet.abi.video.PresentMode, +) void; + +pub const BackingStorage = enum { allocate, virtual }; + +backbuffer_lock: std.Thread.Mutex = .{}, + +backbuffer: ?[]Color, +width: u16, +height: u16, + +driver: Driver, + +write_pixels_fn: *const WritePixelsSyncFn, +write_pixels_arg: ?*anyopaque, + +pub fn init( + comptime name: []const u8, + width: u16, + height: u16, + comptime write_pixels_fn: WritePixelsSyncFn, + write_pixels_arg: ?*anyopaque, + comptime backing: BackingStorage, +) !Host_VNC_Output { + const fb: ?[]Color = switch (backing) { + .allocate => try std.heap.page_allocator.alloc(Color, @as(u32, width) * @as(u32, height)), + .virtual => null, + }; + errdefer @compileError("No errors beyond this point."); + + return .{ + .driver = comptime .{ + .name = name, + .class = .{ + .video = .{ + .get_properties_fn = get_properties, + .begin_write_pixels_fn = begin_write_pixels, + }, + }, + }, + + .width = width, + .height = height, + .backbuffer = fb, + + .write_pixels_fn = &write_pixels_fn, + .write_pixels_arg = write_pixels_arg, + }; +} + +fn get_properties(driver: *Driver) ashet.video.DeviceProperties { + const vd: *Host_VNC_Output = @fieldParentPtr("driver", driver); + return .{ + .resolution = .{ + .width = vd.width, + .height = vd.height, + }, + }; +} + +fn begin_write_pixels( + driver: *Driver, + call: *ashet.overlapped.AsyncCall, + rectangle: ashet.abi.Rectangle, + pixels: []const Color, + stride: usize, + mode: ashet.abi.video.PresentMode, +) void { + const vd: *Host_VNC_Output = @fieldParentPtr("driver", driver); + + if (vd.backbuffer) |backbuffer| { + ashet.video.utils.copy_pixels( + Color, + .{ + .dst_buffer = .{ + .data = backbuffer.ptr, + .width = vd.width, + .height = vd.height, + .stride = vd.width, + }, + .dst_pos = .{ + .x = @intCast(rectangle.x), + .y = @intCast(rectangle.y), + }, + .src_buffer = .{ + .data = pixels.ptr, + .width = rectangle.width, + .height = rectangle.height, + .stride = stride, + }, + }, + null, + ); + } + + vd.write_pixels_fn( + vd.write_pixels_ctx, + rectangle, + pixels, + stride, + mode, + ); + + return call.finalize(ashet.abi.video.WritePixels, .{}); +} diff --git a/src/kernel/drivers/video/Host_VNC_Output.zig b/src/kernel/drivers/video/Host_VNC_Output.zig deleted file mode 100644 index 9d1decb8..00000000 --- a/src/kernel/drivers/video/Host_VNC_Output.zig +++ /dev/null @@ -1,95 +0,0 @@ -const std = @import("std"); -const ashet = @import("../../main.zig"); -const logger = std.log.scoped(.virtual_screen); - -const Host_VNC_Output = @This(); -const Driver = ashet.drivers.Driver; -const Color = ashet.abi.Color; -const Resolution = ashet.abi.Size; - -const VNC_Server = @import("../../port/hosted/VNC_Server.zig"); - -backbuffer_lock: std.Thread.Mutex = .{}, - -backbuffer: []Color, -width: u16, -height: u16, - -driver: Driver = .{ - .name = "Host VNC Screen", - .class = .{ - .video = .{ - .get_properties_fn = get_properties, - .begin_write_pixels_fn = begin_write_pixels, - }, - }, -}, - -pub fn init( - width: u16, - height: u16, -) !Host_VNC_Output { - const fb = try std.heap.page_allocator.alloc(Color, @as(u32, width) * @as(u32, height)); - errdefer std.heap.page_allocator.free(fb); - - return .{ - .width = width, - .height = height, - .backbuffer = fb, - }; -} - -fn get_properties(driver: *Driver) ashet.video.DeviceProperties { - const vd: *Host_VNC_Output = @fieldParentPtr("driver", driver); - return .{ - .resolution = .{ - .width = vd.width, - .height = vd.height, - }, - }; -} - -fn vnc_server(output: *Host_VNC_Output) *VNC_Server { - return @fieldParentPtr("screen", output); -} - -fn begin_write_pixels( - driver: *Driver, - call: *ashet.overlapped.AsyncCall, - rectangle: ashet.abi.Rectangle, - pixels: []const Color, - stride: usize, - mode: ashet.abi.video.PresentMode, -) void { - const vd: *Host_VNC_Output = @fieldParentPtr("driver", driver); - - ashet.video.utils.copy_pixels( - Color, - .{ - .dst_buffer = .{ - .data = vd.backbuffer.ptr, - .width = vd.width, - .height = vd.height, - .stride = vd.width, - }, - .dst_pos = .{ - .x = @intCast(rectangle.x), - .y = @intCast(rectangle.y), - }, - .src_buffer = .{ - .data = pixels.ptr, - .width = rectangle.width, - .height = rectangle.height, - .stride = stride, - }, - }, - null, - ); - - switch (mode) { - .dont_care => {}, - .immediate, .vblank => vd.vnc_server().notify_flush(), - } - - return call.finalize(ashet.abi.video.WritePixels, .{}); -} diff --git a/src/kernel/port/hosted/VNC_Server.zig b/src/kernel/port/hosted/VNC_Server.zig index f6c28ef4..cc1b56d4 100644 --- a/src/kernel/port/hosted/VNC_Server.zig +++ b/src/kernel/port/hosted/VNC_Server.zig @@ -13,7 +13,7 @@ const VNC_Server = @This(); allocator: std.mem.Allocator, socket: network.Socket, -screen: ashet.drivers.video.Host_VNC_Output, +screen: ashet.drivers.video.Externally_Managed_Output, input: ashet.drivers.input.Host_VNC_Input, /// Guards the `current_session` field access. @@ -44,7 +44,7 @@ pub fn init( server.* = .{ .allocator = allocator, .socket = server_sock, - .screen = try ashet.drivers.video.Host_VNC_Output.init(width, height), + .screen = try ashet.drivers.video.Externally_Managed_Output.init(width, height), .input = ashet.drivers.input.Host_VNC_Input.init(), }; diff --git a/src/kernel/port/machine/x86/hosted-linux/Wayland_Display.zig b/src/kernel/port/machine/x86/hosted-linux/Wayland_Display.zig index c7c99547..5625d163 100644 --- a/src/kernel/port/machine/x86/hosted-linux/Wayland_Display.zig +++ b/src/kernel/port/machine/x86/hosted-linux/Wayland_Display.zig @@ -47,7 +47,7 @@ should_render: bool = true, running: bool = true, // devices: -screen: ashet.drivers.video.Host_VNC_Output, +screen: ashet.drivers.video.Externally_Managed_Output, // input: ashet.drivers.input.Host_SDL_Input, window_width: u31, diff --git a/src/kernel/port/machine/x86/hosted-linux/X11_Display.zig b/src/kernel/port/machine/x86/hosted-linux/X11_Display.zig index 493b2fc9..a8ab190e 100644 --- a/src/kernel/port/machine/x86/hosted-linux/X11_Display.zig +++ b/src/kernel/port/machine/x86/hosted-linux/X11_Display.zig @@ -39,7 +39,7 @@ put_image_msg_buffer: []align(4) u8, put_image_chunk_height: u16, // devices: -screen: ashet.drivers.video.Host_VNC_Output, +screen: ashet.drivers.video.Externally_Managed_Output, // input: ashet.drivers.input.Host_SDL_Input, pub fn init( From 481039993be3df807dbc1b78d7c8bdae5990e487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Wed, 9 Sep 2026 22:38:58 +0200 Subject: [PATCH 09/17] Makes all graphics drivers now compile with the new begin_write_pixels_fn --- .../video/Externally_Managed_Output.zig | 6 +- src/kernel/drivers/video/HSTX_DVI_2.zig | 43 ++++++++++-- .../video/Memory_Mapped_Framebuffer.zig | 63 ++++++++++++++---- src/kernel/drivers/video/VGA.zig | 46 ++++++++++--- .../drivers/video/Virtio_GPU_Device.zig | 66 +++++++++++++++---- src/kernel/port/hosted/VNC_Server.zig | 39 +++++++++-- .../x86/hosted-linux/Wayland_Display.zig | 31 ++++++++- .../machine/x86/hosted-linux/X11_Display.zig | 40 +++++++++-- 8 files changed, 274 insertions(+), 60 deletions(-) diff --git a/src/kernel/drivers/video/Externally_Managed_Output.zig b/src/kernel/drivers/video/Externally_Managed_Output.zig index bf8bcb6a..82c9a7d2 100644 --- a/src/kernel/drivers/video/Externally_Managed_Output.zig +++ b/src/kernel/drivers/video/Externally_Managed_Output.zig @@ -38,14 +38,14 @@ height: u16, driver: Driver, write_pixels_fn: *const WritePixelsSyncFn, -write_pixels_arg: ?*anyopaque, +write_pixels_ctx: ?*anyopaque, pub fn init( comptime name: []const u8, width: u16, height: u16, comptime write_pixels_fn: WritePixelsSyncFn, - write_pixels_arg: ?*anyopaque, + write_pixels_ctx: ?*anyopaque, comptime backing: BackingStorage, ) !Host_VNC_Output { const fb: ?[]Color = switch (backing) { @@ -70,7 +70,7 @@ pub fn init( .backbuffer = fb, .write_pixels_fn = &write_pixels_fn, - .write_pixels_arg = write_pixels_arg, + .write_pixels_ctx = write_pixels_ctx, }; } diff --git a/src/kernel/drivers/video/HSTX_DVI_2.zig b/src/kernel/drivers/video/HSTX_DVI_2.zig index a6dfeeb5..be10f852 100644 --- a/src/kernel/drivers/video/HSTX_DVI_2.zig +++ b/src/kernel/drivers/video/HSTX_DVI_2.zig @@ -98,8 +98,8 @@ pub fn init(comptime clock_config: rp2350.clocks.config.Global) !HSTX_DVI { .class = .{ .video = .{ .get_properties_fn = get_properties, - .flush_fn = flush, .get_one_vblank_event_fn = get_one_vblank_event, + .begin_write_pixels_fn = begin_write_pixels, }, }, }, @@ -116,10 +116,7 @@ fn get_properties(dri: *Driver) ashet.video.DeviceProperties { const vd = instance(dri); _ = vd; return .{ - .video_memory = &framebuffer, - .video_memory_mapping = .unbuffered, .resolution = framebuffer_size, - .stride = framebuffer_size.width, }; } @@ -132,9 +129,41 @@ fn get_one_vblank_event(dri: *Driver) bool { return had_vsync; } -fn flush(dri: *Driver) void { - const vd = instance(dri); - _ = vd; +fn begin_write_pixels( + driver: *Driver, + call: *ashet.overlapped.AsyncCall, + rectangle: ashet.abi.Rectangle, + pixels: []const Color, + stride: usize, + mode: ashet.abi.video.PresentMode, +) void { + _ = driver; + _ = mode; + + ashet.video.utils.copy_pixels( + Color, + .{ + .dst_buffer = .{ + .data = &framebuffer, + .width = framebuffer_size.width, + .height = framebuffer_size.height, + .stride = framebuffer_size.width, + }, + .dst_pos = .{ + .x = @intCast(rectangle.x), + .y = @intCast(rectangle.y), + }, + .src_buffer = .{ + .data = pixels.ptr, + .width = rectangle.width, + .height = rectangle.height, + .stride = stride, + }, + }, + null, + ); + + return call.finalize(ashet.abi.video.WritePixels, .{}); } const dma_data0_section = ".sram.bank2"; diff --git a/src/kernel/drivers/video/Memory_Mapped_Framebuffer.zig b/src/kernel/drivers/video/Memory_Mapped_Framebuffer.zig index 9db0f302..db0909d8 100644 --- a/src/kernel/drivers/video/Memory_Mapped_Framebuffer.zig +++ b/src/kernel/drivers/video/Memory_Mapped_Framebuffer.zig @@ -47,7 +47,7 @@ pub fn create(allocator: std.mem.Allocator, driver_name: []const u8, config: Con .name = driver_name, .class = .{ .video = .{ - .flush_fn = framebuffer.flush_fn, + .begin_write_pixels_fn = begin_write_pixels, .get_properties_fn = get_properties, }, }, @@ -77,6 +77,54 @@ pub fn create(allocator: std.mem.Allocator, driver_name: []const u8, config: Con return driver; } +fn get_properties(driver: *Driver) ashet.video.DeviceProperties { + const vd: *Memory_Mapped_Framebuffer = @fieldParentPtr("driver", driver); + return .{ + .resolution = .{ + .width = vd.width, + .height = vd.height, + }, + }; +} + +fn begin_write_pixels( + driver: *Driver, + call: *ashet.overlapped.AsyncCall, + rectangle: ashet.abi.Rectangle, + pixels: []const Color, + stride: usize, + mode: ashet.abi.video.PresentMode, +) void { + const vd: *Memory_Mapped_Framebuffer = @fieldParentPtr("driver", driver); + + ashet.video.utils.copy_pixels( + Color, + .{ + .dst_buffer = .{ + .data = vd.backing_buffer.ptr, + .width = vd.width, + .height = vd.height, + .stride = vd.width, + }, + .dst_pos = .{ + .x = @intCast(rectangle.x), + .y = @intCast(rectangle.y), + }, + .src_buffer = .{ + .data = pixels.ptr, + .width = rectangle.width, + .height = rectangle.height, + .stride = stride, + }, + }, + null, + ); + + _ = mode; + + return call.finalize(ashet.abi.video.WritePixels, .{}); +} + pub const Framebuffer = struct { flush_fn: *const fn (*Driver) void, base: [*]u8, @@ -241,16 +289,3 @@ pub const Config = struct { }.flush; } }; - -fn get_properties(driver: *Driver) ashet.video.DeviceProperties { - const vd: *Memory_Mapped_Framebuffer = @fieldParentPtr("driver", driver); - return .{ - .resolution = .{ - .width = vd.width, - .height = vd.height, - }, - .stride = vd.width, - .video_memory = vd.backing_buffer, - .video_memory_mapping = .buffered, - }; -} diff --git a/src/kernel/drivers/video/VGA.zig b/src/kernel/drivers/video/VGA.zig index f307ba3b..72ed9828 100644 --- a/src/kernel/drivers/video/VGA.zig +++ b/src/kernel/drivers/video/VGA.zig @@ -20,7 +20,7 @@ driver: Driver = .{ .class = .{ .video = .{ .get_properties_fn = get_properties, - .flush_fn = flush, + .begin_write_pixels_fn = begin_write_pixels, }, }, }, @@ -58,24 +58,54 @@ pub fn init(vga: *VGA) !void { fn get_properties(driver: *Driver) ashet.video.DeviceProperties { const vd: *VGA = @alignCast(@fieldParentPtr("driver", driver)); + _ = vd; return .{ .resolution = .{ .width = width, .height = height, }, - .stride = width, - .video_memory = &vd.backbuffer, - .video_memory_mapping = .buffered, }; } -fn flush(driver: *Driver) void { +fn begin_write_pixels( + driver: *Driver, + call: *ashet.overlapped.AsyncCall, + rectangle: ashet.abi.Rectangle, + pixels: []const Color, + stride: usize, + mode: ashet.abi.video.PresentMode, +) void { const vd: *VGA = @alignCast(@fieldParentPtr("driver", driver)); - - // vd.loadPalette(vd.palette); + _ = vd; const target = @as([*]align(ashet.memory.page_size) Color, @ptrFromInt(0xA0000))[0 .. width * height]; - std.mem.copyForwards(Color, target, &vd.backbuffer); + + ashet.video.utils.copy_pixels( + Color, + .{ + .dst_buffer = .{ + .data = target, + .width = width, + .height = height, + .stride = width, + }, + .dst_pos = .{ + .x = @intCast(rectangle.x), + .y = @intCast(rectangle.y), + }, + .src_buffer = .{ + .data = pixels.ptr, + .width = rectangle.width, + .height = rectangle.height, + .stride = stride, + }, + }, + null, + ); + + _ = mode; + + return call.finalize(ashet.abi.video.WritePixels, .{}); } fn writeVgaRegisters(regs: [61]u8) void { diff --git a/src/kernel/drivers/video/Virtio_GPU_Device.zig b/src/kernel/drivers/video/Virtio_GPU_Device.zig index 10e11150..1fe37aec 100644 --- a/src/kernel/drivers/video/Virtio_GPU_Device.zig +++ b/src/kernel/drivers/video/Virtio_GPU_Device.zig @@ -15,16 +15,16 @@ backing_buffer: [max_width * max_height]Color align(ashet.memory.page_size) = un gpu: GPU, -graphics_resized: bool = true, -graphics_width: u16 = 256, -graphics_height: u16 = 128, +graphics_resized: bool, // TODO(gpu_support): Drop this, and compute the border once at the start +graphics_width: u16, +graphics_height: u16, driver: Driver = .{ .name = "Virtio GPU Device", .class = .{ .video = .{ .get_properties_fn = get_properties, - .flush_fn = flush, + .begin_write_pixels_fn = begin_write_pixels, }, }, }, @@ -36,13 +36,13 @@ pub fn init(allocator: std.mem.Allocator, index: usize, regs: *volatile virtio.C vd.* = Virtio_GPU_Device{ .gpu = undefined, + .graphics_resized = true, + .graphics_width = @intCast(@min(std.math.maxInt(u16), vd.gpu.fb_width)), + .graphics_height = @intCast(@min(std.math.maxInt(u16), vd.gpu.fb_height)), }; try vd.gpu.initialize(allocator, regs); - vd.graphics_width = @intCast(@min(std.math.maxInt(u16), vd.gpu.fb_width)); - vd.graphics_height = @intCast(@min(std.math.maxInt(u16), vd.gpu.fb_height)); - @memset(&vd.backing_buffer, ashet.video.defaults.border_color); ashet.video.load_splash_screen(.{ @@ -52,7 +52,7 @@ pub fn init(allocator: std.mem.Allocator, index: usize, regs: *volatile virtio.C .stride = vd.graphics_width, }); - vd.driver.class.video.flush(); + vd.flush(); return vd; } @@ -77,6 +77,49 @@ inline fn pal(vd: *Virtio_GPU_Device, color: Color) u32 { // return @intFromEnum(color.to_abgr8888()); } +fn begin_write_pixels( + driver: *Driver, + call: *ashet.overlapped.AsyncCall, + rectangle: ashet.abi.Rectangle, + pixels: []const Color, + stride: usize, + mode: ashet.abi.video.PresentMode, +) void { + const vd: *Virtio_GPU_Device = @alignCast(@fieldParentPtr("driver", driver)); + + // TODO(gpu_support): We can refactor this to directly convert the pixels into the expected virtio GPU format. + + ashet.video.utils.copy_pixels( + Color, + .{ + .dst_buffer = .{ + .data = &vd.backing_buffer, + .width = vd.graphics_width, + .height = vd.graphics_height, + .stride = vd.graphics_width, + }, + .dst_pos = .{ + .x = @intCast(rectangle.x), + .y = @intCast(rectangle.y), + }, + .src_buffer = .{ + .data = pixels.ptr, + .width = rectangle.width, + .height = rectangle.height, + .stride = stride, + }, + }, + null, + ); + + switch (mode) { + .immediate, .vblank => vd.flush(), + .dont_care => {}, + } + + return call.finalize(ashet.abi.video.WritePixels, .{}); +} + fn get_properties(driver: *Driver) ashet.video.DeviceProperties { const vd: *Virtio_GPU_Device = @alignCast(@fieldParentPtr("driver", driver)); return .{ @@ -84,9 +127,6 @@ fn get_properties(driver: *Driver) ashet.video.DeviceProperties { .width = vd.graphics_width, .height = vd.graphics_height, }, - .stride = vd.graphics_width, - .video_memory = &vd.backing_buffer, - .video_memory_mapping = .buffered, }; } @@ -101,9 +141,7 @@ fn get_properties(driver: *Driver) ashet.video.DeviceProperties { /// no_mul,Debug: debug(platform-virt): frame flush time: 38071785 cycles, avg 41297847 cycles /// no_runsaf,Debug: debug(platform-virt): frame flush time: 36532901 cycles, avg 41069180 cycles /// no safety,Debug: debug(platform-virt): frame flush time: 35441413 cycles, avg 35434932 cycles -fn flush(driver: *Driver) void { - const vd: *Virtio_GPU_Device = @alignCast(@fieldParentPtr("driver", driver)); - +fn flush(vd: *Virtio_GPU_Device) void { @setRuntimeSafety(false); // const flush_time_start = readHwCounter(); diff --git a/src/kernel/port/hosted/VNC_Server.zig b/src/kernel/port/hosted/VNC_Server.zig index cc1b56d4..caa90679 100644 --- a/src/kernel/port/hosted/VNC_Server.zig +++ b/src/kernel/port/hosted/VNC_Server.zig @@ -44,7 +44,14 @@ pub fn init( server.* = .{ .allocator = allocator, .socket = server_sock, - .screen = try ashet.drivers.video.Externally_Managed_Output.init(width, height), + .screen = try ashet.drivers.video.Externally_Managed_Output.init( + "VNC Output", + width, + height, + write_vnc_pixels, + server, + .allocate, + ), .input = ashet.drivers.input.Host_VNC_Input.init(), }; @@ -94,10 +101,10 @@ fn connection_handler(vd: *VNC_Server) !void { }, .{ .reader = &read_buffer, .writer = &write_buffer }); defer server.close(); - const new_framebuffer = try local_allocator.dupe(ashet.abi.Color, vd.screen.backbuffer); + const new_framebuffer = try local_allocator.dupe(ashet.abi.Color, vd.screen.backbuffer.?); defer local_allocator.free(new_framebuffer); - const old_framebuffer = try local_allocator.dupe(ashet.abi.Color, vd.screen.backbuffer); + const old_framebuffer = try local_allocator.dupe(ashet.abi.Color, vd.screen.backbuffer.?); defer local_allocator.free(old_framebuffer); std.debug.print("protocol version: {}\n", .{server.protocol_version}); @@ -231,9 +238,31 @@ const Session_State = struct { // end of write_lock guard. }; +fn write_vnc_pixels( + context: ?*anyopaque, + rectangle: ashet.abi.Rectangle, + pixels: []const ashet.abi.Color, + stride: usize, + mode: ashet.abi.video.PresentMode, +) void { + // TODO(gpu_support): Improve this function to utilize the provided information. + const server: *VNC_Server = @ptrCast(@alignCast(context.?)); + + _ = rectangle; + _ = pixels; + _ = stride; + + switch (mode) { + .vblank, .immediate => { + server.notify_flush(); + }, + .dont_care => {}, + } +} + /// Notifies the VNC_Server of a flush event of the screen device. /// This allows us to hold back incremental updates until new content arrives. -pub fn notify_flush(vd: *VNC_Server) void { +fn notify_flush(vd: *VNC_Server) void { vd.session_lock.lock(); defer vd.session_lock.unlock(); @@ -260,7 +289,7 @@ fn send_incremental_update(vd: *VNC_Server, state: *Session_State, request_alloc { // vd.screen.backbuffer_lock.lock(); // defer vd.screen.backbuffer_lock.unlock(); - @memcpy(state.new_framebuffer, vd.screen.backbuffer); + @memcpy(state.new_framebuffer, vd.screen.backbuffer.?); } if (state.server.pixel_format.is_indexed() and !state.sent_color_map) { diff --git a/src/kernel/port/machine/x86/hosted-linux/Wayland_Display.zig b/src/kernel/port/machine/x86/hosted-linux/Wayland_Display.zig index 5625d163..4b02ac6b 100644 --- a/src/kernel/port/machine/x86/hosted-linux/Wayland_Display.zig +++ b/src/kernel/port/machine/x86/hosted-linux/Wayland_Display.zig @@ -67,7 +67,14 @@ pub fn init( .allocator = allocator, .index = index, - .screen = try .init(width, height), + .screen = try ashet.drivers.video.Externally_Managed_Output.init( + "Wayland Window Output", + width, + height, + write_wayland_pixels, + server, + .allocate, // TODO(gpu_support): Is this necessary? + ), // .input = ashet.drivers.input.Host_SDL_Input.init(), .window_width = @max(1, initial_scale) * width, @@ -85,7 +92,7 @@ pub fn init( .swap_chain = undefined, }; - @memset(server.screen.backbuffer, ashet.abi.Color.red); + @memset(server.screen.backbuffer.?, ashet.abi.Color.red); server.connection = shimizu.posix.Connection.open(allocator, .{}) catch |err| switch (err) { error.FileNotFound => return error.NoWaylandSupport, @@ -159,6 +166,24 @@ pub fn init( return server; } +fn write_wayland_pixels( + context: ?*anyopaque, + rectangle: ashet.abi.Rectangle, + pixels: []const ashet.abi.Color, + stride: usize, + mode: ashet.abi.video.PresentMode, +) void { + const display: *Wayland_Display = @ptrCast(@alignCast(context.?)); + + // TODO(gpu_server): Do we need to operate here? + + _ = rectangle; + _ = pixels; + _ = stride; + _ = mode; + _ = display; +} + pub fn process_events_wrapper(server_ptr: ?*anyopaque) callconv(.c) u32 { const server: *Wayland_Display = @ptrCast(@alignCast(server_ptr.?)); @@ -287,7 +312,7 @@ fn copyFromDriver(server: *Wayland_Display, pixels: []Pixel) void { std.debug.assert(2 * offset_x + scaled_w <= window_w); std.debug.assert(2 * offset_y + scaled_h <= window_h); - var src_ptr: [*]const ashet.abi.Color = server.screen.backbuffer.ptr; + var src_ptr: [*]const ashet.abi.Color = server.screen.backbuffer.?.ptr; var dst_ptr: [*]Pixel = pixels.ptr + window_w * offset_y + offset_x; for (0..content_h) |_| { diff --git a/src/kernel/port/machine/x86/hosted-linux/X11_Display.zig b/src/kernel/port/machine/x86/hosted-linux/X11_Display.zig index a8ab190e..4ceaee0c 100644 --- a/src/kernel/port/machine/x86/hosted-linux/X11_Display.zig +++ b/src/kernel/port/machine/x86/hosted-linux/X11_Display.zig @@ -38,6 +38,8 @@ running: bool = true, put_image_msg_buffer: []align(4) u8, put_image_chunk_height: u16, +screen_dirty: bool = true, + // devices: screen: ashet.drivers.video.Externally_Managed_Output, // input: ashet.drivers.input.Host_SDL_Input, @@ -98,7 +100,14 @@ pub fn init( .allocator = allocator, .index = index, - .screen = try .init(window_width, window_height), + .screen = try ashet.drivers.video.Externally_Managed_Output.init( + "X11 Window Output", + window_width, + window_height, + write_x11_pixels, + null, + .allocate, + ), // .input = ashet.drivers.input.Host_SDL_Input.init(), .socket_read_buffer = socket_read_buffer, @@ -123,7 +132,7 @@ pub fn init( server.source = x11.Source.initAfterSetup(server.socket_reader.interface()); server.sink = .{ .writer = &server.socket_writer.interface }; - @memset(server.screen.backbuffer, ashet.abi.Color.red); + @memset(server.screen.backbuffer.?, ashet.abi.Color.red); const base_resource = server.setup.resource_id_base; @@ -176,6 +185,25 @@ pub fn init( return server; } +fn write_x11_pixels( + context: ?*anyopaque, + rectangle: ashet.abi.Rectangle, + pixels: []const ashet.abi.Color, + stride: usize, + mode: ashet.abi.video.PresentMode, +) void { + const display: *X11_Display = @ptrCast(@alignCast(context.?)); + + _ = rectangle; + _ = pixels; + _ = stride; + _ = mode; + + // TODO(gpu_server): We can directly send the right X11 frames here + // instead of doing the thread dance. + display.screen_dirty = true; +} + pub fn process_events_wrapper(server_ptr: ?*anyopaque) callconv(.c) u32 { const server: *X11_Display = @ptrCast(@alignCast(server_ptr.?)); @@ -374,20 +402,20 @@ fn handle_mouse_button_event(server: *X11_Display, button: u8, is_press: bool) ! } fn render_on_demand(server: *X11_Display) !void { - defer std.debug.assert(server.screen.backbuffer_dirty == false); + defer std.debug.assert(server.screen_dirty == false); - if (server.screen.backbuffer_dirty) { + if (server.screen_dirty) { try server.force_render(); } } fn force_render(server: *X11_Display) !void { - defer server.screen.backbuffer_dirty = false; + defer server.screen_dirty = false; // Put window content in chunks, as we can't transfer full images // as X11 has only maxInt(u18) - var source_pixels: [*]const ashet.abi.Color = server.screen.backbuffer.ptr; + var source_pixels: [*]const ashet.abi.Color = server.screen.backbuffer.?.ptr; var base_y: u16 = 0; while (base_y < server.screen.height) : (base_y += server.put_image_chunk_height) { // BB GG RR XX From 3eb35c15fa00fd2b5ea72726a6aaa1cc34471a5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Wed, 9 Sep 2026 23:19:42 +0200 Subject: [PATCH 10/17] Makes OS compile again --- .../drivers/video/Ashet_Framebuffer.zig | 38 +++++++++++++++++-- .../desktop/classic/src/classic-desktop.zig | 17 --------- .../module/src/libashet/graphics.zig | 4 +- .../libAshetOS/module/src/libashet/video.zig | 15 +++++--- 4 files changed, 46 insertions(+), 28 deletions(-) diff --git a/src/kernel/drivers/video/Ashet_Framebuffer.zig b/src/kernel/drivers/video/Ashet_Framebuffer.zig index e7972017..16b246a4 100644 --- a/src/kernel/drivers/video/Ashet_Framebuffer.zig +++ b/src/kernel/drivers/video/Ashet_Framebuffer.zig @@ -16,7 +16,7 @@ driver: Driver = .{ .class = .{ .video = .{ .get_properties_fn = get_properties, - .flush_fn = flush, + .begin_write_pixels_fn = begin_write_pixels, }, }, }, @@ -57,10 +57,40 @@ fn get_properties(driver: *Driver) ashet.video.DeviceProperties { }; } -fn flush(driver: *Driver) void { +fn begin_write_pixels( + driver: *Driver, + call: *ashet.overlapped.AsyncCall, + rectangle: ashet.abi.Rectangle, + pixels: []const Color, + stride: usize, + mode: ashet.abi.video.PresentMode, +) void { const vd = driver.resolve(Ashet_Framebuffer, "driver"); - vd.control.flush = 1; + ashet.video.utils.copy_pixels( + Color, + .{ + .dst_buffer = .{ + .data = vd.framebuffer, + .width = width, + .height = height, + .stride = width, + }, + .dst_pos = .{ + .x = @intCast(rectangle.x), + .y = @intCast(rectangle.y), + }, + .src_buffer = .{ + .data = pixels.ptr, + .width = rectangle.width, + .height = rectangle.height, + .stride = stride, + }, + }, + null, + ); + + _ = mode; - // TODO: wait for vblank? + return call.finalize(ashet.abi.video.WritePixels, .{}); } diff --git a/src/userland/apps/desktop/classic/src/classic-desktop.zig b/src/userland/apps/desktop/classic/src/classic-desktop.zig index 3696ab5b..b15e305f 100644 --- a/src/userland/apps/desktop/classic/src/classic-desktop.zig +++ b/src/userland/apps/desktop/classic/src/classic-desktop.zig @@ -48,23 +48,6 @@ pub fn main() !void { fb_size.height, }); - const vmem = try video_output.get_video_memory(); - std.log.info("video memory: base=0x{X:0>8}, stride={}, width={}, height={}", .{ - @intFromPtr(vmem.base), - vmem.stride, - vmem.width, - vmem.height, - }); - - // Load nice pattern: - var scanline: [*]abi.Color = vmem.base; - for (0..vmem.height) |y| { - for (scanline[0..vmem.width], 0..) |*pixel, x| { - pixel.* = Color.from_u8(@as(u4, @truncate(x ^ y))); - } - scanline += vmem.stride; - } - // Let the rest of the system continue to boot: ashet.process.thread.yield(); diff --git a/src/userland/libs/libAshetOS/module/src/libashet/graphics.zig b/src/userland/libs/libAshetOS/module/src/libashet/graphics.zig index ecdd78ec..a02fdcb4 100644 --- a/src/userland/libs/libAshetOS/module/src/libashet/graphics.zig +++ b/src/userland/libs/libAshetOS/module/src/libashet/graphics.zig @@ -184,7 +184,7 @@ pub fn create_widget_framebuffer(widget: ashet.abi.Widget) !Framebuffer { return try ashet.abi.draw.create_widget_framebuffer(widget); } -pub fn get_framebuffer_memory(fb: Framebuffer) !ashet.abi.VideoMemory { +pub fn get_framebuffer_memory(fb: Framebuffer) !ashet.abi.video.VideoMemory { return try ashet.abi.draw.get_framebuffer_memory(fb); } @@ -267,7 +267,7 @@ pub const abm = struct { return header; } - pub fn read_pixels(file: ashet.fs.File, abm_offset: u64, header: Header, vmem: ashet.abi.VideoMemory) !void { + pub fn read_pixels(file: ashet.fs.File, abm_offset: u64, header: Header, vmem: ashet.abi.video.VideoMemory) !void { const pixel_count: u32 = @as(u32, header.width) * @as(u32, header.height); const pixel_offset: u64 = @sizeOf(abm.Header); diff --git a/src/userland/libs/libAshetOS/module/src/libashet/video.zig b/src/userland/libs/libAshetOS/module/src/libashet/video.zig index db8a66bb..1d6f0d88 100644 --- a/src/userland/libs/libAshetOS/module/src/libashet/video.zig +++ b/src/userland/libs/libAshetOS/module/src/libashet/video.zig @@ -4,23 +4,28 @@ const ashet = @import("../libashet.zig"); const abi = ashet.abi; +pub const VideoOutputID = abi.video.VideoOutputID; +pub const VideoMemory = abi.video.VideoMemory; +pub const BufferKind = abi.video.BufferKind; +pub const PresentMode = abi.video.PresentMode; + pub const WaitForVBlank = ashet.abi.video.WaitForVBlank; pub const Output = opaque { pub fn release(out: *Output) void { - _ = out; + abi.resources.release(.from_ptr(out)); } pub fn get_resolution(out: *Output) !abi.Size { return try abi.video.get_resolution(@ptrCast(out)); } - pub fn get_video_memory(out: *Output) !abi.VideoMemory { - return try abi.video.get_video_memory(@ptrCast(out)); - } + // pub fn get_video_memory(out: *Output) !abi.VideoMemory { + // return try abi.video.get_video_memory(@ptrCast(out)); + // } }; -pub fn acquire(id: abi.VideoOutputID) !*Output { +pub fn acquire(id: VideoOutputID) !*Output { return @ptrCast( try abi.video.acquire(id), ); From f042f4becad3c3cce1cfb1c205cbf8bd94eed095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Fri, 11 Sep 2026 14:15:01 +0200 Subject: [PATCH 11/17] Starts working on mappable buffers, makes x86 compile again --- justfile | 3 + scripts/gdb | 2 +- scripts/gdb.sh | 4 +- src/abi/src/ashet.abi | 8 +- src/kernel/components/video.zig | 149 +++++++++++++++--- .../video/Memory_Mapped_Framebuffer.zig | 17 +- src/kernel/drivers/video/VGA.zig | 14 ++ .../drivers/video/Virtio_GPU_Device.zig | 4 + src/kernel/port/platform/rv32.zig | 2 + 9 files changed, 178 insertions(+), 25 deletions(-) diff --git a/justfile b/justfile index de428513..fd464604 100644 --- a/justfile +++ b/justfile @@ -20,6 +20,9 @@ run-avap-simulation: "drive;zig-out/x86-hosted-linux/disk.img" \ "video;avap-v1;640;400;/dev/serial/by-id/usb-Ashet_Technologies_Fast_Bridge_AT-FB-00001-if00-port0" +run machine: + {{zig}} build {{default_params}} --summary none -Doptimize-kernel={{optimize_kernel}} -Doptimize-apps={{optimize_apps}} -Dmachine={{machine}} tools run + [working-directory: 'src/kernel'] build-kernel: {{zig}} build {{default_params}} -Dmachine=x86-hosted-linux -Dno-emit-bin diff --git a/scripts/gdb b/scripts/gdb index d721e8d6..5bc5ec9d 100644 --- a/scripts/gdb +++ b/scripts/gdb @@ -6,4 +6,4 @@ show disassemble-next-line # break m-profile.zig:51 # break ashet_scheduler_switchTasks # break ashet_scheduler_threadTrampoline -# break multi_tasking.zig:107 \ No newline at end of file +# break multi_tasking.zig:107 diff --git a/scripts/gdb.sh b/scripts/gdb.sh index a5b72901..ea70adf3 100755 --- a/scripts/gdb.sh +++ b/scripts/gdb.sh @@ -5,8 +5,8 @@ export "PATH=/home/felix/projects/forks/binutils-gdb/prefix/bin/:${PATH}" case $MACHINE in rv32_virt) - exec riscv32-none-eabi-gdb \ - "${ROOT}/zig-out/bin/ashet-os" \ + exec riscv32-elf-gdb \ + "${ROOT}/zig-out/rv32-qemu-virt/kernel.elf" \ -ex "target remote localhost:1234" ;; microvm) diff --git a/src/abi/src/ashet.abi b/src/abi/src/ashet.abi index 617c4a50..cffaed06 100644 --- a/src/abi/src/ashet.abi +++ b/src/abi/src/ashet.abi @@ -710,12 +710,16 @@ namespace video { /// Returns a pointer to linear video memory, row-major. /// - /// NOTE: The pointer inside `memory` is only valid until the next `Present` operation - /// for any front or back buffer mapping for the associated video output or until + /// NOTE: The pointer inside `memory` is only valid until the next `Present` or `WritePixels` + /// operation for any front or back buffer mapping for the associated video output or until /// the buffer mapping is destroyed. /// /// This requires careful management and it is not recommended to share different /// `BufferMapping` resources with other actors. + /// + /// LORE: Some video hardware uses dedicated memory buffers that are scanned out (like VGA or RP2350 HSTX), + /// some other hardware uses a double-buffering scheme (like most modern GPUs) where two buffers are + /// allocated, and we always syscall get_video_memory { in buffer: BufferMapping; diff --git a/src/kernel/components/video.zig b/src/kernel/components/video.zig index d18525a9..0faa3886 100644 --- a/src/kernel/components/video.zig +++ b/src/kernel/components/video.zig @@ -6,27 +6,66 @@ const logger = std.log.scoped(.video); pub const Color = ashet.abi.Color; pub const OutputID = ashet.abi.video.VideoOutputID; pub const Resolution = ashet.abi.Size; -pub const VideoMemory = ashet.abi.video.VideoMemory; const Rectangle = ashet.abi.Rectangle; const PresentMode = ashet.abi.video.PresentMode; -pub const Buffering = enum { - buffered, - unbuffered, -}; - pub const DeviceProperties = struct { + /// The video resolution of the device. resolution: Resolution, - // stride: usize, - // video_memory_mapping: Buffering, - // video_memory: []align(ashet.memory.page_size) Color, + /// Determines how buffer mappings work with the device. + buffer_support: MappableBufferSupport, + + pub const MappableBufferSupport = enum { + /// The device does not support memory-mappable buffers. + none, + + /// The device only supports a front buffer, which keeps + /// its address between swaps. + front_stable, + }; +}; + +/// A raw device-backed video memory buffer. +pub const VideoMemory = struct { + /// A pointer to the first pixel. + /// + /// The pixel layout is row-major. This means that we have a + /// sequence of image lines, each line `width` elements long. + /// + /// Lines in the video memory are `stride` elements apart, so + /// the index of a pixel is `stride * y + x`. + base: [*]Color, + + /// The distance of two pixel rows in memory. + /// This unit is provided in "number of `base` indices". + /// + /// NOTE: For the 8 bit color format we use, this is also + /// a byte offset. + stride: usize, + + comptime { + std.debug.assert(@sizeOf(Color) == 1); + } +}; + +/// Determines the type of video memory buffer. +pub const BufferKind = enum { + /// The front buffer is the memory area the + /// scanout unit reads and displays. + /// + /// Changes to this buffer are immediately + /// reflected + front, + + /// + back, }; pub const VideoDevice = struct { get_properties_fn: *const fn (*ashet.drivers.Driver) DeviceProperties, - get_one_vblank_event_fn: ?*const fn (*ashet.drivers.Driver) bool = null, // TODO: Go through all drivers and see which actually support this + get_one_vblank_event_fn: ?*const fn (*ashet.drivers.Driver) bool = null, // TODO(gpu_support): Go through all drivers and see which actually support this begin_write_pixels_fn: *const fn ( driver: *ashet.drivers.Driver, @@ -37,15 +76,25 @@ pub const VideoDevice = struct { mode: PresentMode, ) void, - pub fn get_properties(vd: *VideoDevice) DeviceProperties { + create_mapped_buffer_fn: ?*const fn ( + driver: *ashet.drivers.Driver, + buffer: BufferKind, + ) error{ SystemResources, IoError, Unsupported }!void = unsupported_create_mapped_buffer, + + get_mapped_buffer_fn: ?*const fn ( + driver: *ashet.drivers.Driver, + buffer: BufferKind, + ) error{IoError}!VideoMemory = unsupported_get_mapped_buffer, + + fn get_properties(vd: *VideoDevice) DeviceProperties { // pub return vd.get_properties_fn(ashet.drivers.resolveDriver(.video, vd)); } - pub fn supports_vblank_event(vd: *VideoDevice) bool { + fn supports_vblank_event(vd: *VideoDevice) bool { // pub return vd.get_one_vblank_event_fn != null; } - pub fn get_one_vblank_event(vd: *VideoDevice) bool { + fn get_one_vblank_event(vd: *VideoDevice) bool { // pub if (vd.get_one_vblank_event_fn) |get_one_vblank_event_fn| { return get_one_vblank_event_fn(ashet.drivers.resolveDriver(.video, vd)); } else { @@ -53,7 +102,21 @@ pub const VideoDevice = struct { } } - pub fn begin_write_pixels( + fn create_mapped_buffer( // pub + vd: *VideoDevice, + buffer: BufferKind, + ) error{ SystemResources, IoError, Unsupported }!void { + return vd.create_mapped_buffer_fn(ashet.drivers.resolveDriver(.video, vd), buffer); + } + + fn get_mapped_buffer( // pub + vd: *VideoDevice, + buffer: BufferKind, + ) error{IoError}!VideoMemory { + return vd.create_mapped_buffer_fn(ashet.drivers.resolveDriver(.video, vd), buffer); + } + + fn begin_write_pixels( // pub vd: *VideoDevice, call: *ashet.overlapped.AsyncCall, rectangle: Rectangle, @@ -70,6 +133,54 @@ pub const VideoDevice = struct { mode, ); } + + pub fn default_create_mapped_buffer_front( + driver: *ashet.drivers.Driver, + buffer: BufferKind, + ) error{ SystemResources, IoError, Unsupported }!void { + _ = driver; + switch (buffer) { + .front => {}, + .back => return error.Unsupported, + } + } + + pub fn default_create_mapped_buffer_back( + driver: *ashet.drivers.Driver, + buffer: BufferKind, + ) error{ SystemResources, IoError, Unsupported }!void { + _ = driver; + switch (buffer) { + .front => return error.Unsupported, + .back => {}, + } + } + + pub fn default_create_mapped_buffer_both( + driver: *ashet.drivers.Driver, + buffer: BufferKind, + ) error{ SystemResources, IoError, Unsupported }!void { + _ = driver; + _ = buffer; + } + + fn unsupported_create_mapped_buffer( + driver: *ashet.drivers.Driver, + buffer: BufferKind, + ) error{ SystemResources, IoError, Unsupported }!void { + _ = driver; + _ = buffer; + return error.Unsupported; + } + + fn unsupported_get_mapped_buffer( + driver: *ashet.drivers.Driver, + buffer: BufferKind, + ) error{IoError}!VideoMemory { + _ = driver; + _ = buffer; + @panic("kernel bug: get_mapped_buffer_fn was not correctly set by the "); + } }; pub const Output = struct { @@ -162,7 +273,7 @@ pub const BufferMapping = struct { /// The raw exposed video memory. Writing to this will change the content /// on the screen. /// Memory is interpreted with the current video mode to produce an image. - pub fn get_video_memory(mapping: *const BufferMapping) VideoMemory { + pub fn get_video_memory(mapping: *const BufferMapping) ashet.abi.video.VideoMemory { _ = mapping; @panic("TODO: Implement get_video_memory"); // TODO(gpu_support): Implement get_video_memory // const props = mapping.output.video_driver.get_properties(); @@ -276,7 +387,7 @@ pub fn present_async(call: *ashet.overlapped.AsyncCall, inputs: ashet.abi.video. @panic("TODO: present_async!"); } -pub fn load_splash_screen(vmem: VideoMemory) void { +pub fn load_splash_screen(vmem: ashet.abi.video.VideoMemory) void { const splash = defaults.splash_screen; const clamp_w = @min(vmem.width, splash.width); const clamp_h = @min(vmem.height, splash.height); @@ -298,7 +409,7 @@ pub fn load_splash_screen(vmem: VideoMemory) void { pub const defaults = struct { /// The splash screen that should be shown until the operating system /// has fully bootet. This has to be displayed in 256x128 8bpp video mode. - pub const splash_screen: VideoMemory = .{ + pub const splash_screen: ashet.abi.video.VideoMemory = .{ .width = 256, .height = 128, .stride = 256, @@ -347,8 +458,8 @@ pub const utils = struct { std.debug.assert(options.dst_pos.x +| options.src_buffer.width <= options.dst_buffer.width); std.debug.assert(options.dst_pos.y +| options.src_buffer.height <= options.dst_buffer.height); - var dst_iter = options.dst_buffer.data + options.dst_pos.y * options.dst_buffer.stride + options.dst_pos.x; - var src_iter = options.src_buffer.data; + var dst_iter: [*]DstPixel = options.dst_buffer.data + options.dst_pos.y * options.dst_buffer.stride + options.dst_pos.x; + var src_iter: [*]const Color = options.src_buffer.data; for (0..options.src_buffer.height) |_| { const dst_row = dst_iter; diff --git a/src/kernel/drivers/video/Memory_Mapped_Framebuffer.zig b/src/kernel/drivers/video/Memory_Mapped_Framebuffer.zig index db0909d8..56d0cd0b 100644 --- a/src/kernel/drivers/video/Memory_Mapped_Framebuffer.zig +++ b/src/kernel/drivers/video/Memory_Mapped_Framebuffer.zig @@ -25,7 +25,7 @@ byte_per_pixel: u32, backing_buffer: []align(ashet.memory.page_size) Color, border_color: Color = ashet.video.defaults.border_color, -pub fn create(allocator: std.mem.Allocator, driver_name: []const u8, config: Config) !Memory_Mapped_Framebuffer { +pub fn create(allocator: std.mem.Allocator, comptime driver_name: []const u8, config: Config) !Memory_Mapped_Framebuffer { const framebuffer = try config.instantiate(); const width = std.math.cast(u16, framebuffer.width) orelse return error.FramebufferSize; @@ -49,6 +49,8 @@ pub fn create(allocator: std.mem.Allocator, driver_name: []const u8, config: Con .video = .{ .begin_write_pixels_fn = begin_write_pixels, .get_properties_fn = get_properties, + .create_mapped_buffer_fn = ashet.video.VideoDevice.default_create_mapped_buffer_front, + .get_mapped_buffer_fn = get_mapped_buffer, }, }, }, @@ -77,6 +79,17 @@ pub fn create(allocator: std.mem.Allocator, driver_name: []const u8, config: Con return driver; } +fn get_mapped_buffer(driver: *Driver, buffer: ashet.video.BufferKind) error{IoError}!ashet.video.VideoMemory { + const vd: *Memory_Mapped_Framebuffer = @fieldParentPtr("driver", driver); + return switch (buffer) { + .front => .{ + .base = vd.backing_buffer.ptr, + .stride = vd.width, + }, + .back => @panic("kernel bug: driver layer invoked get_mapped_buffer for unsupported buffer"), + }; +} + fn get_properties(driver: *Driver) ashet.video.DeviceProperties { const vd: *Memory_Mapped_Framebuffer = @fieldParentPtr("driver", driver); return .{ @@ -84,6 +97,8 @@ fn get_properties(driver: *Driver) ashet.video.DeviceProperties { .width = vd.width, .height = vd.height, }, + + .buffer_support = .front_stable, }; } diff --git a/src/kernel/drivers/video/VGA.zig b/src/kernel/drivers/video/VGA.zig index 72ed9828..45690381 100644 --- a/src/kernel/drivers/video/VGA.zig +++ b/src/kernel/drivers/video/VGA.zig @@ -21,6 +21,8 @@ driver: Driver = .{ .video = .{ .get_properties_fn = get_properties, .begin_write_pixels_fn = begin_write_pixels, + .create_mapped_buffer_fn = ashet.video.VideoDevice.default_create_mapped_buffer_front, + .get_mapped_buffer_fn = get_mapped_buffer, }, }, }, @@ -64,6 +66,18 @@ fn get_properties(driver: *Driver) ashet.video.DeviceProperties { .width = width, .height = height, }, + .buffer_support = .front_stable, + }; +} + +fn get_mapped_buffer(driver: *Driver, buffer: ashet.video.BufferKind) error{IoError}!ashet.video.VideoMemory { + _ = driver; + return switch (buffer) { + .front => .{ + .base = @ptrFromInt(0xA0000), + .stride = width, + }, + .back => @panic("kernel bug: driver layer invoked get_mapped_buffer for unsupported buffer"), }; } diff --git a/src/kernel/drivers/video/Virtio_GPU_Device.zig b/src/kernel/drivers/video/Virtio_GPU_Device.zig index 1fe37aec..c63029bf 100644 --- a/src/kernel/drivers/video/Virtio_GPU_Device.zig +++ b/src/kernel/drivers/video/Virtio_GPU_Device.zig @@ -52,6 +52,7 @@ pub fn init(allocator: std.mem.Allocator, index: usize, regs: *volatile virtio.C .stride = vd.graphics_width, }); + logger.info("write initial flush", .{}); vd.flush(); return vd; @@ -236,6 +237,7 @@ const GPU = struct { return; } + logger.debug("initialize gpu.vq", .{}); try gpu.vq.init(0, regs); // try cursor_vq.init(1, regs); @@ -249,6 +251,7 @@ const GPU = struct { // // Those descriptors are not full, so reset avail_i // cursor_vq.avail_i = 0; + logger.debug("get display info...", .{}); const di = (try gpu.getDisplayInfo()) orelse { logger.err("failed to query gpu display info!", .{}); return; @@ -267,6 +270,7 @@ const GPU = struct { logger.info("detected framebuffer size: {}x{}", .{ width, height }); + logger.debug("setup framebuffer...", .{}); gpu.fb_mem = gpu.setupFramebuffer(allocator, Scanout.first, ResourceId.framebuffer, width, height) catch |err| { logger.err("failed to setup framebuffer: {s}", .{@errorName(err)}); return; diff --git a/src/kernel/port/platform/rv32.zig b/src/kernel/port/platform/rv32.zig index 8e3a54b7..25027902 100644 --- a/src/kernel/port/platform/rv32.zig +++ b/src/kernel/port/platform/rv32.zig @@ -22,6 +22,8 @@ pub const scheduler = struct { pub const start = struct { fn handleTrap() align(4) callconv(.c) noreturn { + ashet.machine_config.debug_write("RISC-V TRAP\r\n"); + const trap_reason = csr.ControlStatusRegister.read(.mcause); const trap_location = csr.ControlStatusRegister.read(.mepc); const trap_status = csr.ControlStatusRegister.read(.mstatus); From 1476ca9483bcc139bef74a77b964e6af88382c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Fri, 11 Sep 2026 16:27:08 +0200 Subject: [PATCH 12/17] Further continues integration of the new video api: Implements BufferMapping and creates an implicit buffer mapping for Framebuffers --- src/abi/src/ashet.abi | 21 +- src/kernel/components/graphics.zig | 140 +++++++--- src/kernel/components/syscalls.zig | 30 +-- src/kernel/components/video.zig | 254 +++++++++++++----- .../video/Memory_Mapped_Framebuffer.zig | 15 +- src/kernel/drivers/video/VGA.zig | 13 +- .../apps/demos/revision2026/revision2026.zig | 11 +- .../libAshetOS/module/src/libashet/video.zig | 23 +- 8 files changed, 349 insertions(+), 158 deletions(-) diff --git a/src/abi/src/ashet.abi b/src/abi/src/ashet.abi index cffaed06..fbe423c0 100644 --- a/src/abi/src/ashet.abi +++ b/src/abi/src/ashet.abi @@ -696,7 +696,7 @@ namespace video { /// each other and contains `width` valid elements. /// /// There are `height` total scanlines available. - field base: [*]align(4) Color; + field base: [*]Color; /// Length of a scanline in elements. field stride: usize; @@ -1454,14 +1454,16 @@ namespace draw { error Unsupported; } - /// Marks a portion of the framebuffer as changed and forces the OS to - /// perform an update action if necessary. - syscall invalidate_framebuffer { - in fb: Framebuffer; - in area: Rectangle; + //? TODO(gpu_refactor): Make this syscall only be usable for memory framebuffers. + //? + //? /// Marks a portion of the framebuffer as changed and forces the OS to + //? /// perform an update action if necessary. + //? syscall invalidate_framebuffer { + //? in fb: Framebuffer; + //? in area: Rectangle; - error InvalidHandle; - } + //? error InvalidHandle; + //? } /// Renders the provided Ashet Graphics Protocol @`sequence` into @`target` framebuffer. /// @@ -1471,14 +1473,17 @@ namespace draw { async_call Render { /// The framebuffer which should be drawn to. in target: Framebuffer; + /// The AGP code that defines the drawing. in sequence: bytestr; + /// If the target framebuffer is invalidatable, it is automatically invalidated after the completion /// of the command sequence, ensuring presentation of the contents. /// /// This is useful when painting into widgets or windows to ensure the window manager /// actually sees the changes as soon as they are done, reducing graphics pipeline latency. in auto_invalidate: bool; + error BadCode; error InvalidHandle; error SystemResources; diff --git a/src/kernel/components/graphics.zig b/src/kernel/components/graphics.zig index f1e07886..459d427a 100644 --- a/src/kernel/components/graphics.zig +++ b/src/kernel/components/graphics.zig @@ -195,7 +195,10 @@ pub const Framebuffer = struct { pub const Type = union(ashet.abi.FramebufferType) { memory: Bitmap, - video: noreturn, // TODO(gpu_support): video: VideoOut, + video: struct { + mapping: *ashet.video.BufferMapping, + link: ashet.video.BufferMapping.FramebufferLink = .{ .data = {} }, + }, window: *ashet.gui.Window, widget: *ashet.gui.Widget, }; @@ -227,23 +230,27 @@ pub const Framebuffer = struct { } pub fn create_video_output(output: *ashet.video.Output) error{SystemResources}!*Framebuffer { - _ = output; - // TODO(gpu_support): - @panic("TODO: graphics.create_video_output!"); - - // const fb = ashet.memory.type_pool(Framebuffer).alloc() catch return error.SystemResources; - // errdefer ashet.memory.type_pool(Framebuffer).free(fb); - - // fb.* = .{ - // .type = .{ - // .video = .{ - // .output = output, - // .memory = output.get_video_memory(), - // }, - // }, - // }; - - // return fb; + const mapping = output.get_or_create_buffer_mapping(.back_buffer, .shared) catch |err| { + return switch (err) { + error.SystemResources => |e| return e, + error.Unsupported, error.AlreadyExists => @panic("kernel bug: soft backbuffer sharing must always be supported"), + }; + }; + errdefer @panic("specific failure mode not supported yet"); // TODO(gpu_support): The mapping has to be destroyed iff no framebuffer shares exist and no exclusive user exists. + + const fb = ashet.memory.type_pool(Framebuffer).alloc() catch return error.SystemResources; + errdefer ashet.memory.type_pool(Framebuffer).free(fb); + + fb.* = .{ + .type = .{ + .video = .{ + .mapping = mapping, + }, + }, + }; + mapping.add_framebuffer_link(&fb.type.video.link); + + return fb; } pub fn create_window(window: *ashet.gui.Window) error{SystemResources}!*Framebuffer { @@ -276,26 +283,19 @@ pub const Framebuffer = struct { const back_buffer = bmp.pixels[0 .. @as(usize, bmp.width) * bmp.stride]; ashet.memory.allocator.free(back_buffer); }, - .video => unreachable, // TODO(gpu_support) + .video => |*video| { + video.mapping.remove_framebuffer_link(&video.link); + }, .window => {}, .widget => {}, } ashet.memory.type_pool(Framebuffer).free(fb); } - fn invalidate(fb: *Framebuffer) void { - switch (fb.type) { - .memory => {}, // no-op, nothing to invalidate - .video => unreachable, // TODO(gpu_support) - .window => |win| win.invalidate_full(), - .widget => |widget| widget.window.invalidate_region(widget.bounds), - } - } - pub fn get_size(fb: Framebuffer) Size { return switch (fb.type) { .memory => |mem| .new(mem.width, mem.height), - .video => unreachable, // TODO(gpu_support) + .video => |video| video.mapping.get_resolution(), .window => |win| win.size, .widget => |widget| widget.bounds.size(), }; @@ -317,16 +317,15 @@ pub const Framebuffer = struct { .height = mem.height, .stride = mem.stride, }, - .video => unreachable, // TODO(gpu_support) - // TODO(gpu_support): .video => |video| blk: { - // const mem = video.output.get_video_memory(); - // break :blk .{ - // .pixels = mem.base, - // .height = mem.height, - // .width = mem.width, - // .stride = mem.stride, - // }; - // }, + .video => |video| blk: { + const mem = video.mapping.get_video_memory(); + break :blk .{ + .pixels = mem.base, + .height = mem.height, + .width = mem.width, + .stride = mem.stride, + }; + }, .window => |win| .{ .pixels = win.pixels.ptr, .width = win.size.width, @@ -732,7 +731,68 @@ fn render_sync(call: *ashet.overlapped.AsyncCall, inputs: ashet.abi.draw.Render. } if (inputs.auto_invalidate) { - fb.invalidate(); + + // switch (fb.type) { + // .video => @panic("TODO: Updating a kernel buffer mapping isn't supported yet!"), // TODO(gpu_support): We need to refactor this to include the invalidation of framebuffers only inside the "Render()" routine, so it's asynchronous + + // .memory => {}, // always ok + + // .widget => |widget| widget.window.invalidate_region(.{ + // .x = widget.bounds.x +| region.x, + // .y = widget.bounds.y +| region.y, + // .width = region.width, + // .height = region.height, + // }), + // .window => |window| window.invalidate_region(region), + // } + + switch (fb.type) { + .memory => {}, // no-op, nothing to invalidate + .video => |vmem| { + const mapping = vmem.mapping; + const output = vmem.mapping.output; + + const size = mapping.get_resolution(); + + const output_handle = ashet.resources.get_handle( + call.resource_owner, + &output.system_resource, + ) orelse @panic("TODO: Framebuffer invocator does not also own video output."); // TODO(gpu_support): This one happens when we invalidate a framebuffer which we inherited from another process. + + var write_pixels_call = ashet.abi.video.WritePixels.new(.{ + .output = output_handle.unsafe_cast(.video_video_output), // catch @panic("kernel bug: mistake in resource resolution"), + .stride = mapping.video_memory.stride, + .destination = .new(.zero, size), + .mode = .immediate, + .pixels_ptr = mapping.video_memory.base, + .pixels_len = mapping.video_memory.stride * (size.height -| 1) + size.width, + }); + + ashet.overlapped.schedule( + call.resource_owner, + call.context, + &write_pixels_call.arc, + ) catch |err| return switch (err) { + error.AlreadyScheduled => unreachable, + error.SystemResources => |e| e, + }; + + var completed: [1]?*ashet.abi.overlapped.ARC = .{&write_pixels_call.arc}; + const count = ashet.overlapped.await_completion_of( + call.context, + &completed, + ) catch |err| return switch (err) { + error.Unscheduled => unreachable, + error.InvalidOperation => unreachable, + }; + std.debug.assert(count == 1); + std.debug.assert(completed[0] == &write_pixels_call.arc); + + // @panic("video buffer invalidation not supported"), + }, + .window => |win| win.invalidate_full(), + .widget => |widget| widget.window.invalidate_region(widget.bounds), + } } return .{}; diff --git a/src/kernel/components/syscalls.zig b/src/kernel/components/syscalls.zig index 20698722..62178e3c 100644 --- a/src/kernel/components/syscalls.zig +++ b/src/kernel/components/syscalls.zig @@ -392,10 +392,15 @@ pub const syscalls = struct { return output.get_resolution(); } - pub fn create_buffer_mapping(output: abi.video.VideoOutput, requested_kind: abi.video.BufferKind) error{ InvalidHandle, Unsupported, AlreadyExists, SystemResources }!abi.video.BufferMapping { - _ = output; - _ = requested_kind; - not_implemented_yet(@src()); // TODO(gpu_support) + pub fn create_buffer_mapping(output_handle: abi.video.VideoOutput, requested_kind: abi.video.BufferKind) error{ InvalidHandle, Unsupported, AlreadyExists, SystemResources }!abi.video.BufferMapping { + const proc, const output = try resolve_typed_resource(ashet.video.Output, output_handle.as_resource()); + + const mapping = try output.get_or_create_buffer_mapping(requested_kind, .exclusive); + errdefer mapping.destroy(); + + const handle = try ashet.resources.add_to_process(proc, &mapping.system_resource); + + return handle.unsafe_cast(.video_buffer_mapping); } pub fn get_video_memory(buffer_handle: abi.video.BufferMapping) error{InvalidHandle}!abi.video.VideoMemory { @@ -545,7 +550,6 @@ pub const syscalls = struct { .stride = mem.stride, .base = mem.pixels, }, - .video => |vdev| vdev.memory, // TODO: Temporary hack until a true "create_buffer_mapping" syscall is available else => error.Unsupported, }; } @@ -555,19 +559,9 @@ pub const syscalls = struct { pub fn invalidate_framebuffer(framebuffer: abi.Framebuffer, region: abi.Rectangle) error{InvalidHandle}!void { _, const fb = try resolve_typed_resource(ashet.graphics.Framebuffer, framebuffer.as_resource()); - switch (fb.type) { - .video => |vdev| vdev.output.flush(), // TODO: Decide if asynchronous or synchronous flush - - .memory => {}, // always ok - - .widget => |widget| widget.window.invalidate_region(.{ - .x = widget.bounds.x +| region.x, - .y = widget.bounds.y +| region.y, - .width = region.width, - .height = region.height, - }), - .window => |window| window.invalidate_region(region), - } + _ = fb; + _ = region; + not_implemented_yet(@src()); // TODO(gpu_support) } // Drawing: diff --git a/src/kernel/components/video.zig b/src/kernel/components/video.zig index 0faa3886..ad016c54 100644 --- a/src/kernel/components/video.zig +++ b/src/kernel/components/video.zig @@ -1,11 +1,13 @@ const std = @import("std"); const builtin = @import("builtin"); const ashet = @import("../main.zig"); +const astd = @import("ashet-std"); const logger = std.log.scoped(.video); pub const Color = ashet.abi.Color; pub const OutputID = ashet.abi.video.VideoOutputID; pub const Resolution = ashet.abi.Size; +pub const BufferKind = ashet.abi.video.BufferKind; const Rectangle = ashet.abi.Rectangle; const PresentMode = ashet.abi.video.PresentMode; @@ -24,6 +26,13 @@ pub const DeviceProperties = struct { /// The device only supports a front buffer, which keeps /// its address between swaps. front_stable, + + pub fn supports_buffer(support: MappableBufferSupport, kind: BufferKind) bool { + return switch (support) { + .none => false, + .front_stable => (kind == .front_buffer), + }; + } }; }; @@ -50,20 +59,24 @@ pub const VideoMemory = struct { } }; -/// Determines the type of video memory buffer. -pub const BufferKind = enum { - /// The front buffer is the memory area the - /// scanout unit reads and displays. - /// - /// Changes to this buffer are immediately - /// reflected - front, - - /// - back, -}; - pub const VideoDevice = struct { + pub const MappingFunctions = struct { + create_mapped_buffer_fn: *const fn ( + driver: *ashet.drivers.Driver, + buffer: BufferKind, + ) error{ SystemResources, Unsupported }!void, + + get_mapped_buffer_fn: *const fn ( + driver: *ashet.drivers.Driver, + buffer: BufferKind, + ) VideoMemory, + + destroy_mapped_buffer_fn: *const fn ( + driver: *ashet.drivers.Driver, + buffer: BufferKind, + ) void, + }; + get_properties_fn: *const fn (*ashet.drivers.Driver) DeviceProperties, get_one_vblank_event_fn: ?*const fn (*ashet.drivers.Driver) bool = null, // TODO(gpu_support): Go through all drivers and see which actually support this @@ -76,25 +89,17 @@ pub const VideoDevice = struct { mode: PresentMode, ) void, - create_mapped_buffer_fn: ?*const fn ( - driver: *ashet.drivers.Driver, - buffer: BufferKind, - ) error{ SystemResources, IoError, Unsupported }!void = unsupported_create_mapped_buffer, + mapping_fns: ?MappingFunctions = null, - get_mapped_buffer_fn: ?*const fn ( - driver: *ashet.drivers.Driver, - buffer: BufferKind, - ) error{IoError}!VideoMemory = unsupported_get_mapped_buffer, - - fn get_properties(vd: *VideoDevice) DeviceProperties { // pub + fn get_properties(vd: *VideoDevice) DeviceProperties { return vd.get_properties_fn(ashet.drivers.resolveDriver(.video, vd)); } - fn supports_vblank_event(vd: *VideoDevice) bool { // pub + fn supports_vblank_event(vd: *VideoDevice) bool { return vd.get_one_vblank_event_fn != null; } - fn get_one_vblank_event(vd: *VideoDevice) bool { // pub + fn get_one_vblank_event(vd: *VideoDevice) bool { if (vd.get_one_vblank_event_fn) |get_one_vblank_event_fn| { return get_one_vblank_event_fn(ashet.drivers.resolveDriver(.video, vd)); } else { @@ -102,21 +107,31 @@ pub const VideoDevice = struct { } } - fn create_mapped_buffer( // pub + fn create_mapped_buffer( vd: *VideoDevice, buffer: BufferKind, - ) error{ SystemResources, IoError, Unsupported }!void { - return vd.create_mapped_buffer_fn(ashet.drivers.resolveDriver(.video, vd), buffer); + ) error{ SystemResources, Unsupported }!void { + const fns = vd.mapping_fns orelse @panic("kernel bug: should never be called when unsupported."); + return fns.create_mapped_buffer_fn(ashet.drivers.resolveDriver(.video, vd), buffer); + } + + fn get_mapped_buffer( + vd: *VideoDevice, + buffer: BufferKind, + ) VideoMemory { + const fns = vd.mapping_fns orelse @panic("kernel bug: should never be called when unsupported."); + return fns.get_mapped_buffer_fn(ashet.drivers.resolveDriver(.video, vd), buffer); } - fn get_mapped_buffer( // pub + fn destroy_mapped_buffer( vd: *VideoDevice, buffer: BufferKind, - ) error{IoError}!VideoMemory { - return vd.create_mapped_buffer_fn(ashet.drivers.resolveDriver(.video, vd), buffer); + ) void { + const fns = vd.mapping_fns orelse @panic("kernel bug: should never be called when unsupported."); + return fns.destroy_mapped_buffer_fn(ashet.drivers.resolveDriver(.video, vd), buffer); } - fn begin_write_pixels( // pub + fn begin_write_pixels( vd: *VideoDevice, call: *ashet.overlapped.AsyncCall, rectangle: Rectangle, @@ -137,11 +152,11 @@ pub const VideoDevice = struct { pub fn default_create_mapped_buffer_front( driver: *ashet.drivers.Driver, buffer: BufferKind, - ) error{ SystemResources, IoError, Unsupported }!void { + ) error{ SystemResources, Unsupported }!void { _ = driver; switch (buffer) { - .front => {}, - .back => return error.Unsupported, + .front_buffer => {}, + .back_buffer => return error.Unsupported, } } @@ -151,8 +166,8 @@ pub const VideoDevice = struct { ) error{ SystemResources, IoError, Unsupported }!void { _ = driver; switch (buffer) { - .front => return error.Unsupported, - .back => {}, + .front_buffer => return error.Unsupported, + .back_buffer => {}, } } @@ -164,22 +179,12 @@ pub const VideoDevice = struct { _ = buffer; } - fn unsupported_create_mapped_buffer( - driver: *ashet.drivers.Driver, - buffer: BufferKind, - ) error{ SystemResources, IoError, Unsupported }!void { - _ = driver; - _ = buffer; - return error.Unsupported; - } - - fn unsupported_get_mapped_buffer( + pub fn destroy_mapped_buffer_noop( driver: *ashet.drivers.Driver, buffer: BufferKind, - ) error{IoError}!VideoMemory { + ) void { _ = driver; _ = buffer; - @panic("kernel bug: get_mapped_buffer_fn was not correctly set by the "); } }; @@ -195,7 +200,9 @@ pub const Output = struct { /// If true, the kernel will automatically flush the screen in a background process. auto_flush: bool = true, // TODO: Fix this flush_required: bool = false, + video_driver: *ashet.drivers.VideoDevice, + properties: DeviceProperties, vsync_awaiters: ashet.overlapped.WorkQueue = .{ .wakeup_thread = null, @@ -204,7 +211,7 @@ pub const Output = struct { fn _noop(_: *Output) void {} pub fn get_resolution(output: *const Output) Resolution { - return output.video_driver.get_properties().resolution; + return output.properties.resolution; } pub fn begin_write_pixels(output: *const Output, call: *ashet.overlapped.AsyncCall, destination: Rectangle, pixels: []const Color, stride: usize, mode: PresentMode) error{ @@ -252,6 +259,74 @@ pub const Output = struct { ); } + pub const MappingSharing = enum { shared, exclusive }; + + pub fn get_or_create_buffer_mapping(output: *Output, buffer_kind: BufferKind, sharing: MappingSharing) error{ SystemResources, Unsupported, AlreadyExists }!*BufferMapping { + if (output.buffer_mappings.get(buffer_kind)) |mapping| { + switch (sharing) { + .shared => {}, + .exclusive => if (mapping.has_exclusive_user) { + return error.AlreadyExists; + } else { + mapping.has_exclusive_user = true; + }, + } + return mapping; + } + + const has_hw_support = output.properties.buffer_support.supports_buffer(buffer_kind); + const use_hw_buffer = blk: switch (buffer_kind) { + // Front buffers always require hardware support, otherwise our changes + // might not be directly visible. + .front_buffer => { + if (has_hw_support) { + return error.Unsupported; + } + break :blk false; + }, + + // We can always create a backbuffer through software emulation if we + // don't have hardware support. + .back_buffer => has_hw_support, + }; + + // Create buffer mapping: + const maybe_sw_buffer: ?[]Color = if (use_hw_buffer) blk: { + try output.video_driver.create_mapped_buffer(buffer_kind); + break :blk null; + } else blk: { + const total_size = @as(usize, output.properties.resolution.width) * output.properties.resolution.height; + break :blk ashet.memory.page_allocator.alloc(Color, total_size) catch return error.SystemResources; + }; + errdefer if (maybe_sw_buffer) |buffer| { + ashet.memory.page_allocator.free(buffer); + }; + + const mapping = ashet.memory.type_pool(BufferMapping).alloc() catch return error.SystemResources; + errdefer ashet.memory.type_pool(BufferMapping).free(mapping); + + mapping.* = .{ + .output = output, + + .kind = buffer_kind, + .is_soft_buffer = !use_hw_buffer, + .video_memory = if (use_hw_buffer) + output.video_driver.get_mapped_buffer(buffer_kind) + else + .{ + .base = maybe_sw_buffer.?.ptr, + .stride = output.properties.resolution.width, + }, + + .has_exclusive_user = switch (sharing) { + .exclusive => true, + .shared => false, + }, + }; + + return mapping; + } + /// Notifies all overlapped events that wait for V-Blank on this output. pub fn notify_vblank_awaiters(output: *Output) void { while (output.vsync_awaiters.dequeue()) |tup| { @@ -262,30 +337,78 @@ pub const Output = struct { }; pub const BufferMapping = struct { - pub const Destructor = ashet.resources.Destructor(@This(), _noop); + const FramebufferList = astd.DoublyLinkedList(void, .{ + .tag = struct {}, + .address_pinning = true, // BufferMapping has a stable address + }); - system_resource: ashet.resources.SystemResource = .{ .type = .video_video_output }, + pub const FramebufferLink = FramebufferList.Node; + + pub const Destructor = ashet.resources.Destructor(@This(), _destroy); + + system_resource: ashet.resources.SystemResource = .{ .type = .video_buffer_mapping }, output: *Output, + kind: BufferKind, + + /// If true, the buffer was created through a "create_buffer_mapping" call, + /// and is currently held by userland accessible system resource. + has_exclusive_user: bool, + + linked_framebuffers: FramebufferList = .empty, + + /// If true, the buffer is a software-emulated buffer instead of a hardware buffer. + is_soft_buffer: bool, + + video_memory: VideoMemory, - fn _noop(_: *BufferMapping) void {} + pub const destroy = Destructor.destroy; + + fn _destroy(mapping: *BufferMapping) void { + if (mapping.linked_framebuffers.len > 0) { + @panic("BufferMapping.destroy: missing framebuffer invalidation"); // TODO(gpu_support): Refactor into a list of framebuffers and invalidate the framebuffer resources as well + } + + if (mapping.is_soft_buffer) { + const total_size = @as(usize, mapping.output.properties.resolution.width) * mapping.output.properties.resolution.height; + std.debug.assert(mapping.video_memory.stride == mapping.output.properties.resolution.width); + + const buffer = mapping.video_memory.base[0..total_size]; + ashet.memory.page_allocator.free(buffer); + + @panic("not implemented yet"); + } else { + mapping.output.video_driver.destroy_mapped_buffer(mapping.kind); + } + + // Reset the internally stored pointer + mapping.output.buffer_mappings.set(mapping.kind, null); + + ashet.memory.type_pool(BufferMapping).free(mapping); + } /// The raw exposed video memory. Writing to this will change the content /// on the screen. /// Memory is interpreted with the current video mode to produce an image. pub fn get_video_memory(mapping: *const BufferMapping) ashet.abi.video.VideoMemory { - _ = mapping; - @panic("TODO: Implement get_video_memory"); // TODO(gpu_support): Implement get_video_memory - // const props = mapping.output.video_driver.get_properties(); - - // std.debug.assert(props.video_memory.len >= (props.stride * @as(usize, props.resolution.height))); - - // return .{ - // .base = props.video_memory.ptr, - // .stride = props.stride, - // .width = props.resolution.width, - // .height = props.resolution.height, - // }; + return .{ + .base = mapping.video_memory.base, + .stride = mapping.video_memory.stride, + .width = mapping.output.properties.resolution.width, + .height = mapping.output.properties.resolution.height, + }; + } + + pub fn add_framebuffer_link(mapping: *BufferMapping, link: *FramebufferLink) void { + mapping.linked_framebuffers.append(link); + } + + pub fn remove_framebuffer_link(mapping: *BufferMapping, link: *FramebufferLink) void { + mapping.linked_framebuffers.append(link); + } + + pub fn get_resolution(mapping: *const BufferMapping) Resolution { + return mapping.output.properties.resolution; } }; @@ -308,6 +431,7 @@ pub fn initialize() !void { while (drivers.next()) |driver| : (index += 1) { video_outputs[index] = Output{ .video_driver = driver, + .properties = driver.get_properties(), }; const output = &video_outputs[index]; diff --git a/src/kernel/drivers/video/Memory_Mapped_Framebuffer.zig b/src/kernel/drivers/video/Memory_Mapped_Framebuffer.zig index 56d0cd0b..c16f3acd 100644 --- a/src/kernel/drivers/video/Memory_Mapped_Framebuffer.zig +++ b/src/kernel/drivers/video/Memory_Mapped_Framebuffer.zig @@ -49,8 +49,6 @@ pub fn create(allocator: std.mem.Allocator, comptime driver_name: []const u8, co .video = .{ .begin_write_pixels_fn = begin_write_pixels, .get_properties_fn = get_properties, - .create_mapped_buffer_fn = ashet.video.VideoDevice.default_create_mapped_buffer_front, - .get_mapped_buffer_fn = get_mapped_buffer, }, }, }, @@ -79,17 +77,6 @@ pub fn create(allocator: std.mem.Allocator, comptime driver_name: []const u8, co return driver; } -fn get_mapped_buffer(driver: *Driver, buffer: ashet.video.BufferKind) error{IoError}!ashet.video.VideoMemory { - const vd: *Memory_Mapped_Framebuffer = @fieldParentPtr("driver", driver); - return switch (buffer) { - .front => .{ - .base = vd.backing_buffer.ptr, - .stride = vd.width, - }, - .back => @panic("kernel bug: driver layer invoked get_mapped_buffer for unsupported buffer"), - }; -} - fn get_properties(driver: *Driver) ashet.video.DeviceProperties { const vd: *Memory_Mapped_Framebuffer = @fieldParentPtr("driver", driver); return .{ @@ -98,7 +85,7 @@ fn get_properties(driver: *Driver) ashet.video.DeviceProperties { .height = vd.height, }, - .buffer_support = .front_stable, + .buffer_support = .none, }; } diff --git a/src/kernel/drivers/video/VGA.zig b/src/kernel/drivers/video/VGA.zig index 45690381..6cea9d89 100644 --- a/src/kernel/drivers/video/VGA.zig +++ b/src/kernel/drivers/video/VGA.zig @@ -21,8 +21,11 @@ driver: Driver = .{ .video = .{ .get_properties_fn = get_properties, .begin_write_pixels_fn = begin_write_pixels, - .create_mapped_buffer_fn = ashet.video.VideoDevice.default_create_mapped_buffer_front, - .get_mapped_buffer_fn = get_mapped_buffer, + .mapping_fns = .{ + .create_mapped_buffer_fn = ashet.video.VideoDevice.default_create_mapped_buffer_front, + .get_mapped_buffer_fn = get_mapped_buffer, + .destroy_mapped_buffer_fn = ashet.video.VideoDevice.destroy_mapped_buffer_noop, + }, }, }, }, @@ -70,14 +73,14 @@ fn get_properties(driver: *Driver) ashet.video.DeviceProperties { }; } -fn get_mapped_buffer(driver: *Driver, buffer: ashet.video.BufferKind) error{IoError}!ashet.video.VideoMemory { +fn get_mapped_buffer(driver: *Driver, buffer: ashet.video.BufferKind) ashet.video.VideoMemory { _ = driver; return switch (buffer) { - .front => .{ + .front_buffer => .{ .base = @ptrFromInt(0xA0000), .stride = width, }, - .back => @panic("kernel bug: driver layer invoked get_mapped_buffer for unsupported buffer"), + .back_buffer => @panic("kernel bug: driver layer invoked get_mapped_buffer for unsupported buffer"), }; } diff --git a/src/userland/apps/demos/revision2026/revision2026.zig b/src/userland/apps/demos/revision2026/revision2026.zig index 9fc0dd23..5da8b18b 100644 --- a/src/userland/apps/demos/revision2026/revision2026.zig +++ b/src/userland/apps/demos/revision2026/revision2026.zig @@ -101,10 +101,10 @@ pub fn main() !void { const video_output = try ashet.video.acquire(.primary); defer video_output.release(); - const video_fb = try ashet.graphics.create_video_framebuffer(video_output); - defer video_fb.release(); + const mapping = try video_output.create_mapping(.back_buffer); + defer mapping.release(); - const vmem = try ashet.graphics.get_framebuffer_memory(video_fb); + const vmem = try mapping.get_video_memory(); var loop: u32 = 0; var time: f32 = 0.0; @@ -173,10 +173,11 @@ pub fn main() !void { } } - try ashet.abi.draw.invalidate_framebuffer(video_fb, .everything); - ashet.process.thread.yield(); + try mapping.present(.vblank); + + // TODO(gpu_support): Remove this call once properly implemented: _ = try ashet.overlapped.performOne(ashet.video.WaitForVBlank, .{ .output = @ptrCast(video_output), }); diff --git a/src/userland/libs/libAshetOS/module/src/libashet/video.zig b/src/userland/libs/libAshetOS/module/src/libashet/video.zig index 1d6f0d88..38b95826 100644 --- a/src/userland/libs/libAshetOS/module/src/libashet/video.zig +++ b/src/userland/libs/libAshetOS/module/src/libashet/video.zig @@ -20,9 +20,26 @@ pub const Output = opaque { return try abi.video.get_resolution(@ptrCast(out)); } - // pub fn get_video_memory(out: *Output) !abi.VideoMemory { - // return try abi.video.get_video_memory(@ptrCast(out)); - // } + pub fn create_mapping(out: *Output, kind: BufferKind) !*BufferMapping { + return @ptrCast(try abi.video.create_buffer_mapping(@ptrCast(out), kind)); + } +}; + +pub const BufferMapping = opaque { + pub fn release(mapping: *BufferMapping) void { + abi.resources.release(.from_ptr(mapping)); + } + + pub fn get_video_memory(mapping: *BufferMapping) !abi.video.VideoMemory { + return try ashet.abi.video.get_video_memory(@ptrCast(mapping)); + } + + pub fn present(mapping: *BufferMapping, mode: PresentMode) !void { + _ = try ashet.overlapped.performOne(ashet.abi.video.Present, .{ + .buffer = @ptrCast(mapping), + .mode = mode, + }); + } }; pub fn acquire(id: VideoOutputID) !*Output { From 9c38caf83fc1b80531c4697435e6864e5db45af0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Sun, 13 Sep 2026 23:08:48 +0200 Subject: [PATCH 13/17] Implements first version of vsync support for VGA driver. --- .envrc | 1 + src/kernel/components/video.zig | 2 + src/kernel/drivers/video/VGA.zig | 480 ++++++++++++++-------- src/kernel/drivers/video/x86/vga-regs.zig | 258 ++++++++++++ 4 files changed, 565 insertions(+), 176 deletions(-) create mode 100644 .envrc create mode 100644 src/kernel/drivers/video/x86/vga-regs.zig diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..5d2831a2 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +export ZIG_LOCAL_CACHE_DIR="$PWD/.zig-cache" \ No newline at end of file diff --git a/src/kernel/components/video.zig b/src/kernel/components/video.zig index ad016c54..3613e60e 100644 --- a/src/kernel/components/video.zig +++ b/src/kernel/components/video.zig @@ -95,10 +95,12 @@ pub const VideoDevice = struct { return vd.get_properties_fn(ashet.drivers.resolveDriver(.video, vd)); } + /// Returns true if the video device does support waiting for vertical blanking intervals. fn supports_vblank_event(vd: *VideoDevice) bool { return vd.get_one_vblank_event_fn != null; } + /// Returns `true` if a vertical blanking interval has happened since the last call. fn get_one_vblank_event(vd: *VideoDevice) bool { if (vd.get_one_vblank_event_fn) |get_one_vblank_event_fn| { return get_one_vblank_event_fn(ashet.drivers.resolveDriver(.video, vd)); diff --git a/src/kernel/drivers/video/VGA.zig b/src/kernel/drivers/video/VGA.zig index 6cea9d89..3f75fbca 100644 --- a/src/kernel/drivers/video/VGA.zig +++ b/src/kernel/drivers/video/VGA.zig @@ -1,6 +1,7 @@ const std = @import("std"); const ashet = @import("../../main.zig"); const logger = std.log.scoped(.vga); +const vga_regs = @import("x86/vga-regs.zig"); const x86 = ashet.ports.platforms.x86; const VGA = @This(); @@ -21,6 +22,7 @@ driver: Driver = .{ .video = .{ .get_properties_fn = get_properties, .begin_write_pixels_fn = begin_write_pixels, + .get_one_vblank_event_fn = get_one_vblank_event, .mapping_fns = .{ .create_mapped_buffer_fn = ashet.video.VideoDevice.default_create_mapped_buffer_front, .get_mapped_buffer_fn = get_mapped_buffer, @@ -30,6 +32,9 @@ driver: Driver = .{ }, }, +vblank_irq_support: VBlankIrqSupport, +next_expected_retrace: ashet.time.Instant, + const memory_ranges = [_]x86.vmm.Range{ .{ .base = 0xA0000, .length = 0x20000 }, // these are included in the range above: @@ -43,11 +48,13 @@ pub fn init(vga: *VGA) !void { x86.vmm.update(range, .read_write); } - vga.* = VGA{}; + writeVgaRegisters(g_320x200x256); + + loadFixedPalette(); - writeVgaRegisters(modes.g_320x200x256); + setupVBlankIrq(); - vga.loadFixedPalette(); + const vblank_irq_support = test_blank_irq(); const vmem = @as([*]align(ashet.memory.page_size) Color, @ptrFromInt(0xA0000))[0 .. width * height]; @@ -59,6 +66,16 @@ pub fn init(vga: *VGA) !void { .height = height, .stride = width, }); + + const next_expected_retrace: ashet.time.Instant = switch (vblank_irq_support) { + .supported => undefined, + .unsupported => ashet.time.Instant.now().add_ms(16), + }; + + vga.* = VGA{ + .vblank_irq_support = vblank_irq_support, + .next_expected_retrace = next_expected_retrace, + }; } fn get_properties(driver: *Driver) ashet.video.DeviceProperties { @@ -73,6 +90,24 @@ fn get_properties(driver: *Driver) ashet.video.DeviceProperties { }; } +fn get_one_vblank_event(driver: *Driver) bool { + const vd: *VGA = @alignCast(@fieldParentPtr("driver", driver)); + + return switch (vd.vblank_irq_support) { + .supported => readAndResetIrq(), + + .unsupported => blk: { + var had_vblank_event = false; + const now = ashet.time.Instant.now(); + while (vd.next_expected_retrace.less_or_equal(now)) { + vd.next_expected_retrace = vd.next_expected_retrace.add_ms(16); + had_vblank_event = true; + } + break :blk had_vblank_event; + }, + }; +} + fn get_mapped_buffer(driver: *Driver, buffer: ashet.video.BufferKind) ashet.video.VideoMemory { _ = driver; return switch (buffer) { @@ -97,6 +132,14 @@ fn begin_write_pixels( const target = @as([*]align(ashet.memory.page_size) Color, @ptrFromInt(0xA0000))[0 .. width * height]; + switch (mode) { + .dont_care, .immediate => {}, + + // TODO(gpu_support): This is blocking, which is really *not nice*, but it's a kind of viable + // solution for a first draft. + .vblank => wait_for_vsync(), + } + ashet.video.utils.copy_pixels( Color, .{ @@ -120,80 +163,161 @@ fn begin_write_pixels( null, ); - _ = mode; - return call.finalize(ashet.abi.video.WritePixels, .{}); } -fn writeVgaRegisters(regs: [61]u8) void { - var index: usize = 0; - var i: u8 = 0; +const VBlankIrqSupport = enum { supported, unsupported }; + +/// +/// As QEMU does not actually implement the latching vertical blanking IRQ +/// we need for Ashet OS "await vblank" semantics, we need to emulate this. +/// +/// On a real VGA card we can rely on the blanking interval though. +/// +/// To detect if the IRQ is supported, we manually await a vertical blank +/// +fn test_blank_irq() VBlankIrqSupport { + logger.info("testing VGA IRQ support...", .{}); + wait_for_vsync(); + + _ = readAndResetIrq(); // IRQ is now off - // write MISCELLANEOUS reg - x86.out(u8, VGA_MISC_WRITE, regs[index]); - index += 1; + // Wait for the next frame to happen + wait_for_vsync(); - // write SEQUENCER regs - i = 0; - while (i < VGA_NUM_SEQ_REGS) : (i += 1) { - x86.out(u8, VGA_SEQ_INDEX, i); - x86.out(u8, VGA_SEQ_DATA, regs[index]); - index += 1; + // If an IRQ has latched after a frame, we are now actually safe that we can rely on the vblank information: + if (readAndResetIrq()) { + return .supported; } - // unlock CRTC registers - x86.out(u8, VGA_CRTC_INDEX, 0x03); - x86.out(u8, VGA_CRTC_DATA, x86.in(u8, VGA_CRTC_DATA) | 0x80); - x86.out(u8, VGA_CRTC_INDEX, 0x11); - x86.out(u8, VGA_CRTC_DATA, x86.in(u8, VGA_CRTC_DATA) & ~@as(u8, 0x80)); + return .unsupported; +} + +pub fn setupVBlankIrq() void { + const io_address_select = vga_regs.MiscellaneousOutputRegister.read().io_address_select; + + const crtc_index = io_address_select.crtcIndexPort(); + const crtc_data = io_address_select.crtcDataPort(); + + // Preserve the currently selected CRTC register. + const previous_index = crtc_index.read(); + defer crtc_index.write(previous_index); + + // Vertical Retrace End register. + crtc_index.write(0x11); + + var value = crtc_data.read(); + + // Bit 5 = 0: enable vertical-retrace interrupt generation. + // + // Bit 4 = 0: clear the pending vertical-retrace interrupt. + value &= ~@as(u8, 0x30); + crtc_data.write(value); + + // Bit 4 = 1: permit the next vertical-retrace interrupt to occur. + value |= 0x10; + crtc_data.write(value); +} + +/// Reads the VGA vertical-retrace interrupt latch and, if set, clears and +/// rearms it for the next vertical retrace. +/// +/// Returns whether a vertical-retrace interrupt was pending. +pub fn readAndResetIrq() bool { + const pending = vga_regs.InputStatus0Register.read().crt_interrupt_pending; + + // Don't touch the latch when there is nothing to acknowledge. In + // particular, this avoids clearing an interrupt that arrives immediately + // after the status read. + if (!pending) + return false; + + const io_address_select = vga_regs.MiscellaneousOutputRegister.read().io_address_select; + + const crtc_index = io_address_select.crtcIndexPort(); + const crtc_data = io_address_select.crtcDataPort(); + + // Preserve the currently selected CRTC register. + const previous_index = crtc_index.read(); + defer crtc_index.write(previous_index); + + crtc_index.write(0x11); + + const value = crtc_data.read(); + + // Bit 4 = 0 clears the interrupt latch. + crtc_data.write(value & ~@as(u8, 0x10)); + + // Bit 4 = 1 rearms it for the next vertical retrace. + crtc_data.write(value | 0x10); - // make sure they remain unlocked - // TODO: Reinsert again - // regs[0x03] |= 0x80; - // regs[0x11] &= ~0x80; + return true; +} + +fn writeVgaRegisters(config: VgaRegisterConfig) void { + // Write MISCELLANEOUS register. + config.miscellaneous_output.write(); - // write CRTC regs + const io_address_select = config.miscellaneous_output.io_address_select; + const crtc_index = io_address_select.crtcIndexPort(); + const crtc_data = io_address_select.crtcDataPort(); - i = 0; - while (i < VGA_NUM_CRTC_REGS) : (i += 1) { - x86.out(u8, VGA_CRTC_INDEX, i); - x86.out(u8, VGA_CRTC_DATA, regs[index]); - index += 1; + // Write SEQUENCER registers. + for (config.sequencer, 0..) |value, index| { + vga_regs.VgaPort.sequencer_index.write(@intCast(index)); + vga_regs.VgaPort.sequencer_data.write(value); } - // write GRAPHICS CONTROLLER regs - i = 0; - while (i < VGA_NUM_GC_REGS) : (i += 1) { - x86.out(u8, VGA_GC_INDEX, i); - x86.out(u8, VGA_GC_DATA, regs[index]); - index += 1; + + // Unlock CRTC registers. + crtc_index.write(0x03); + crtc_data.write(crtc_data.read() | 0x80); + + crtc_index.write(0x11); + crtc_data.write(crtc_data.read() & ~@as(u8, 0x80)); + + // Write CRTC registers. + for (config.crtc, 0..) |value, index| { + crtc_index.write(@intCast(index)); + crtc_data.write(value); } - // write ATTRIBUTE CONTROLLER regs - i = 0; - while (i < VGA_NUM_AC_REGS) : (i += 1) { - _ = x86.in(u8, VGA_INSTAT_READ); - x86.out(u8, VGA_AC_INDEX, i); - x86.out(u8, VGA_AC_WRITE, regs[index]); - index += 1; + + // Write GRAPHICS CONTROLLER registers. + for (config.graphics_controller, 0..) |value, index| { + vga_regs.VgaPort.graphics_controller_index.write(@intCast(index)); + vga_regs.VgaPort.graphics_controller_data.write(value); + } + + // Write ATTRIBUTE CONTROLLER registers. + for (config.attribute_controller, 0..) |value, index| { + // Reset Attribute Controller flip-flop to index state. + _ = vga_regs.InputStatus1Register.read(io_address_select); + + vga_regs.VgaPort.attribute_index_data.write(@intCast(index)); + vga_regs.VgaPort.attribute_index_data.write(value); } - // lock 16-color palette and unblank display - _ = x86.in(u8, VGA_INSTAT_READ); - x86.out(u8, VGA_AC_INDEX, 0x20); + + // Lock 16-color palette and unblank display. + _ = vga_regs.InputStatus1Register.read(io_address_select); + vga_regs.VgaPort.attribute_index_data.write(0x20); } -pub fn setPlane(plane: u2) void { - const pmask: u8 = u8(1) << plane; +fn setPlane(plane: u2) void { + const pmask: u8 = @as(u8, 1) << plane; + + // Set read plane. + vga_regs.VgaPort.graphics_controller_index.write(4); + vga_regs.VgaPort.graphics_controller_data.write(plane); - // set read plane - x86.out(u8, VGA_GC_INDEX, 4); - x86.out(u8, VGA_GC_DATA, plane); - // set write plane - x86.out(u8, VGA_SEQ_INDEX, 2); - x86.out(u8, VGA_SEQ_DATA, pmask); + // Set write plane. + vga_regs.VgaPort.sequencer_index.write(2); + vga_regs.VgaPort.sequencer_data.write(pmask); } fn getFramebufferSegment() [*]volatile u8 { - x86.out(u8, VGA_GC_INDEX, 6); - const seg = (x86.in(u8, VGA_GC_DATA) >> 2) & 3; + vga_regs.VgaPort.graphics_controller_index.write(6); + + const seg = (vga_regs.VgaPort.graphics_controller_data.read() >> 2) & 3; + return @as([*]volatile u8, @ptrFromInt(switch (@as(u2, @truncate(seg))) { 0, 1 => @as(u32, 0xA0000), 2 => @as(u32, 0xB0000), @@ -201,9 +325,6 @@ fn getFramebufferSegment() [*]volatile u8 { })); } -const PALETTE_INDEX = 0x03c8; -const PALETTE_DATA = 0x03c9; - const RGB = packed struct { b: u8, g: u8, @@ -215,146 +336,153 @@ const RGB = packed struct { fn loadPalette(vga: VGA, palette: [256]Color) void { _ = vga; - x86.out(u8, PALETTE_INDEX, 0); // tell the VGA that palette data is coming. - for (palette) |rgb| { + // Tell the VGA that palette data is coming, starting at entry 0. + vga_regs.VgaPort.palette_write_index.write(0); - // enhance RGB565 to RGB666 - x86.out(u8, PALETTE_DATA, (@as(u6, rgb.r) << 1) | (rgb.r >> 4)); - x86.out(u8, PALETTE_DATA, (@as(u6, rgb.g) << 0)); - x86.out(u8, PALETTE_DATA, (@as(u6, rgb.b) << 1) | (rgb.b >> 4)); + for (palette) |rgb| { + // Enhance RGB565 to RGB666. + vga_regs.VgaPort.palette_data.write( + (@as(u6, rgb.r) << 1) | (rgb.r >> 4), + ); + vga_regs.VgaPort.palette_data.write( + @as(u6, rgb.g), + ); + vga_regs.VgaPort.palette_data.write( + (@as(u6, rgb.b) << 1) | (rgb.b >> 4), + ); } } -fn loadFixedPalette(vga: VGA) void { +fn loadFixedPalette() void { @setEvalBranchQuota(10_000); - _ = vga; - x86.out(u8, PALETTE_INDEX, 0); // tell the VGA that palette data is coming. + // Tell the VGA that palette data is coming, starting at entry 0. + vga_regs.VgaPort.palette_write_index.write(0); + inline for (0..256) |index| { const color: Color = comptime .from_u8(@intCast(index)); - const rgb = comptime color.to_rgb888(); const r6 = comptime Color.compress_channel(rgb.r, u6); const g6 = comptime Color.compress_channel(rgb.g, u6); const b6 = comptime Color.compress_channel(rgb.b, u6); - x86.out(u8, PALETTE_DATA, r6); - x86.out(u8, PALETTE_DATA, g6); - x86.out(u8, PALETTE_DATA, b6); + vga_regs.VgaPort.palette_data.write(r6); + vga_regs.VgaPort.palette_data.write(g6); + vga_regs.VgaPort.palette_data.write(b6); } } // pub fn setPaletteEntry(entry: u8, color: RGB) void { -// io.out(u8, PALETTE_INDEX, entry); // tell the VGA that palette data is coming. -// io.out(u8, PALETTE_DATA, color.r >> 2); // write the data -// io.out(u8, PALETTE_DATA, color.g >> 2); -// io.out(u8, PALETTE_DATA, color.b >> 2); +// vga_regs.VgaPort.palette_write_index.write(entry); +// vga_regs.VgaPort.palette_data.write(color.r >> 2); +// vga_regs.VgaPort.palette_data.write(color.g >> 2); +// vga_regs.VgaPort.palette_data.write(color.b >> 2); // } -// see: http://www.brackeen.com/vga/source/bc31/palette.c.html -pub fn waitForVSync() void { - const INPUT_STATUS = 0x03da; - const VRETRACE = 0x08; - - // wait until done with vertical retrace - while ((x86.in(u8, INPUT_STATUS) & VRETRACE) != 0) {} - // wait until done refreshing - while ((x86.in(u8, INPUT_STATUS) & VRETRACE) == 0) {} +fn wait_for_vsync() void { + const io_address_select = vga_regs.MiscellaneousOutputRegister.read().io_address_select; + + // Wait until the current vertical retrace has ended. + while (vga_regs.InputStatus1Register.read(io_address_select).vertical_retrace) {} + + // Wait until the next vertical retrace begins. + while (!vga_regs.InputStatus1Register.read(io_address_select).vertical_retrace) {} } -const VGA_AC_INDEX = 0x3C0; -const VGA_AC_WRITE = 0x3C0; -const VGA_AC_READ = 0x3C1; -const VGA_MISC_WRITE = 0x3C2; -const VGA_SEQ_INDEX = 0x3C4; -const VGA_SEQ_DATA = 0x3C5; -const VGA_DAC_READ_INDEX = 0x3C7; -const VGA_DAC_WRITE_INDEX = 0x3C8; -const VGA_DAC_DATA = 0x3C9; -const VGA_MISC_READ = 0x3CC; -const VGA_GC_INDEX = 0x3CE; -const VGA_GC_DATA = 0x3CF; -// COLOR emulation MONO emulation -const VGA_CRTC_INDEX = 0x3D4; // 0x3B4 -const VGA_CRTC_DATA = 0x3D5; // 0x3B5 -const VGA_INSTAT_READ = 0x3DA; +const VGA_NUM_REGS = + 1 + + VGA_NUM_SEQ_REGS + + VGA_NUM_CRTC_REGS + + VGA_NUM_GC_REGS + + VGA_NUM_AC_REGS; const VGA_NUM_SEQ_REGS = 5; const VGA_NUM_CRTC_REGS = 25; const VGA_NUM_GC_REGS = 9; const VGA_NUM_AC_REGS = 21; -const VGA_NUM_REGS = (1 + VGA_NUM_SEQ_REGS + VGA_NUM_CRTC_REGS + VGA_NUM_GC_REGS + VGA_NUM_AC_REGS); - -// pub fn setPixelDirect(x: usize, y: usize, c: Color) void { -// switch (mode) { -// .mode320x200 => { -// // setPlane(@truncate(u2, 0)); -// var segment = getFramebufferSegment(); -// segment[320 * y + x] = c; -// }, - -// .mode640x480 => { -// const wd_in_bytes = 640 / 8; -// const off = wd_in_bytes * y + x / 8; -// const px = @truncate(u3, x & 7); -// var mask: u8 = u8(0x80) >> px; -// var pmask: u8 = 1; - -// comptime var p: usize = 0; -// inline while (p < 4) : (p += 1) { -// setPlane(@truncate(u2, p)); -// var segment = getFramebufferSegment(); -// const src = segment[off]; -// segment[off] = if ((pmask & c) != 0) src | mask else src & ~mask; -// pmask <<= 1; -// } -// }, -// } -// } -// pub fn swapBuffers() void { -// @setRuntimeSafety(false); -// @setCold(false); - -// switch (mode) { -// .mode320x200 => { -// @intToPtr(*[height][width]Color, 0xA0000).* = backbuffer; -// }, -// .mode640x480 => { - -// // const bytes_per_line = 640 / 8; -// var plane: usize = 0; -// while (plane < 4) : (plane += 1) { -// const plane_mask: u8 = u8(1) << @truncate(u3, plane); -// setPlane(@truncate(u2, plane)); - -// var segment = get_fb_seg(); - -// var offset: usize = 0; - -// var y: usize = 0; -// while (y < 480) : (y += 1) { -// var x: usize = 0; -// while (x < 640) : (x += 8) { -// // const offset = bytes_per_line * y + (x / 8); -// var bits: u8 = 0; - -// // unroll for maximum fastness -// comptime var px: usize = 0; -// inline while (px < 8) : (px += 1) { -// const mask = u8(0x80) >> px; -// const index = backbuffer[y][x + px]; -// if ((index & plane_mask) != 0) { -// bits |= mask; -// } -// } - -// segment[offset] = bits; -// offset += 1; -// } -// } -// } -// }, -// } -// } +pub const VgaRegisterConfig = struct { + miscellaneous_output: vga_regs.MiscellaneousOutputRegister, + + sequencer: [VGA_NUM_SEQ_REGS]u8, + crtc: [VGA_NUM_CRTC_REGS]u8, + graphics_controller: [VGA_NUM_GC_REGS]u8, + attribute_controller: [VGA_NUM_AC_REGS]u8, +}; + +pub const g_320x200x256: VgaRegisterConfig = .{ + .miscellaneous_output = @bitCast(@as(u8, 0x63)), + + .sequencer = .{ + 0x03, + 0x01, + 0x0F, + 0x00, + 0x0E, + }, + + .crtc = .{ + 0x5F, + 0x4F, + 0x50, + 0x82, + 0x54, + 0x80, + 0xBF, + 0x1F, + 0x00, + 0x41, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x9C, + 0x0E, + 0x8F, + 0x28, + 0x40, + 0x96, + 0xB9, + 0xA3, + 0xFF, + }, + + .graphics_controller = .{ + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x40, + 0x05, + 0x0F, + 0xFF, + }, + + .attribute_controller = .{ + 0x00, + 0x01, + 0x02, + 0x03, + 0x04, + 0x05, + 0x06, + 0x07, + 0x08, + 0x09, + 0x0A, + 0x0B, + 0x0C, + 0x0D, + 0x0E, + 0x0F, + 0x41, + 0x00, + 0x0F, + 0x00, + 0x00, + }, +}; diff --git a/src/kernel/drivers/video/x86/vga-regs.zig b/src/kernel/drivers/video/x86/vga-regs.zig new file mode 100644 index 00000000..00fb1a5d --- /dev/null +++ b/src/kernel/drivers/video/x86/vga-regs.zig @@ -0,0 +1,258 @@ +const std = @import("std"); +const ashet = @import("../../../main.zig"); +const x86 = ashet.ports.platforms.x86; + +pub const VgaPort = enum(u16) { + pub fn read(port: VgaPort) u8 { + return x86.in(u8, @intFromEnum(port)); + } + + pub fn write(port: VgaPort, value: u8) void { + x86.out(u8, @intFromEnum(port), value); + } + + _, + + // Monochrome-compatible CRTC bank. + pub const crtc_index_monochrome: VgaPort = @enumFromInt(0x03B4); + pub const crtc_data_monochrome: VgaPort = @enumFromInt(0x03B5); + + pub const feature_control_write_monochrome: VgaPort = @enumFromInt(0x03BA); + pub const input_status_1_monochrome: VgaPort = @enumFromInt(0x03BA); + + /// Attribute Controller index/data stream. + pub const attribute_index_data: VgaPort = @enumFromInt(0x03C0); + + /// Read-back port for the currently selected Attribute Controller register. + pub const attribute_data_read: VgaPort = @enumFromInt(0x03C1); + + pub const miscellaneous_output_write: VgaPort = @enumFromInt(0x03C2); + pub const input_status_0: VgaPort = @enumFromInt(0x03C2); + + pub const video_subsystem_enable: VgaPort = @enumFromInt(0x03C3); + + pub const sequencer_index: VgaPort = @enumFromInt(0x03C4); + pub const sequencer_data: VgaPort = @enumFromInt(0x03C5); + + pub const palette_mask: VgaPort = @enumFromInt(0x03C6); + + /// Read: DAC state. + /// Write: palette read index. + pub const palette_read_index: VgaPort = @enumFromInt(0x03C7); + pub const dac_state: VgaPort = @enumFromInt(0x03C7); + + pub const palette_write_index: VgaPort = @enumFromInt(0x03C8); + + /// Streaming DAC palette data. + pub const palette_data: VgaPort = @enumFromInt(0x03C9); + + pub const feature_control_read: VgaPort = @enumFromInt(0x03CA); + + pub const miscellaneous_output_read: VgaPort = @enumFromInt(0x03CC); + + pub const graphics_controller_index: VgaPort = @enumFromInt(0x03CE); + pub const graphics_controller_data: VgaPort = @enumFromInt(0x03CF); + + // Color/graphics-compatible CRTC bank. + pub const crtc_index_color_graphics: VgaPort = @enumFromInt(0x03D4); + pub const crtc_data_color_graphics: VgaPort = @enumFromInt(0x03D5); + + pub const feature_control_write_color_graphics: VgaPort = @enumFromInt(0x03DA); + pub const input_status_1_color_graphics: VgaPort = @enumFromInt(0x03DA); +}; + +pub const IoAddressSelect = enum(u1) { + monochrome = 0, + color_graphics = 1, + + pub fn crtcIndexPort(select: IoAddressSelect) VgaPort { + return switch (select) { + .monochrome => VgaPort.crtc_index_monochrome, + .color_graphics => VgaPort.crtc_index_color_graphics, + }; + } + + pub fn crtcDataPort(select: IoAddressSelect) VgaPort { + return switch (select) { + .monochrome => VgaPort.crtc_data_monochrome, + .color_graphics => VgaPort.crtc_data_color_graphics, + }; + } + + pub fn inputStatus1Port(select: IoAddressSelect) VgaPort { + return switch (select) { + .monochrome => VgaPort.input_status_1_monochrome, + .color_graphics => VgaPort.input_status_1_color_graphics, + }; + } + + pub fn featureControlWritePort(select: IoAddressSelect) VgaPort { + return switch (select) { + .monochrome => VgaPort.feature_control_write_monochrome, + .color_graphics => VgaPort.feature_control_write_color_graphics, + }; + } +}; + +pub const MiscellaneousOutputRegister = packed struct(u8) { + pub const read_addr = VgaPort.miscellaneous_output_read; + pub const write_addr = VgaPort.miscellaneous_output_write; + + pub const SyncPolarity = enum(u1) { + positive = 0, + negative = 1, + }; + + /// This bit selects the CRT controller addresses. + io_address_select: IoAddressSelect, + + /// Controls system access to display memory. + ram_enable: bool, + + /// Selects the dot clock used to drive display timing. + clock_select: enum(u2) { + @"25 MHz" = 0b00, + @"28 MHz" = 0b01, + reserved_2 = 0b10, + reserved_3 = 0b11, + }, + + _reserved0: u1 = 0, + + /// Selects the upper/lower 64 KiB page when operating in odd/even mode. + odd_even_page_select: enum(u1) { + low = 0, + high = 1, + }, + + /// Determines the polarity of the horizontal sync pulse. + hsync_polarity: SyncPolarity, + + /// Determines the polarity of the vertical sync pulse. + vsync_polarity: SyncPolarity, + + pub fn read() MiscellaneousOutputRegister { + return @bitCast(read_addr.read()); + } + + pub fn write(reg: MiscellaneousOutputRegister) void { + write_addr.write(@bitCast(reg)); + } +}; + +pub const InputStatus0Register = packed struct(u8) { + pub const read_addr = VgaPort.input_status_0; + + _reserved0: u4, + + /// Hardware monitor/configuration sense input. + switch_sense: bool, + + _reserved1: u2, + + /// Set while a vertical-retrace interrupt is pending. + crt_interrupt_pending: bool, + + pub fn read() InputStatus0Register { + return @bitCast(read_addr.read()); + } +}; + +pub const VideoSubsystemEnableRegister = packed struct(u8) { + pub const read_addr = VgaPort.video_subsystem_enable; + pub const write_addr = VgaPort.video_subsystem_enable; + + /// Enables VGA I/O and memory address decoding. + enabled: bool, + + _reserved0: u7 = 0, + + pub fn read() VideoSubsystemEnableRegister { + return @bitCast(read_addr.read()); + } + + pub fn write(reg: VideoSubsystemEnableRegister) void { + write_addr.write(@bitCast(reg)); + } +}; + +pub const PaletteMaskRegister = packed struct(u8) { + pub const read_addr = VgaPort.palette_mask; + pub const write_addr = VgaPort.palette_mask; + + /// Bits set here enable the corresponding palette-index bits. + mask: u8, + + pub fn read() PaletteMaskRegister { + return @bitCast(read_addr.read()); + } + + pub fn write(reg: PaletteMaskRegister) void { + write_addr.write(@bitCast(reg)); + } +}; + +pub const DacStateRegister = packed struct(u8) { + pub const read_addr = VgaPort.dac_state; + + pub const State = enum(u2) { + read = 0b00, + write = 0b11, + _, + }; + + state: State, + + _reserved0: u6, + + pub fn read() DacStateRegister { + return @bitCast(read_addr.read()); + } +}; + +pub const FeatureControlRegister = packed struct(u8) { + pub const read_addr = VgaPort.feature_control_read; + + _reserved0: u3 = 0, + + vertical_sync_select: enum(u1) { + normal = 0, + sync_or_display_enable = 1, + } = .normal, + + _reserved1: u4 = 0, + + pub fn read() FeatureControlRegister { + return @bitCast(read_addr.read()); + } + + pub fn write( + io_address_select: IoAddressSelect, + reg: FeatureControlRegister, + ) void { + io_address_select.featureControlWritePort().write(@bitCast(reg)); + } +}; + +pub const InputStatus1Register = packed struct(u8) { + /// Set while active display output is disabled. + /// + /// This includes horizontal and vertical blanking/retrace periods. + display_disabled: bool, + + _reserved0: u2, + + /// Set while vertical retrace is active. + vertical_retrace: bool, + + /// Diagnostic video-data feedback. + diagnostic: u2, + + _reserved1: u2, + + /// Reading this register also resets the Attribute Controller + /// index/data flip-flop to the index state. + pub fn read(io_address_select: IoAddressSelect) InputStatus1Register { + return @bitCast(io_address_select.inputStatus1Port().read()); + } +}; From 7515ce154764591b340823386e7c45a74f8c34e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Sun, 13 Sep 2026 23:42:25 +0200 Subject: [PATCH 14/17] Streamlines the implementation for vsync including a synthetic vertical blanking event --- src/kernel/components/video.zig | 44 ++++++++--- .../video/Memory_Mapped_Framebuffer.zig | 76 ++++++++++--------- src/kernel/drivers/video/VGA.zig | 66 ++++++---------- src/kernel/port/platform/x86/multiboot.zig | 34 ++++----- 4 files changed, 113 insertions(+), 107 deletions(-) diff --git a/src/kernel/components/video.zig b/src/kernel/components/video.zig index 3613e60e..9fb15fb1 100644 --- a/src/kernel/components/video.zig +++ b/src/kernel/components/video.zig @@ -77,8 +77,11 @@ pub const VideoDevice = struct { ) void, }; + pub const VBlankFunctions = struct { + get_one_vblank_event_fn: *const fn (*ashet.drivers.Driver) bool, // TODO(gpu_support): Go through all drivers and see which actually support this + }; + get_properties_fn: *const fn (*ashet.drivers.Driver) DeviceProperties, - get_one_vblank_event_fn: ?*const fn (*ashet.drivers.Driver) bool = null, // TODO(gpu_support): Go through all drivers and see which actually support this begin_write_pixels_fn: *const fn ( driver: *ashet.drivers.Driver, @@ -89,7 +92,8 @@ pub const VideoDevice = struct { mode: PresentMode, ) void, - mapping_fns: ?MappingFunctions = null, + vblank_fns: ?VBlankFunctions, + mapping_fns: ?MappingFunctions, fn get_properties(vd: *VideoDevice) DeviceProperties { return vd.get_properties_fn(ashet.drivers.resolveDriver(.video, vd)); @@ -97,13 +101,13 @@ pub const VideoDevice = struct { /// Returns true if the video device does support waiting for vertical blanking intervals. fn supports_vblank_event(vd: *VideoDevice) bool { - return vd.get_one_vblank_event_fn != null; + return vd.vblank_fns != null; } /// Returns `true` if a vertical blanking interval has happened since the last call. fn get_one_vblank_event(vd: *VideoDevice) bool { - if (vd.get_one_vblank_event_fn) |get_one_vblank_event_fn| { - return get_one_vblank_event_fn(ashet.drivers.resolveDriver(.video, vd)); + if (vd.vblank_fns) |*funcs| { + return funcs.get_one_vblank_event_fn(ashet.drivers.resolveDriver(.video, vd)); } else { @panic("invalid API use"); } @@ -416,6 +420,10 @@ pub const BufferMapping = struct { var video_outputs: []Output = &.{}; +var next_expected_synthetic_frame: ashet.time.Instant = .system_start; + +const synthetic_frame_time_ms = 33; // roughly 30 FPS for synthetic video outputs + pub fn initialize() !void { const count: usize = blk: { var drivers = ashet.drivers.enumerate(.video); @@ -444,23 +452,35 @@ pub fn initialize() !void { }); } } + next_expected_synthetic_frame = ashet.time.Instant.now().add_ms(synthetic_frame_time_ms); } ///Ticks the video subsystem pub fn tick() void { + const had_synthetic_virtual_vblank = get_one_synthetic_vblank_event(); + + // Go through all video outputs and check if they had a vertical blanking event: for (video_outputs) |*video_output| { - // Go through all video outputs that support vertical blanking - // notifications and complete the awaiters: - if (!video_output.video_driver.supports_vblank_event()) - continue; + const had_vblank_event = if (video_output.video_driver.supports_vblank_event()) + video_output.video_driver.get_one_vblank_event() + else + had_synthetic_virtual_vblank; - if (video_output.video_driver.get_one_vblank_event()) { - // video_output.force_flush(); + if (had_vblank_event) { video_output.notify_vblank_awaiters(); } } +} - // TODO(gpu_support): How to implement non-vblanking video outputs with WaitForVSync? +fn get_one_synthetic_vblank_event() bool { + const now = ashet.time.Instant.now(); + + var had_vblank_event = false; + while (next_expected_synthetic_frame.less_or_equal(now)) { + next_expected_synthetic_frame = next_expected_synthetic_frame.add_ms(16); + had_vblank_event = true; + } + return had_vblank_event; } pub fn enumerate(maybe_ids: ?[]OutputID) usize { diff --git a/src/kernel/drivers/video/Memory_Mapped_Framebuffer.zig b/src/kernel/drivers/video/Memory_Mapped_Framebuffer.zig index c16f3acd..bd70d798 100644 --- a/src/kernel/drivers/video/Memory_Mapped_Framebuffer.zig +++ b/src/kernel/drivers/video/Memory_Mapped_Framebuffer.zig @@ -16,11 +16,7 @@ const Memory_Mapped_Framebuffer = @This(); driver: Driver, -base: [*]u8, -stride: usize, -width: u16, -height: u16, -byte_per_pixel: u32, +framebuffer: Framebuffer, backing_buffer: []align(ashet.memory.page_size) Color, border_color: Color = ashet.video.defaults.border_color, @@ -28,9 +24,6 @@ border_color: Color = ashet.video.defaults.border_color, pub fn create(allocator: std.mem.Allocator, comptime driver_name: []const u8, config: Config) !Memory_Mapped_Framebuffer { const framebuffer = try config.instantiate(); - const width = std.math.cast(u16, framebuffer.width) orelse return error.FramebufferSize; - const height = std.math.cast(u16, framebuffer.height) orelse return error.FramebufferSize; - ashet.memory.protection.ensure_accessible_slice(framebuffer.base[0 .. framebuffer.height * framebuffer.stride]); // Assert we can actually access the whole framebuffer: @@ -39,7 +32,7 @@ pub fn create(allocator: std.mem.Allocator, comptime driver_name: []const u8, co std.mem.doNotOptimizeAway(x); } - const vmem = try allocator.alignedAlloc(Color, .fromByteUnits(ashet.memory.page_size), framebuffer.width * framebuffer.height); + const vmem = try allocator.alignedAlloc(Color, .fromByteUnits(ashet.memory.page_size), @as(usize, framebuffer.width) * framebuffer.height); errdefer allocator.free(vmem); var driver = Memory_Mapped_Framebuffer{ @@ -49,16 +42,12 @@ pub fn create(allocator: std.mem.Allocator, comptime driver_name: []const u8, co .video = .{ .begin_write_pixels_fn = begin_write_pixels, .get_properties_fn = get_properties, + .vblank_fns = null, // The memory mapped framebuffer has no vertical blanking support + .mapping_fns = null, // The memory mapped framebuffer is expected to use RGB values }, }, }, - - .base = framebuffer.base, - .stride = framebuffer.stride, - .width = width, - .height = height, - .byte_per_pixel = framebuffer.byte_per_pixel, - + .framebuffer = framebuffer, .backing_buffer = vmem, }; @@ -66,13 +55,12 @@ pub fn create(allocator: std.mem.Allocator, comptime driver_name: []const u8, co @memset(vmem, ashet.video.defaults.border_color); ashet.video.load_splash_screen(.{ .base = vmem.ptr, - .width = width, - .height = height, + .width = framebuffer.width, + .height = framebuffer.height, .stride = framebuffer.width, }); - // Immediate flush to show the boot splash: - framebuffer.flush_fn(&driver.driver); + driver.present(); return driver; } @@ -81,8 +69,8 @@ fn get_properties(driver: *Driver) ashet.video.DeviceProperties { const vd: *Memory_Mapped_Framebuffer = @fieldParentPtr("driver", driver); return .{ .resolution = .{ - .width = vd.width, - .height = vd.height, + .width = vd.framebuffer.width, + .height = vd.framebuffer.height, }, .buffer_support = .none, @@ -104,9 +92,9 @@ fn begin_write_pixels( .{ .dst_buffer = .{ .data = vd.backing_buffer.ptr, - .width = vd.width, - .height = vd.height, - .stride = vd.width, + .width = vd.framebuffer.width, + .height = vd.framebuffer.height, + .stride = vd.framebuffer.width, }, .dst_pos = .{ .x = @intCast(rectangle.x), @@ -122,17 +110,28 @@ fn begin_write_pixels( null, ); - _ = mode; + switch (mode) { + .dont_care => {}, // we just don't have to show anything here + + // for swapping modes, we do have to copy over portions of the memory: + .immediate, .vblank => vd.present(), + } return call.finalize(ashet.abi.video.WritePixels, .{}); } +fn present(vd: *Memory_Mapped_Framebuffer) void { + + // Immediate flush to show the boot splash: + vd.framebuffer.flush_fn(&vd.driver); +} + pub const Framebuffer = struct { flush_fn: *const fn (*Driver) void, base: [*]u8, stride: usize, - width: u32, - height: u32, + width: u16, + height: u16, byte_per_pixel: u32, }; @@ -155,6 +154,9 @@ pub const Config = struct { pub fn instantiate(cfg: Config) error{Unsupported}!Framebuffer { errdefer logger.warn("unsupported framebuffer configuration: {}", .{cfg}); + const width = std.math.cast(u16, cfg.width) orelse return error.Unsupported; + const height = std.math.cast(u16, cfg.height) orelse return error.Unsupported; + // special case for if (cfg.red_mask_size == 0 and cfg.green_mask_size == 0 and @@ -177,8 +179,8 @@ pub const Config = struct { .base = cfg.scanline0, .stride = 4 * cfg.width, - .width = cfg.width, - .height = cfg.height, + .width = width, + .height = height, .byte_per_pixel = @divExact(cfg.bits_per_pixel, 8), }; @@ -223,8 +225,8 @@ pub const Config = struct { .base = cfg.scanline0, .stride = cfg.bytes_per_scan_line, - .width = cfg.width, - .height = cfg.height, + .width = width, + .height = height, .byte_per_pixel = @divExact(cfg.bits_per_pixel, 8), }; @@ -238,10 +240,10 @@ pub const Config = struct { @setRuntimeSafety(false); // const flush_time_start = readHwCounter(); - const pixel_count = @as(usize, vd.width) * @as(usize, vd.height); + const pixel_count = @as(usize, vd.framebuffer.width) * @as(usize, vd.framebuffer.height); { - var row = vd.base; + var row = vd.framebuffer.base; var ind: usize = 0; var x: usize = 0; @@ -249,12 +251,12 @@ pub const Config = struct { write(row + ind, color); x += 1; - ind += vd.byte_per_pixel; + ind += vd.framebuffer.byte_per_pixel; - if (x == vd.width) { + if (x == vd.framebuffer.width) { x = 0; ind = 0; - row += vd.stride; + row += vd.framebuffer.stride; } } } diff --git a/src/kernel/drivers/video/VGA.zig b/src/kernel/drivers/video/VGA.zig index 3f75fbca..ec2d118c 100644 --- a/src/kernel/drivers/video/VGA.zig +++ b/src/kernel/drivers/video/VGA.zig @@ -14,26 +14,7 @@ const modes = @import("x86/vga-mode-presets.zig"); const width = 320; const height = 200; -backbuffer: [width * height]Color align(ashet.memory.page_size) = undefined, - -driver: Driver = .{ - .name = "VGA", - .class = .{ - .video = .{ - .get_properties_fn = get_properties, - .begin_write_pixels_fn = begin_write_pixels, - .get_one_vblank_event_fn = get_one_vblank_event, - .mapping_fns = .{ - .create_mapped_buffer_fn = ashet.video.VideoDevice.default_create_mapped_buffer_front, - .get_mapped_buffer_fn = get_mapped_buffer, - .destroy_mapped_buffer_fn = ashet.video.VideoDevice.destroy_mapped_buffer_noop, - }, - }, - }, -}, - -vblank_irq_support: VBlankIrqSupport, -next_expected_retrace: ashet.time.Instant, +driver: Driver, const memory_ranges = [_]x86.vmm.Range{ .{ .base = 0xA0000, .length = 0x20000 }, @@ -67,14 +48,29 @@ pub fn init(vga: *VGA) !void { .stride = width, }); - const next_expected_retrace: ashet.time.Instant = switch (vblank_irq_support) { - .supported => undefined, - .unsupported => ashet.time.Instant.now().add_ms(16), - }; - vga.* = VGA{ - .vblank_irq_support = vblank_irq_support, - .next_expected_retrace = next_expected_retrace, + .driver = .{ + .name = "VGA", + .class = .{ + .video = .{ + .get_properties_fn = get_properties, + .begin_write_pixels_fn = begin_write_pixels, + + .mapping_fns = .{ + .create_mapped_buffer_fn = ashet.video.VideoDevice.default_create_mapped_buffer_front, + .get_mapped_buffer_fn = get_mapped_buffer, + .destroy_mapped_buffer_fn = ashet.video.VideoDevice.destroy_mapped_buffer_noop, + }, + + .vblank_fns = switch (vblank_irq_support) { + .unsupported => null, + .supported => .{ + .get_one_vblank_event_fn = get_one_vblank_event, + }, + }, + }, + }, + }, }; } @@ -92,20 +88,8 @@ fn get_properties(driver: *Driver) ashet.video.DeviceProperties { fn get_one_vblank_event(driver: *Driver) bool { const vd: *VGA = @alignCast(@fieldParentPtr("driver", driver)); - - return switch (vd.vblank_irq_support) { - .supported => readAndResetIrq(), - - .unsupported => blk: { - var had_vblank_event = false; - const now = ashet.time.Instant.now(); - while (vd.next_expected_retrace.less_or_equal(now)) { - vd.next_expected_retrace = vd.next_expected_retrace.add_ms(16); - had_vblank_event = true; - } - break :blk had_vblank_event; - }, - }; + _ = vd; + return readAndResetIrq(); } fn get_mapped_buffer(driver: *Driver, buffer: ashet.video.BufferKind) ashet.video.VideoMemory { diff --git a/src/kernel/port/platform/x86/multiboot.zig b/src/kernel/port/platform/x86/multiboot.zig index 4c644610..621127bc 100644 --- a/src/kernel/port/platform/x86/multiboot.zig +++ b/src/kernel/port/platform/x86/multiboot.zig @@ -38,17 +38,17 @@ pub const Header = extern struct { const Flags = packed struct(u32) { /// If bit 0 in the ‘flags’ word is set, then all boot modules loaded along with the operating system must be aligned on page (4KB) boundaries. Some operating systems expect to be able to map the pages containing boot modules directly into a paged address space during startup, and thus need the boot modules to be page-aligned. - req_modules_align_4k: bool, + req_modules_align_4k: bool, // [0] /// If bit 1 in the ‘flags’ word is set, then information on available memory via at least the ‘mem_*’ fields of the Multiboot information structure (see Boot information format) must be included. If the boot loader is capable of passing a memory map (the ‘mmap_*’ fields) and one exists, then it may be included as well. - req_mem_info: bool, + req_mem_info: bool, // [1] /// If bit 2 in the ‘flags’ word is set, information about the video mode table (see Boot information format) must be available to the kernel. - req_video_mode: bool, + req_video_mode: bool, // [2] padding0: u13 = 0, /// If bit 16 in the ‘flags’ word is set, then the fields at offsets 12-28 in the Multiboot header are valid, and the boot loader should use them instead of the fields in the actual executable header to calculate where to load the OS image. This information does not need to be provided if the kernel image is in ELF format, but it must be provided if the images is in a.out format or in some other format. Compliant boot loaders must be able to load images that either are in ELF format or contain the load address information embedded in the Multiboot header; they may also directly support other executable formats, such as particular a.out variants, but are not required to. - hint_use_embedded_offsets: bool, + hint_use_embedded_offsets: bool, // [16] padding1: u15 = 0, }; @@ -197,19 +197,19 @@ pub const Info = extern struct { framebuffer: Framebuffer, pub const Flags = packed struct(u32) { - mem: bool, - boot_device: bool, - cmdline: bool, - mods: bool, - syms_v1: bool, - syms_v2: bool, - mmap: bool, - drives: bool, - config_table: bool, - boot_loader_name: bool, - apm_table: bool, - vbe: bool, - framebuffer: bool, + mem: bool, // [0] + boot_device: bool, // [1] + cmdline: bool, // [2] + mods: bool, // [3] + syms_v1: bool, // [4] + syms_v2: bool, // [5] + mmap: bool, // [6] + drives: bool, // [7] + config_table: bool, // [8] + boot_loader_name: bool, // [9] + apm_table: bool, // [10] + vbe: bool, // [11] + framebuffer: bool, // [12] _reserved: u19, pub fn format(flags: Flags, writer: *std.Io.Writer) !void { From 75f6444fbd320f2669f41e144ff43faecafe410e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Mon, 14 Sep 2026 00:10:53 +0200 Subject: [PATCH 15/17] Adapts the x86 PC port to the improved driver interface --- src/kernel/components/graphics.zig | 2 +- .../drivers/video/AVAPv1_Framebuffer.zig | 3 +++ .../video/Externally_Managed_Output.zig | 25 +++++++++++++++++++ .../drivers/video/Virtio_GPU_Device.zig | 3 +++ .../drivers/video/Virtual_Video_Output.zig | 4 +++ src/kernel/port/hosted/VNC_Server.zig | 5 +++- .../x86/hosted-linux/Wayland_Display.zig | 2 ++ .../machine/x86/hosted-linux/X11_Display.zig | 1 + 8 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/kernel/components/graphics.zig b/src/kernel/components/graphics.zig index 459d427a..6a617151 100644 --- a/src/kernel/components/graphics.zig +++ b/src/kernel/components/graphics.zig @@ -37,7 +37,7 @@ pub const RasterizerBackend = enum { } }; -pub var selected_rasterizer: RasterizerBackend = .linear_async; +pub var selected_rasterizer: RasterizerBackend = .linear_sync; pub var use_perfctrl: bool = false; comptime { diff --git a/src/kernel/drivers/video/AVAPv1_Framebuffer.zig b/src/kernel/drivers/video/AVAPv1_Framebuffer.zig index 4e1e58fa..5e9c7fb2 100644 --- a/src/kernel/drivers/video/AVAPv1_Framebuffer.zig +++ b/src/kernel/drivers/video/AVAPv1_Framebuffer.zig @@ -17,6 +17,8 @@ driver: Driver = .{ .video = .{ .get_properties_fn = get_properties, .begin_write_pixels_fn = driver_begin_write_pixels, + .mapping_fns = null, + .vblank_fns = null, // TODO(gpu_support): We actually can do AVAPv1 vertical blanking intervals, but we can't do them trivially yet }, }, }, @@ -121,6 +123,7 @@ fn get_properties(driver: *Driver) ashet.video.DeviceProperties { .width = width, .height = height, }, + .buffer_support = .none, }; } diff --git a/src/kernel/drivers/video/Externally_Managed_Output.zig b/src/kernel/drivers/video/Externally_Managed_Output.zig index 82c9a7d2..85d2bc6a 100644 --- a/src/kernel/drivers/video/Externally_Managed_Output.zig +++ b/src/kernel/drivers/video/Externally_Managed_Output.zig @@ -40,6 +40,8 @@ driver: Driver, write_pixels_fn: *const WritePixelsSyncFn, write_pixels_ctx: ?*anyopaque, +had_vblank_event: std.atomic.Value(bool) = .init(false), + pub fn init( comptime name: []const u8, width: u16, @@ -47,6 +49,7 @@ pub fn init( comptime write_pixels_fn: WritePixelsSyncFn, write_pixels_ctx: ?*anyopaque, comptime backing: BackingStorage, + comptime supports_vblank_await: bool, ) !Host_VNC_Output { const fb: ?[]Color = switch (backing) { .allocate => try std.heap.page_allocator.alloc(Color, @as(u32, width) * @as(u32, height)), @@ -61,6 +64,15 @@ pub fn init( .video = .{ .get_properties_fn = get_properties, .begin_write_pixels_fn = begin_write_pixels, + + .vblank_fns = if (supports_vblank_await) + .{ + .get_one_vblank_event_fn = get_one_vblank_event, + } + else + null, + + .mapping_fns = null, }, }, }, @@ -74,6 +86,10 @@ pub fn init( }; } +pub fn notify_vblank_event(vd: *Host_VNC_Output) void { + vd.had_vblank_event.store(true, .seq_cst); +} + fn get_properties(driver: *Driver) ashet.video.DeviceProperties { const vd: *Host_VNC_Output = @fieldParentPtr("driver", driver); return .{ @@ -81,9 +97,18 @@ fn get_properties(driver: *Driver) ashet.video.DeviceProperties { .width = vd.width, .height = vd.height, }, + .buffer_support = .none, }; } +fn get_one_vblank_event(driver: *Driver) bool { + const vd: *Host_VNC_Output = @fieldParentPtr("driver", driver); + + const had_vblank = vd.had_vblank_event.swap(false, .seq_cst); + + return had_vblank; +} + fn begin_write_pixels( driver: *Driver, call: *ashet.overlapped.AsyncCall, diff --git a/src/kernel/drivers/video/Virtio_GPU_Device.zig b/src/kernel/drivers/video/Virtio_GPU_Device.zig index c63029bf..f0553a69 100644 --- a/src/kernel/drivers/video/Virtio_GPU_Device.zig +++ b/src/kernel/drivers/video/Virtio_GPU_Device.zig @@ -25,6 +25,8 @@ driver: Driver = .{ .video = .{ .get_properties_fn = get_properties, .begin_write_pixels_fn = begin_write_pixels, + .vblank_fns = null, // VirtIO does not support vertical blanking events + .mapping_fns = null, // VirtIO does not support memory mappings of 8bpp graphics }, }, }, @@ -128,6 +130,7 @@ fn get_properties(driver: *Driver) ashet.video.DeviceProperties { .width = vd.graphics_width, .height = vd.graphics_height, }, + .buffer_support = .none, }; } diff --git a/src/kernel/drivers/video/Virtual_Video_Output.zig b/src/kernel/drivers/video/Virtual_Video_Output.zig index 50c5b9d1..2aac18b7 100644 --- a/src/kernel/drivers/video/Virtual_Video_Output.zig +++ b/src/kernel/drivers/video/Virtual_Video_Output.zig @@ -16,6 +16,8 @@ driver: Driver = .{ .video = .{ .get_properties_fn = get_properties, .begin_write_pixels_fn = driver_begin_write_pixels, + .vblank_fns = null, + .mapping_fns = null, }, }, }, @@ -36,8 +38,10 @@ fn get_properties(driver: *Driver) ashet.video.DeviceProperties { .width = width, .height = height, }, + .buffer_support = .none, }; } + fn driver_begin_write_pixels( driver: *Driver, call: *ashet.overlapped.AsyncCall, diff --git a/src/kernel/port/hosted/VNC_Server.zig b/src/kernel/port/hosted/VNC_Server.zig index caa90679..b1410b93 100644 --- a/src/kernel/port/hosted/VNC_Server.zig +++ b/src/kernel/port/hosted/VNC_Server.zig @@ -51,6 +51,7 @@ pub fn init( write_vnc_pixels, server, .allocate, + true, ), .input = ashet.drivers.input.Host_VNC_Input.init(), }; @@ -391,10 +392,12 @@ fn handle_event(vd: *VNC_Server, state: *Session_State, request_allocator: std.m }, // use internal handler .framebuffer_update_request => |req| { - _ = vd; _ = request_allocator; state.incremental_update_request = req; // try vd.send_incremental_update(state, request_allocator, req); + + // Notify the OS that we have a new frame requested, and we shall provide more data + vd.screen.notify_vblank_event(); }, .key_event => |ev| { diff --git a/src/kernel/port/machine/x86/hosted-linux/Wayland_Display.zig b/src/kernel/port/machine/x86/hosted-linux/Wayland_Display.zig index 4b02ac6b..fbdc379e 100644 --- a/src/kernel/port/machine/x86/hosted-linux/Wayland_Display.zig +++ b/src/kernel/port/machine/x86/hosted-linux/Wayland_Display.zig @@ -74,6 +74,7 @@ pub fn init( write_wayland_pixels, server, .allocate, // TODO(gpu_support): Is this necessary? + true, ), // .input = ashet.drivers.input.Host_SDL_Input.init(), @@ -227,6 +228,7 @@ pub fn process_events(server: *Wayland_Display) !void { server.should_render = false; server.frame_count += 1; + server.screen.notify_vblank_event(); } // logger.info("tick {}", .{server.frame_count}); diff --git a/src/kernel/port/machine/x86/hosted-linux/X11_Display.zig b/src/kernel/port/machine/x86/hosted-linux/X11_Display.zig index 4ceaee0c..25182d10 100644 --- a/src/kernel/port/machine/x86/hosted-linux/X11_Display.zig +++ b/src/kernel/port/machine/x86/hosted-linux/X11_Display.zig @@ -107,6 +107,7 @@ pub fn init( write_x11_pixels, null, .allocate, + false, ), // .input = ashet.drivers.input.Host_SDL_Input.init(), From a0aaac6d43e07437b6a99b63cb57846a07349bab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Wed, 16 Sep 2026 20:55:22 +0200 Subject: [PATCH 16/17] xq: Fixes issues with AVAPv1 framebuffer driver. Codex: Implements Host_EvDev_Input device to use the simulation without a window on Linux --- justfile | 4 +- src/kernel/drivers/drivers.zig | 1 + src/kernel/drivers/input/Host_EvDev_Input.zig | 229 ++++++++++++++++++ .../drivers/video/AVAPv1_Framebuffer.zig | 20 +- src/kernel/main.zig | 1 + src/kernel/port/hosted/initialize.zig | 24 ++ 6 files changed, 274 insertions(+), 5 deletions(-) create mode 100644 src/kernel/drivers/input/Host_EvDev_Input.zig diff --git a/justfile b/justfile index fd464604..2e8d96d8 100644 --- a/justfile +++ b/justfile @@ -18,7 +18,9 @@ run-avap-simulation: zig-out/bin/debug-filter --elf kernel=./zig-out/x86-hosted-linux/kernel.elf \ ./zig-out/x86-hosted-linux/kernel.elf \ "drive;zig-out/x86-hosted-linux/disk.img" \ - "video;avap-v1;640;400;/dev/serial/by-id/usb-Ashet_Technologies_Fast_Bridge_AT-FB-00001-if00-port0" + "video;avap-v1;640;400;/dev/serial/by-id/usb-Ashet_Technologies_Fast_Bridge_AT-FB-00001-if00-port0" \ + "input;evdev;/dev/input/event24" \ + "input;evdev;/dev/input/event7" run machine: {{zig}} build {{default_params}} --summary none -Doptimize-kernel={{optimize_kernel}} -Doptimize-apps={{optimize_apps}} -Dmachine={{machine}} tools run diff --git a/src/kernel/drivers/drivers.zig b/src/kernel/drivers/drivers.zig index c08babd8..e95c4ec4 100644 --- a/src/kernel/drivers/drivers.zig +++ b/src/kernel/drivers/drivers.zig @@ -72,6 +72,7 @@ pub const input = struct { pub const Host_VNC_Input = @import("input/Host_VNC_Input.zig"); pub const Host_SDL_Input = @import("input/Host_SDL_Input.zig"); + pub const Host_EvDev_Input = @import("input/Host_EvDev_Input.zig"); pub const Generic_PS2_Device = @import("input/Generic_PS2_Device.zig"); pub const PropIO_PS2_Device = @import("input/PropIO_PS2_Device.zig"); diff --git a/src/kernel/drivers/input/Host_EvDev_Input.zig b/src/kernel/drivers/input/Host_EvDev_Input.zig new file mode 100644 index 00000000..47e39827 --- /dev/null +++ b/src/kernel/drivers/input/Host_EvDev_Input.zig @@ -0,0 +1,229 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const ashet = @import("../../main.zig"); +const codes = @import("../../data/input-event-codes.zig"); +const evdev = @import("evdev.zig"); +const linux = std.os.linux; +const logger = std.log.scoped(.host_evdev_input); + +const Host_EvDev_Input = @This(); +const Driver = ashet.drivers.Driver; + +// These are kernel longs, not libc's time_t (which is 64-bit on i386 musl). +const InputEvent = extern struct { + seconds: usize, + microseconds: usize, + type: u16, + code: u16, + value: i32, +}; + +comptime { + if (builtin.cpu.arch == .x86) { + std.debug.assert(@sizeOf(InputEvent) == 16); + std.debug.assert(@offsetOf(InputEvent, "type") == 8); + } +} + +const KeyState = [codes.KEY.CNT / 8]u8; +const get_version = linux.IOCTL.IOR('E', 0x01, i32); +const get_keys = linux.IOCTL.IOR('E', 0x18, KeyState); +const records_per_poll = 64; +const events_per_poll = 16; + +// Restore modifiers before ordinary keys so initial/resynchronized key presses +// are translated with the correct modifiers by the input subsystem. +const modifiers = [_]u16{ + codes.KEY.LEFTSHIFT, codes.KEY.RIGHTSHIFT, + codes.KEY.LEFTCTRL, codes.KEY.RIGHTCTRL, + codes.KEY.LEFTALT, codes.KEY.RIGHTALT, + codes.KEY.LEFTMETA, codes.KEY.RIGHTMETA, +}; + +driver: Driver = .{ + .name = "Host EvDev Input", + .class = .{ .input = .{ .pollFn = poll } }, +}, +fd: ?std.posix.fd_t, +events: [records_per_poll]InputEvent = undefined, +event_index: usize = 0, +event_count: usize = 0, +pressed: KeyState = @splat(0), +snapshot: KeyState = @splat(0), +sync_index: ?usize = null, +dropped: bool = false, +wheel: i32 = 0, + +pub fn init(path: []const u8) !Host_EvDev_Input { + const fd = try std.posix.open(path, .{ + .ACCMODE = .RDONLY, + .NONBLOCK = true, + .CLOEXEC = true, + }, 0); + errdefer std.posix.close(fd); + + var version: i32 = undefined; + try query(fd, get_version, &version); + + var device: Host_EvDev_Input = .{ .fd = fd }; + try device.synchronize(); + return device; +} + +fn query(fd: std.posix.fd_t, request: u32, result: anytype) !void { + while (true) { + switch (linux.E.init(linux.ioctl(fd, request, @intFromPtr(result)))) { + .SUCCESS => return, + .INTR => continue, + .NOTTY => return error.NotEvdevDevice, + .NODEV => return error.DeviceDisconnected, + else => return error.DeviceIoError, + } + } +} + +fn synchronize(device: *Host_EvDev_Input) !void { + device.snapshot = @splat(0); + try query(device.fd.?, get_keys, &device.snapshot); + device.sync_index = 0; +} + +fn isPressed(state: *const KeyState, code: u16) bool { + return state[code / 8] & (@as(u8, 1) << @as(u3, @truncate(code))) != 0; +} + +fn setPressed(state: *KeyState, code: u16, down: bool) void { + const mask = @as(u8, 1) << @as(u3, @truncate(code)); + if (down) { + state[code / 8] |= mask; + } else { + state[code / 8] &= ~mask; + } +} + +fn keyEvent(code: u16, down: bool) ?ashet.input.raw.Event { + if (evdev.keyFromEvdev(code)) |usage| { + return .{ .keyboard = .{ .usage = usage, .down = down } }; + } + if (evdev.mouseFromEvdev(code)) |button| { + return .{ .mouse_button = .{ .button = button, .down = down } }; + } + return null; +} + +fn disable(device: *Host_EvDev_Input, comptime message: []const u8, args: anytype) void { + const fd = device.fd orelse return; + logger.err("disabling evdev fd {}: " ++ message, .{fd} ++ args); + std.posix.close(fd); + device.fd = null; + device.event_count = 0; + device.event_index = 0; + device.wheel = 0; + // Reconcile to an empty snapshot to release everything we published. + device.snapshot = @splat(0); + device.sync_index = 0; +} + +fn poll(driver: *Driver) void { + const device: *Host_EvDev_Input = @fieldParentPtr("driver", driver); + var records_left: usize = records_per_poll; + var events_left: usize = events_per_poll; + + while (events_left > 0) { + if (device.sync_index) |index| { + if (index == modifiers.len + codes.KEY.CNT) { + device.sync_index = null; + continue; + } + device.sync_index = index + 1; + const code: u16 = if (index < modifiers.len) modifiers[index] else @intCast(index - modifiers.len); + const down = isPressed(&device.snapshot, code); + if (down != isPressed(&device.pressed, code)) { + if (keyEvent(code, down)) |event| { + setPressed(&device.pressed, code, down); + ashet.input.push_raw_event(event); + events_left -= 1; + } + } + continue; + } + + if (device.wheel != 0) { + // Keep each synthetic click together, even across poll boundaries. + if (events_left < 2) return; + const button: ashet.abi.MouseButton = if (device.wheel > 0) .wheel_up else .wheel_down; + ashet.input.push_raw_event(.{ .mouse_button = .{ .button = button, .down = true } }); + ashet.input.push_raw_event(.{ .mouse_button = .{ .button = button, .down = false } }); + device.wheel += if (device.wheel > 0) @as(i32, -1) else 1; + events_left -= 2; + continue; + } + + const fd = device.fd orelse return; + if (records_left == 0) return; + + if (device.event_index == device.event_count) { + const buffer = std.mem.sliceAsBytes(device.events[0..records_left]); + const result = linux.read(fd, buffer.ptr, buffer.len); + switch (linux.E.init(result)) { + .SUCCESS => {}, + .INTR => continue, + .AGAIN => return, + else => |err| { + device.disable("read failed: {s}", .{@tagName(err)}); + continue; + }, + } + if (result == 0 or result % @sizeOf(InputEvent) != 0) { + device.disable("invalid read length: {}", .{result}); + continue; + } + device.event_index = 0; + device.event_count = result / @sizeOf(InputEvent); + } + + const event = device.events[device.event_index]; + device.event_index += 1; + records_left -= 1; + + if (device.dropped) { + if (event.type == codes.EV.SYN and event.code == codes.SYN.REPORT) { + device.dropped = false; + device.synchronize() catch |err| { + device.disable("state query failed: {s}", .{@errorName(err)}); + }; + } + continue; + } + + switch (event.type) { + codes.EV.SYN => { + if (event.code == codes.SYN.DROPPED) { + device.dropped = true; + } + }, + codes.EV.KEY => { + if (event.code >= codes.KEY.CNT or event.value < 0 or event.value > 2) continue; + const raw = keyEvent(event.code, event.value != 0) orelse continue; + if (raw == .mouse_button and event.value == 2) continue; + setPressed(&device.pressed, event.code, event.value != 0); + ashet.input.push_raw_event(raw); + events_left -= 1; + }, + codes.EV.REL => switch (event.code) { + codes.REL.X, codes.REL.Y => { + if (event.value == 0) continue; + const delta: i16 = @intCast(std.math.clamp(event.value, std.math.minInt(i16), std.math.maxInt(i16))); + ashet.input.push_raw_event(.{ .mouse_rel_motion = .{ + .dx = if (event.code == codes.REL.X) delta else 0, + .dy = if (event.code == codes.REL.Y) delta else 0, + } }); + events_left -= 1; + }, + codes.REL.WHEEL => device.wheel = event.value, + else => {}, + }, + else => {}, + } + } +} diff --git a/src/kernel/drivers/video/AVAPv1_Framebuffer.zig b/src/kernel/drivers/video/AVAPv1_Framebuffer.zig index 5e9c7fb2..db7aa8fa 100644 --- a/src/kernel/drivers/video/AVAPv1_Framebuffer.zig +++ b/src/kernel/drivers/video/AVAPv1_Framebuffer.zig @@ -32,6 +32,9 @@ pub fn init( .device = undefined, }; + logger.debug("try opening AVAPv1 device at \"{f}\"", .{ + std.zig.fmtString(file_name), + }); fb.device = std.fs.cwd().openFile(file_name, .{ .mode = .read_write }) catch |err| switch (err) { error.FileNotFound, => return error.FileNotFound, @@ -186,7 +189,7 @@ fn begin_write_pixels( stride: usize, mode: ashet.abi.video.PresentMode, ) !void { - // logger.debug("write buffer", .{}); + logger.debug("write buffer({f}, {})", .{ rectangle, pixels.len }); try write_rectangle(vd.device, rectangle, pixels, stride); // logger.debug("swap buffers", .{}); @@ -239,7 +242,7 @@ pub fn write_rectangle(port: std.fs.File, rectangle: ashet.abi.Rectangle, pixels const x = std.math.cast(u16, rectangle.x).?; const y = std.math.cast(u16, rectangle.y).?; - try write_header(port, length, .write_buffer); + try write_header(port, length, .write_rectangle); try write_all(port, &int_slice(u16, x)); try write_all(port, &int_slice(u16, y)); @@ -290,6 +293,8 @@ pub fn swap_buffers(port: std.fs.File) !void { } fn write_command(port: std.fs.File, cmd: Command, buffer: []const u8) !void { + logger.debug("=> cmd({t}, {} bytes)", .{ cmd, buffer.len }); + try write_header(port, buffer.len, cmd); if (buffer.len > 0) { try write_all(port, buffer); @@ -332,9 +337,13 @@ fn write_footer(port: std.fs.File, total_length: usize) !void { } fn read_header(port: std.fs.File, deadline: Deadline) !Header { + logger.debug("read cmd...", .{}); var buffer: [4]u8 = undefined; try read_all(port, &buffer, deadline); - return @bitCast(std.mem.readInt(u32, &buffer, .little)); + const header: Header = @bitCast(std.mem.readInt(u32, &buffer, .little)); + + logger.debug("<= rsp({t}, {s}, {} bytes)", .{ header.cmd, if (header.ack) "ack" else "nak", header.length }); + return header; } fn read_footer(port: std.fs.File, response: Header, deadline: Deadline) !void { @@ -353,7 +362,10 @@ fn read_discarding(port: std.fs.File, length: usize, deadline: Deadline, output: const len = try port.read(buffer[0..limit]); if (len > 0 and output == .log) { - logger.err("unexpected data from device: {x}", .{buffer[0..len]}); + logger.err("unexpected data from device: {x} (\"{f}\")", .{ + buffer[0..len], + std.zig.fmtString(buffer[0..len]), + }); } count += len; diff --git a/src/kernel/main.zig b/src/kernel/main.zig index 16633273..b838c2c3 100644 --- a/src/kernel/main.zig +++ b/src/kernel/main.zig @@ -103,6 +103,7 @@ pub const log_levels = struct { pub var nested_i2c_device: LogLevel = .debug; pub var generic_ps2: LogLevel = .info; + pub var ashet_fb: LogLevel = .info; pub var ds1306: LogLevel = .info; diff --git a/src/kernel/port/hosted/initialize.zig b/src/kernel/port/hosted/initialize.zig index 849910a4..85935188 100644 --- a/src/kernel/port/hosted/initialize.zig +++ b/src/kernel/port/hosted/initialize.zig @@ -3,6 +3,7 @@ //! const std = @import("std"); +const builtin = @import("builtin"); const ashet = @import("../../main.zig"); const logger = std.log.scoped(.hosted); @@ -110,6 +111,29 @@ pub fn initialize(comptime video_drivers: std.StaticStringMap(VideoDriverCtor)) driver.* = try ashet.drivers.block.Host_Disk_Image.init(file, mode); ashet.drivers.install(&driver.driver); + } else if (std.mem.eql(u8, component, "input")) { + const device_type = iter.next() orelse badKernelOption("input", "missing input device type", .{}); + + if (std.mem.eql(u8, device_type, "evdev")) { + if (builtin.os.tag == .linux) { + // "input;evdev;/dev/input/eventX" + const path = iter.next() orelse badKernelOption("input", "missing evdev device path", .{}); + if (path.len == 0) badKernelOption("input", "empty evdev device path", .{}); + if (iter.next()) |option| badKernelOption("input", "unexpected option \"{f}\"", .{ + std.zig.fmtString(option), + }); + + const driver = try global_memory.create(ashet.drivers.input.Host_EvDev_Input); + driver.* = ashet.drivers.input.Host_EvDev_Input.init(path) catch |err| { + badKernelOption("input", "cannot initialize evdev device '{s}': {s}", .{ path, @errorName(err) }); + }; + ashet.drivers.install(&driver.driver); + } else { + badKernelOption("input", "evdev is only supported on Linux", .{}); + } + } else { + badKernelOption("input", "bad input device type '{s}'", .{device_type}); + } } else if (std.mem.eql(u8, component, "video")) { // "video::::" const device_type = iter.next() orelse badKernelOption("video", "missing video device type", .{}); From 7b127292fce3adcec5925c05b539d5cb0bb8063b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Tue, 22 Sep 2026 20:50:56 +0200 Subject: [PATCH 17/17] Overhauls desktop rendering and fixes one or more incremental rendering bugs. --- .../desktop/classic/src/WindowManager.zig | 14 +- .../desktop/classic/src/classic-desktop.zig | 123 ++++++++++++------ 2 files changed, 97 insertions(+), 40 deletions(-) diff --git a/src/userland/apps/desktop/classic/src/WindowManager.zig b/src/userland/apps/desktop/classic/src/WindowManager.zig index 1a2b89bc..24124662 100644 --- a/src/userland/apps/desktop/classic/src/WindowManager.zig +++ b/src/userland/apps/desktop/classic/src/WindowManager.zig @@ -397,13 +397,14 @@ fn window_from_cursor(wm: *WindowManager, point: Point) ?WindowSurface { return null; } -pub fn render(wm: *WindowManager, q: *ashet.graphics.CommandQueue, theme: themes.Theme) !void { +pub const RenderMode = enum { incremental, full }; +pub fn render(wm: *WindowManager, q: *ashet.graphics.CommandQueue, theme: themes.Theme, mode: RenderMode) !void { { var iter = wm.minimized_iterator(); while (iter.next()) |mini| { const window = mini.window; - if (!wm.damage_tracking.is_area_tainted(mini.bounds)) + if (mode != .full and !wm.damage_tracking.is_area_tainted(mini.bounds)) continue; const style = if (window.flags.focus) @@ -433,7 +434,7 @@ pub fn render(wm: *WindowManager, q: *ashet.graphics.CommandQueue, theme: themes const client_rectangle = window.client_rectangle; const window_rectangle = window.screenRectangle(); - if (!wm.damage_tracking.is_area_tainted(window_rectangle)) + if (mode != .full and !wm.damage_tracking.is_area_tainted(window_rectangle)) continue; const style = if (window.flags.focus) @@ -984,9 +985,16 @@ pub fn window_iterator(wm: *WindowManager, filter: WindowIterator.Filter, direct /// will move the window to the top, and unminimizes it. fn move_window_to_top(wm: *WindowManager, window: *Window) void { window.flags.minimized = false; + + if (wm.focused_window != null and wm.focused_window != window) { + wm.damage_tracking.invalidate_region(wm.focused_window.?.screenRectangle()); + } + wm.active_windows.remove(&window.node); wm.active_windows.append(&window.node); wm.focused_window = window; + + wm.damage_tracking.invalidate_region(window.screenRectangle()); } fn top_window(wm: *WindowManager) ?*Window { diff --git a/src/userland/apps/desktop/classic/src/classic-desktop.zig b/src/userland/apps/desktop/classic/src/classic-desktop.zig index b15e305f..8bc57b53 100644 --- a/src/userland/apps/desktop/classic/src/classic-desktop.zig +++ b/src/userland/apps/desktop/classic/src/classic-desktop.zig @@ -144,6 +144,9 @@ pub fn main() !void { std.log.info("classic desktop ready!", .{}); + var incremental_rendering = true; + var damage_rendering = false; + while (true) { const completed = try ashet.overlapped.await_events(.{ .input = &wait_input_event.arc, @@ -158,51 +161,78 @@ pub fn main() !void { if (damage_tracking.is_tainted()) { defer damage_tracking.clear(); - // try render_queue.clear(current_theme.desktop_color); + if (incremental_rendering) { - if (maybe_wallpaper) |wallpaper| { - for (damage_tracking.tainted_regions()) |rect| { - try render_queue.blit_partial_framebuffer(rect, rect.position(), wallpaper); - } - } else { - for (damage_tracking.tainted_regions()) |rect| { - try render_queue.fill_rect(rect, current_theme.desktop_color); - } - } + // try render_queue.clear(current_theme.desktop_color); - // Draw desktop: - { - var iter = apps.iterate(fb_size); + if (maybe_wallpaper) |wallpaper| { + for (damage_tracking.tainted_regions()) |rect| { + try render_queue.blit_partial_framebuffer(rect, rect.position(), wallpaper); + } + } else { + for (damage_tracking.tainted_regions()) |rect| { + try render_queue.fill_rect(rect, current_theme.desktop_color); + } + } + for (damage_tracking.tainted_regions()) |clip_rect| { + try render_queue.set_clip_rect(clip_rect); - icon_iter: while (iter.next()) |desktop_icon| { - const text_size = try ashet.graphics.measure_text_size(default_font, desktop_icon.app.get_display_name()); + // Draw desktop: + { + var iter = apps.iterate(fb_size); - const text_rect: Rectangle = .new(desktop_icon.bounds.corner(.bottom_left).move_by(0, 2), text_size); - const icon_rect = desktop_icon.bounds.grow(2); + while (iter.next()) |desktop_icon| { + const text_size = try ashet.graphics.measure_text_size(default_font, desktop_icon.app.get_display_name()); - // if (!damage_tracking.is_area_tainted(icon_rect.enclosingRegion(text_rect))) - // continue; + const text_rect: Rectangle = .new(desktop_icon.bounds.corner(.bottom_left).move_by(0, 2), text_size); + const icon_rect = desktop_icon.bounds.grow(2); - var icon_obscured = false; - var text_obscured = false; - { - var win_iter = window_manager.window_iterator(WindowManager.WindowIterator.is_regular, .bottom_to_top); - while (win_iter.next()) |window| { - const window_rectangle = window.screenRectangle(); + try render_queue.blit_framebuffer( + desktop_icon.bounds.corner(.top_left), + desktop_icon.icon, + ); - if (!icon_obscured) { - icon_obscured = window_rectangle.containsRectangle(icon_rect); - } - if (!text_obscured) { - text_obscured = window_rectangle.containsRectangle(text_rect); + if (selected_app_icon == desktop_icon.index) { + try render_queue.draw_rect( + icon_rect, + Color.red, + ); + } else { + try render_queue.draw_rect( + icon_rect, + Color.black, + ); } - if (text_obscured and icon_obscured) - continue :icon_iter; + try render_queue.draw_text( + text_rect.position(), + default_font, + Color.black, + desktop_icon.app.get_display_name(), + ); } } - if (!icon_obscured and damage_tracking.is_area_tainted(icon_rect)) { + try window_manager.render(&render_queue, current_theme, .incremental); + } + try render_queue.set_clip_rect(.everything); + } else { + if (maybe_wallpaper) |wallpaper| { + try render_queue.blit_framebuffer(.zero, wallpaper); + } else { + try render_queue.clear(current_theme.desktop_color); + } + + // Draw desktop: + { + var iter = apps.iterate(fb_size); + + while (iter.next()) |desktop_icon| { + const text_size = try ashet.graphics.measure_text_size(default_font, desktop_icon.app.get_display_name()); + + const text_rect: Rectangle = .new(desktop_icon.bounds.corner(.bottom_left).move_by(0, 2), text_size); + const icon_rect = desktop_icon.bounds.grow(2); + try render_queue.blit_framebuffer( desktop_icon.bounds.corner(.top_left), desktop_icon.icon, @@ -219,9 +249,7 @@ pub fn main() !void { Color.black, ); } - } - if (!text_obscured and damage_tracking.is_area_tainted(text_rect)) { try render_queue.draw_text( text_rect.position(), default_font, @@ -230,12 +258,18 @@ pub fn main() !void { ); } } - } - try window_manager.render(&render_queue, current_theme); + try window_manager.render(&render_queue, current_theme, .full); + } try Cursor.paint(&render_queue, cursor.position, Color.black); + if (damage_rendering) { + for (damage_tracking.tainted_regions()) |rect| { + try render_queue.draw_rect(rect, .red); + } + } + try render_queue.submit(video_fb, .{}); } } @@ -268,6 +302,21 @@ pub fn main() !void { cursor.move(motion.dx, motion.dy); }, + .key_press => |key| { + if (key.modifiers.shift and key.usage == .f10) { + incremental_rendering = !incremental_rendering; + damage_tracking.invalidate_screen(); + + logger.info("render mode now {s}", .{if (incremental_rendering) "incremental" else "full"}); + } + if (key.modifiers.shift and key.usage == .f11) { + damage_rendering = !damage_rendering; + damage_tracking.invalidate_screen(); + + logger.info("damage tracking is now {s}", .{if (damage_rendering) "visible" else "hidden"}); + } + }, + else => {}, }