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/justfile b/justfile index fd79be6b..2e8d96d8 100644 --- a/justfile +++ b/justfile @@ -12,9 +12,22 @@ 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" \ + "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 + [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/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/db/abi-id-db.json b/src/abi/db/abi-id-db.json index a12c0545..644601a9 100644 --- a/src/abi/db/abi-id-db.json +++ b/src/abi/db/abi-id-db.json @@ -2443,6 +2443,98 @@ { "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 + }, + { + "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 cf79048e..fbe423c0 100644 --- a/src/abi/src/ashet.abi +++ b/src/abi/src/ashet.abi @@ -381,52 +381,354 @@ 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; } + //? 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. + /// + /// 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. + /// + /// 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. + /// + /// 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. 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. + /// + /// 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; + + /// `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 + /// 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: [*]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` 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; + + /// 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 +1409,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; @@ -1147,19 +1449,21 @@ 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; } - /// 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. /// @@ -1169,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; @@ -1536,8 +1843,6 @@ resource File { } resource Directory { } -resource VideoOutput { } - resource Font { } /// A framebuffer is something that can be drawn on. @@ -1675,13 +1980,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; @@ -3254,25 +3552,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/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/graphics.zig b/src/kernel/components/graphics.zig index f4d21d82..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 { @@ -195,7 +195,10 @@ pub const Framebuffer = struct { pub const Type = union(ashet.abi.FramebufferType) { memory: Bitmap, - video: VideoOut, + video: struct { + mapping: *ashet.video.BufferMapping, + link: ashet.video.BufferMapping.FramebufferLink = .{ .data = {} }, + }, window: *ashet.gui.Window, widget: *ashet.gui.Widget, }; @@ -227,17 +230,25 @@ pub const Framebuffer = struct { } pub fn create_video_output(output: *ashet.video.Output) error{SystemResources}!*Framebuffer { + 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 = .{ - .output = output, - .memory = output.get_video_memory(), + .mapping = mapping, }, }, }; + mapping.add_framebuffer_link(&fb.type.video.link); return fb; } @@ -272,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 => {}, + .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 => |video| video.output.flush(), - .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 => |video| video.output.get_resolution(), + .video => |video| video.mapping.get_resolution(), .window => |win| win.size, .widget => |widget| widget.bounds.size(), }; @@ -314,7 +318,7 @@ pub const Framebuffer = struct { .stride = mem.stride, }, .video => |video| blk: { - const mem = video.output.get_video_memory(); + const mem = video.mapping.get_video_memory(); break :blk .{ .pixels = mem.base, .height = mem.height, @@ -727,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/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..62178e3c 100644 --- a/src/kernel/components/syscalls.zig +++ b/src/kernel/components/syscalls.zig @@ -373,40 +373,39 @@ 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 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()); - pub fn get_palette(output: abi.VideoOutput, palette: *[abi.palette_size]abi.Color) error{InvalidHandle}!void { - _ = output; - _ = palette; - not_implemented_yet(@src()); + 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 set_palette(output: abi.VideoOutput, palette: *const [abi.palette_size]abi.Color) error{ InvalidHandle, Unsupported } { - _ = output; - _ = palette; - 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(); } }; @@ -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); @@ -542,7 +541,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| .{ @@ -551,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, }; } @@ -561,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 1d8e068d..9fb15fb1 100644 --- a/src/kernel/components/video.zig +++ b/src/kernel/components/video.zig @@ -1,61 +1,214 @@ 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.VideoOutputID; +pub const OutputID = ashet.abi.video.VideoOutputID; pub const Resolution = ashet.abi.Size; -pub const VideoMemory = ashet.abi.VideoMemory; +pub const BufferKind = ashet.abi.video.BufferKind; -pub const Buffering = enum { - buffered, - unbuffered, -}; +const Rectangle = ashet.abi.Rectangle; +const PresentMode = ashet.abi.video.PresentMode; pub const DeviceProperties = struct { + /// The video resolution of the device. resolution: Resolution, + + /// 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, + + pub fn supports_buffer(support: MappableBufferSupport, kind: BufferKind) bool { + return switch (support) { + .none => false, + .front_stable => (kind == .front_buffer), + }; + } + }; +}; + +/// 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, - video_memory_mapping: Buffering, - video_memory: []align(ashet.memory.page_size) Color, + comptime { + std.debug.assert(@sizeOf(Color) == 1); + } }; pub const VideoDevice = struct { - flush_fn: *const fn (*ashet.drivers.Driver) void, + 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, + }; + + 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: Go through all drivers and see which actually support this - pub fn flush(vd: *VideoDevice) void { - vd.flush_fn(ashet.drivers.resolveDriver(.video, vd)); - } + begin_write_pixels_fn: *const fn ( + driver: *ashet.drivers.Driver, + call: *ashet.overlapped.AsyncCall, + rectangle: Rectangle, + pixels: []const Color, + stride: usize, + mode: PresentMode, + ) void, + + vblank_fns: ?VBlankFunctions, + mapping_fns: ?MappingFunctions, - pub fn get_properties(vd: *VideoDevice) DeviceProperties { + fn get_properties(vd: *VideoDevice) DeviceProperties { return vd.get_properties_fn(ashet.drivers.resolveDriver(.video, vd)); } - pub fn supports_vblank_event(vd: *VideoDevice) bool { - return vd.get_one_vblank_event_fn != null; + /// Returns true if the video device does support waiting for vertical blanking intervals. + fn supports_vblank_event(vd: *VideoDevice) bool { + return vd.vblank_fns != null; } - 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)); + /// Returns `true` if a vertical blanking interval has happened since the last call. + fn get_one_vblank_event(vd: *VideoDevice) bool { + if (vd.vblank_fns) |*funcs| { + return funcs.get_one_vblank_event_fn(ashet.drivers.resolveDriver(.video, vd)); } else { @panic("invalid API use"); } } + + fn create_mapped_buffer( + vd: *VideoDevice, + buffer: BufferKind, + ) 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 destroy_mapped_buffer( + vd: *VideoDevice, + buffer: BufferKind, + ) 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( + vd: *VideoDevice, + call: *ashet.overlapped.AsyncCall, + rectangle: Rectangle, + pixels: []const Color, + stride: usize, + mode: PresentMode, + ) void { + vd.begin_write_pixels_fn( + ashet.drivers.resolveDriver(.video, vd), + call, + rectangle, + pixels, + stride, + mode, + ); + } + + pub fn default_create_mapped_buffer_front( + driver: *ashet.drivers.Driver, + buffer: BufferKind, + ) error{ SystemResources, Unsupported }!void { + _ = driver; + switch (buffer) { + .front_buffer => {}, + .back_buffer => 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_buffer => return error.Unsupported, + .back_buffer => {}, + } + } + + pub fn default_create_mapped_buffer_both( + driver: *ashet.drivers.Driver, + buffer: BufferKind, + ) error{ SystemResources, IoError, Unsupported }!void { + _ = driver; + _ = buffer; + } + + pub fn destroy_mapped_buffer_noop( + driver: *ashet.drivers.Driver, + buffer: BufferKind, + ) void { + _ = driver; + _ = buffer; + } }; 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 }, + + 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 = false, + 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, @@ -63,43 +216,121 @@ pub const Output = struct { fn _noop(_: *Output) void {} - pub fn get_resolution(output: Output) Resolution { - return output.video_driver.get_properties().resolution; + pub fn get_resolution(output: *const Output) Resolution { + return output.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; + } - /// 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; + 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, + ); } - /// 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; + 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. @@ -111,10 +342,87 @@ pub const Output = struct { } }; -const frame_rate = 1000 / 30; // 30 Hz +pub const BufferMapping = struct { + const FramebufferList = astd.DoublyLinkedList(void, .{ + .tag = struct {}, + .address_pinning = true, // BufferMapping has a stable address + }); + + 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, + + 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 { + 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; + } +}; var video_outputs: []Output = &.{}; -var video_flush_deadline = ashet.time.Deadline.init_abs(.system_start); + +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: { @@ -133,6 +441,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]; @@ -143,49 +452,35 @@ 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(); - } + 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(); } } +} - if (video_flush_deadline.is_reached()) { - video_flush_deadline.move_forward(frame_rate); - flush_all(); +fn get_one_synthetic_vblank_event() bool { + const now = ashet.time.Instant.now(); - 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}); - } + 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 { @@ -194,19 +489,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,7 +511,29 @@ pub fn wait_for_vblank_async(call: *ashet.overlapped.AsyncCall, inputs: ashet.ab output.vsync_awaiters.enqueue(call, null); } -pub fn load_splash_screen(vmem: VideoMemory) void { +pub fn write_pixels_async(call: *ashet.overlapped.AsyncCall, inputs: ashet.abi.video.WritePixels.Inputs) void { + 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 { + _ = call; + _ = inputs; + @panic("TODO: present_async!"); +} + +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); @@ -240,7 +555,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, @@ -250,3 +565,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: [*]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; + 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/drivers.zig b/src/kernel/drivers/drivers.zig index 2a084fca..e95c4ec4 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"); @@ -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 { @@ -70,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 new file mode 100644 index 00000000..db7aa8fa --- /dev/null +++ b/src/kernel/drivers/video/AVAPv1_Framebuffer.zig @@ -0,0 +1,423 @@ +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, + .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 + }, + }, +}, + +device: std.fs.File, + +pub fn init( + file_name: []const u8, +) error{ FileNotFound, BadFile, DeviceUnresponsive, IoError }!AVAPv1_Framebuffer { + var fb: AVAPv1_Framebuffer = .{ + .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, + + 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; + } + + // 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 { + _ = driver; + // const vd = driver.resolve(AVAPv1_Framebuffer, "driver"); + return .{ + .resolution = .{ + .width = width, + .height = height, + }, + .buffer_support = .none, + }; +} + +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.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 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({f}, {})", .{ rectangle, pixels.len }); + try write_rectangle(vd.device, rectangle, pixels, stride); + + // logger.debug("swap buffers", .{}); + switch (mode) { + .dont_care => {}, + .immediate, .vblank => 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, .log); + 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, .write_buffer); + + 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, .log); + try read_footer(port, header, deadline); + + 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_rectangle); + + 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 { + 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, .log); + try read_footer(port, header, deadline); + + 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 { + logger.debug("=> cmd({t}, {} bytes)", .{ cmd, buffer.len }); + + 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 { + logger.debug("read cmd...", .{}); + var buffer: [4]u8 = undefined; + try read_all(port, &buffer, deadline); + 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 { + const overhead = compute_padding(response.length); + try read_discarding(port, overhead, deadline, .ignore); +} + +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; + while (count < length) { + try deadline.check(); + + 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} (\"{f}\")", .{ + buffer[0..len], + std.zig.fmtString(buffer[0..len]), + }); + } + + 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/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/kernel/drivers/video/Externally_Managed_Output.zig b/src/kernel/drivers/video/Externally_Managed_Output.zig new file mode 100644 index 00000000..85d2bc6a --- /dev/null +++ b/src/kernel/drivers/video/Externally_Managed_Output.zig @@ -0,0 +1,156 @@ +//! +//! 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_ctx: ?*anyopaque, + +had_vblank_event: std.atomic.Value(bool) = .init(false), + +pub fn init( + comptime name: []const u8, + width: u16, + height: u16, + 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)), + .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, + + .vblank_fns = if (supports_vblank_await) + .{ + .get_one_vblank_event_fn = get_one_vblank_event, + } + else + null, + + .mapping_fns = null, + }, + }, + }, + + .width = width, + .height = height, + .backbuffer = fb, + + .write_pixels_fn = &write_pixels_fn, + .write_pixels_ctx = write_pixels_ctx, + }; +} + +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 .{ + .resolution = .{ + .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, + 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/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/Host_VNC_Output.zig b/src/kernel/drivers/video/Host_VNC_Output.zig deleted file mode 100644 index 1be5cce3..00000000 --- a/src/kernel/drivers/video/Host_VNC_Output.zig +++ /dev/null @@ -1,77 +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, -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, - }, - }, -}, - -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), - ); - 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, - }; -} - -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, - }, - }; -} - -fn flush(driver: *Driver) void { - const vd: *Host_VNC_Output = @fieldParentPtr("driver", driver); - - // vd.backbuffer_lock.lock(); - // defer vd.backbuffer_lock.unlock(); - - @memcpy(vd.backbuffer, vd.frontbuffer); - vd.backbuffer_dirty = true; - - vd.vnc_server().notify_flush(); -} - -fn vnc_server(output: *Host_VNC_Output) *VNC_Server { - return @fieldParentPtr("screen", output); -} diff --git a/src/kernel/drivers/video/Memory_Mapped_Framebuffer.zig b/src/kernel/drivers/video/Memory_Mapped_Framebuffer.zig index 9db0f302..bd70d798 100644 --- a/src/kernel/drivers/video/Memory_Mapped_Framebuffer.zig +++ b/src/kernel/drivers/video/Memory_Mapped_Framebuffer.zig @@ -16,21 +16,14 @@ 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, -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; - 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, driver_name: []const u8, config: Con 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{ @@ -47,18 +40,14 @@ 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, + .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,23 +55,83 @@ pub fn create(allocator: std.mem.Allocator, driver_name: []const u8, config: Con @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; } +fn get_properties(driver: *Driver) ashet.video.DeviceProperties { + const vd: *Memory_Mapped_Framebuffer = @fieldParentPtr("driver", driver); + return .{ + .resolution = .{ + .width = vd.framebuffer.width, + .height = vd.framebuffer.height, + }, + + .buffer_support = .none, + }; +} + +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.framebuffer.width, + .height = vd.framebuffer.height, + .stride = vd.framebuffer.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 => {}, // 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, }; @@ -105,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 @@ -127,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), }; @@ -173,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), }; @@ -188,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; @@ -199,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; } } } @@ -241,16 +293,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..ec2d118c 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(); @@ -13,17 +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, - .flush_fn = flush, - }, - }, -}, +driver: Driver, const memory_ranges = [_]x86.vmm.Range{ .{ .base = 0xA0000, .length = 0x20000 }, @@ -38,11 +29,13 @@ pub fn init(vga: *VGA) !void { x86.vmm.update(range, .read_write); } - vga.* = VGA{}; + writeVgaRegisters(g_320x200x256); - writeVgaRegisters(modes.g_320x200x256); + loadFixedPalette(); - vga.loadFixedPalette(); + setupVBlankIrq(); + + const vblank_irq_support = test_blank_irq(); const vmem = @as([*]align(ashet.memory.page_size) Color, @ptrFromInt(0xA0000))[0 .. width * height]; @@ -54,99 +47,261 @@ pub fn init(vga: *VGA) !void { .height = height, .stride = width, }); + + vga.* = VGA{ + .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, + }, + }, + }, + }, + }, + }; } 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, + .buffer_support = .front_stable, }; } -fn flush(driver: *Driver) void { +fn get_one_vblank_event(driver: *Driver) bool { const vd: *VGA = @alignCast(@fieldParentPtr("driver", driver)); + _ = vd; + return readAndResetIrq(); +} - // vd.loadPalette(vd.palette); +fn get_mapped_buffer(driver: *Driver, buffer: ashet.video.BufferKind) ashet.video.VideoMemory { + _ = driver; + return switch (buffer) { + .front_buffer => .{ + .base = @ptrFromInt(0xA0000), + .stride = width, + }, + .back_buffer => @panic("kernel bug: driver layer invoked get_mapped_buffer for unsupported buffer"), + }; +} + +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; const target = @as([*]align(ashet.memory.page_size) Color, @ptrFromInt(0xA0000))[0 .. width * height]; - std.mem.copyForwards(Color, target, &vd.backbuffer); + + 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, + .{ + .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, + ); + + 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(); - // make sure they remain unlocked - // TODO: Reinsert again - // regs[0x03] |= 0x80; - // regs[0x11] &= ~0x80; + // Bit 4 = 0 clears the interrupt latch. + crtc_data.write(value & ~@as(u8, 0x10)); - // write CRTC regs + // Bit 4 = 1 rearms it for the next vertical retrace. + crtc_data.write(value | 0x10); - 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; + return true; +} + +fn writeVgaRegisters(config: VgaRegisterConfig) void { + // Write MISCELLANEOUS register. + config.miscellaneous_output.write(); + + const io_address_select = config.miscellaneous_output.io_address_select; + const crtc_index = io_address_select.crtcIndexPort(); + const crtc_data = io_address_select.crtcDataPort(); + + // 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); } - // lock 16-color palette and unblank display - _ = x86.in(u8, VGA_INSTAT_READ); - x86.out(u8, VGA_AC_INDEX, 0x20); + + // 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. + _ = 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), @@ -154,9 +309,6 @@ fn getFramebufferSegment() [*]volatile u8 { })); } -const PALETTE_INDEX = 0x03c8; -const PALETTE_DATA = 0x03c9; - const RGB = packed struct { b: u8, g: u8, @@ -168,146 +320,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/Virtio_GPU_Device.zig b/src/kernel/drivers/video/Virtio_GPU_Device.zig index 10e11150..f0553a69 100644 --- a/src/kernel/drivers/video/Virtio_GPU_Device.zig +++ b/src/kernel/drivers/video/Virtio_GPU_Device.zig @@ -15,16 +15,18 @@ 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, + .vblank_fns = null, // VirtIO does not support vertical blanking events + .mapping_fns = null, // VirtIO does not support memory mappings of 8bpp graphics }, }, }, @@ -36,13 +38,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 +54,8 @@ pub fn init(allocator: std.mem.Allocator, index: usize, regs: *volatile virtio.C .stride = vd.graphics_width, }); - vd.driver.class.video.flush(); + logger.info("write initial flush", .{}); + vd.flush(); return vd; } @@ -77,6 +80,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 +130,7 @@ 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, + .buffer_support = .none, }; } @@ -101,9 +145,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(); @@ -198,6 +240,7 @@ const GPU = struct { return; } + logger.debug("initialize gpu.vq", .{}); try gpu.vq.init(0, regs); // try cursor_vq.init(1, regs); @@ -211,6 +254,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; @@ -229,6 +273,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/drivers/video/Virtual_Video_Output.zig b/src/kernel/drivers/video/Virtual_Video_Output.zig index 51716636..2aac18b7 100644 --- a/src/kernel/drivers/video/Virtual_Video_Output.zig +++ b/src/kernel/drivers/video/Virtual_Video_Output.zig @@ -7,38 +7,53 @@ 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, + .vblank_fns = null, + .mapping_fns = null, }, }, }, +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, }, + .buffer_support = .none, }; } -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/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/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()); + } +}; 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/VNC_Server.zig b/src/kernel/port/hosted/VNC_Server.zig index f6c28ef4..b1410b93 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,15 @@ 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( + "VNC Output", + width, + height, + write_vnc_pixels, + server, + .allocate, + true, + ), .input = ashet.drivers.input.Host_VNC_Input.init(), }; @@ -94,10 +102,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 +239,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 +290,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) { @@ -362,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/hosted/initialize.zig b/src/kernel/port/hosted/initialize.zig index 8bbb1a74..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); @@ -60,6 +61,7 @@ pub fn initialize(comptime video_drivers: std.StaticStringMap(VideoDriverCtor)) "dummy", "vnc", "sdl", + "avap-v1", }; comptime for (shared_video_drivers) |dri| { @@ -109,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", .{}); @@ -154,9 +179,23 @@ 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!", .{}); + + 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(.{ @@ -168,6 +207,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", .{}); 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..fbdc379e 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, @@ -67,7 +67,15 @@ 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? + true, + ), // .input = ashet.drivers.input.Host_SDL_Input.init(), .window_width = @max(1, initial_scale) * width, @@ -85,8 +93,7 @@ pub fn init( .swap_chain = undefined, }; - @memset(server.screen.frontbuffer, ashet.abi.Color.blue); - @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, @@ -160,6 +167,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.?)); @@ -203,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}); @@ -288,7 +314,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 66754aa5..25182d10 100644 --- a/src/kernel/port/machine/x86/hosted-linux/X11_Display.zig +++ b/src/kernel/port/machine/x86/hosted-linux/X11_Display.zig @@ -38,8 +38,10 @@ running: bool = true, put_image_msg_buffer: []align(4) u8, put_image_chunk_height: u16, +screen_dirty: bool = true, + // 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( @@ -98,7 +100,15 @@ 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, + false, + ), // .input = ashet.drivers.input.Host_SDL_Input.init(), .socket_read_buffer = socket_read_buffer, @@ -123,8 +133,7 @@ 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); + @memset(server.screen.backbuffer.?, ashet.abi.Color.red); const base_resource = server.setup.resource_id_base; @@ -177,6 +186,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.?)); @@ -375,20 +403,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 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); 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 { 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, + }; +}; 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/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 3696ab5b..8bc57b53 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(); @@ -161,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, @@ -175,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, @@ -236,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, @@ -247,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, .{}); } } @@ -285,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 => {}, } 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..38b95826 100644 --- a/src/userland/libs/libAshetOS/module/src/libashet/video.zig +++ b/src/userland/libs/libAshetOS/module/src/libashet/video.zig @@ -4,23 +4,45 @@ 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 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: abi.VideoOutputID) !*Output { +pub fn acquire(id: VideoOutputID) !*Output { return @ptrCast( try abi.video.acquire(id), );