pkg/vulkan: promote Device/Sampler/CommandPool/DescriptorPool
Mirrors how pkg/opengl/ houses the OpenGL Buffer/Program/Texture/etc.
typed wrappers consumed by src/renderer/OpenGL.zig. Renderer-policy
files (Target, Texture, buffer, Pipeline, RenderPass, Frame, shaders)
stay under src/renderer/vulkan/ — same split the OpenGL backend uses.
Decoupling Device from the apprt is what makes this move possible:
- Device.zig drops `platform: apprt.embedded.Platform.Vulkan`.
- Device.init now takes a neutral `HostBootstrap` (raw handles +
the root proc-addr resolver), so pkg/vulkan/ stays free of
libghostty's apprt types.
- Vulkan.zig's `bootstrapFromPlatform` translates the apprt
callbacks into HostBootstrap at the libghostty boundary.
- Target.Options.platform becomes non-optional. The smoke-test
code that justified the optional was deleted in 1427f658a;
its removal here closes a dead fallback (`self.device.platform`)
that would also have stopped working once Device.platform went
away.
Verified via Docker (debian:bookworm-slim + zig 0.15.2 linux-arm64):
zig build -Drenderer=vulkan -Dapp-runtime=none → clean
zig build -Drenderer=opengl -Dapp-runtime=none → clean
Step 1 of 6 in the PR-17 review refactor (slim Vulkan.zig, decouple
shadertoy, etc., to follow).
Co-Authored-By: claude-flow <ruv@ruv.net>
pull/12846/head
parent
2ddf143c15
commit
3ec5f35bd7
|
|
@ -14,7 +14,7 @@
|
|||
const Self = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const vk = @import("vulkan").c;
|
||||
const vk = @import("c.zig").c;
|
||||
|
||||
const Device = @import("Device.zig");
|
||||
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
const Self = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const vk = @import("vulkan").c;
|
||||
const vk = @import("c.zig").c;
|
||||
|
||||
const Device = @import("Device.zig");
|
||||
|
||||
|
|
@ -34,8 +34,7 @@
|
|||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const apprt = @import("../../apprt.zig");
|
||||
const vk = @import("vulkan").c;
|
||||
const vk = @import("c.zig").c;
|
||||
|
||||
const log = std.log.scoped(.vulkan);
|
||||
|
||||
|
|
@ -203,11 +202,6 @@ pub const Dispatch = struct {
|
|||
|
||||
// ---- fields ---------------------------------------------------------
|
||||
|
||||
/// The callbacks the apprt handed us. Held by value (not pointer)
|
||||
/// because the apprt's `Platform.Vulkan` is itself stored by value
|
||||
/// inside the `Surface`.
|
||||
platform: apprt.embedded.Platform.Vulkan,
|
||||
|
||||
instance: vk.VkInstance,
|
||||
physical_device: vk.VkPhysicalDevice,
|
||||
device: vk.VkDevice,
|
||||
|
|
@ -260,14 +254,28 @@ pub fn queueWaitIdle(self: *const Device) vk.VkResult {
|
|||
|
||||
// ---- API ------------------------------------------------------------
|
||||
|
||||
/// Build a `Device` from the host's platform callbacks. Performs:
|
||||
/// 1. Pull host handles via the callbacks. Any null returns ->
|
||||
/// `error.HostHandleMissing`.
|
||||
/// 2. Load the instance-level dispatch via `vkGetInstanceProcAddr`.
|
||||
/// 3. Verify `physicalDeviceProperties.apiVersion >= 1.3`.
|
||||
/// 4. Verify every entry in `REQUIRED_DEVICE_EXTENSIONS` is present
|
||||
/// Pre-resolved host-Vulkan handles passed into `Device.init`. Keeps
|
||||
/// `pkg/vulkan` independent of any apprt type — callers (e.g.
|
||||
/// libghostty's `src/renderer/Vulkan.zig`) translate their own
|
||||
/// platform-callback struct into this neutral shape.
|
||||
pub const HostBootstrap = struct {
|
||||
instance: vk.VkInstance,
|
||||
physical_device: vk.VkPhysicalDevice,
|
||||
device: vk.VkDevice,
|
||||
queue: vk.VkQueue,
|
||||
queue_family_index: u32,
|
||||
/// Root proc-addr resolver. `Device.init` uses this to pull
|
||||
/// `vkGetInstanceProcAddr` itself plus every instance-level
|
||||
/// function it needs to bootstrap the dispatch table.
|
||||
get_instance_proc_addr_raw: *const anyopaque,
|
||||
};
|
||||
|
||||
/// Build a `Device` from pre-resolved host handles. Performs:
|
||||
/// 1. Load the instance-level dispatch via `vkGetInstanceProcAddr`.
|
||||
/// 2. Verify `physicalDeviceProperties.apiVersion >= 1.3`.
|
||||
/// 3. Verify every entry in `REQUIRED_DEVICE_EXTENSIONS` is present
|
||||
/// on the physical device.
|
||||
/// 5. Load the device-level dispatch via `vkGetDeviceProcAddr`.
|
||||
/// 4. Load the device-level dispatch via `vkGetDeviceProcAddr`.
|
||||
///
|
||||
/// On success the returned `Device` is ready for the renderer to
|
||||
/// build pipelines / images / command buffers against. The host
|
||||
|
|
@ -275,38 +283,23 @@ pub fn queueWaitIdle(self: *const Device) vk.VkResult {
|
|||
/// is a no-op stub for symmetry.
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
platform: apprt.embedded.Platform.Vulkan,
|
||||
boot: HostBootstrap,
|
||||
) (Error || Allocator.Error)!Device {
|
||||
// ---- 1. resolve host handles ---------------------------------
|
||||
const instance_handle = platform.instance(platform.userdata) orelse
|
||||
return error.HostHandleMissing;
|
||||
const physical_device_handle = platform.physical_device(platform.userdata) orelse
|
||||
return error.HostHandleMissing;
|
||||
const device_handle = platform.device(platform.userdata) orelse
|
||||
return error.HostHandleMissing;
|
||||
const queue_handle = platform.queue(platform.userdata) orelse
|
||||
return error.HostHandleMissing;
|
||||
const instance = boot.instance;
|
||||
const physical_device = boot.physical_device;
|
||||
const device = boot.device;
|
||||
const queue = boot.queue;
|
||||
const queue_family_index = boot.queue_family_index;
|
||||
|
||||
const instance: vk.VkInstance = @ptrCast(instance_handle);
|
||||
const physical_device: vk.VkPhysicalDevice = @ptrCast(physical_device_handle);
|
||||
const device: vk.VkDevice = @ptrCast(device_handle);
|
||||
const queue: vk.VkQueue = @ptrCast(queue_handle);
|
||||
const queue_family_index = platform.queue_family_index(platform.userdata);
|
||||
|
||||
// ---- 2. instance-level dispatch ------------------------------
|
||||
// The host's get_instance_proc_addr is our root entry point. We
|
||||
// resolve other functions via vkGetInstanceProcAddr (instance,
|
||||
// name); per the Vulkan spec, passing a non-null instance is
|
||||
// valid for any function that takes an instance, physical
|
||||
// device, device, or child object of any of these — i.e.
|
||||
// ---- instance-level dispatch ---------------------------------
|
||||
// The caller-provided get_instance_proc_addr is our root entry
|
||||
// point. We resolve other functions via vkGetInstanceProcAddr
|
||||
// (instance, name); per the Vulkan spec, passing a non-null
|
||||
// instance is valid for any function that takes an instance,
|
||||
// physical device, device, or child object of any of these — i.e.
|
||||
// everything we care about.
|
||||
const get_instance_proc_addr_raw =
|
||||
platform.get_instance_proc_addr(
|
||||
platform.userdata,
|
||||
"vkGetInstanceProcAddr",
|
||||
) orelse return error.HostHandleMissing;
|
||||
const get_instance_proc_addr: std.meta.Child(vk.PFN_vkGetInstanceProcAddr) =
|
||||
@ptrCast(@alignCast(get_instance_proc_addr_raw));
|
||||
@ptrCast(@alignCast(boot.get_instance_proc_addr_raw));
|
||||
|
||||
const InstanceLoader = struct {
|
||||
instance: vk.VkInstance,
|
||||
|
|
@ -338,7 +331,7 @@ pub fn init(
|
|||
const get_device_proc_addr =
|
||||
try il.load(vk.PFN_vkGetDeviceProcAddr, "vkGetDeviceProcAddr");
|
||||
|
||||
// ---- 3. version check ----------------------------------------
|
||||
// ---- version check ------------------------------------------
|
||||
var props: vk.VkPhysicalDeviceProperties = std.mem.zeroes(vk.VkPhysicalDeviceProperties);
|
||||
get_physical_device_properties(physical_device, &props);
|
||||
if (props.apiVersion < MIN_API_VERSION) {
|
||||
|
|
@ -356,7 +349,7 @@ pub fn init(
|
|||
return error.UnsupportedVulkanVersion;
|
||||
}
|
||||
|
||||
// ---- 4. extension check --------------------------------------
|
||||
// ---- extension check ----------------------------------------
|
||||
var ext_count: u32 = 0;
|
||||
{
|
||||
const r = enumerate_device_extension_properties(physical_device, null, &ext_count, null);
|
||||
|
|
@ -409,7 +402,7 @@ pub fn init(
|
|||
}
|
||||
}
|
||||
|
||||
// ---- 5. device-level dispatch --------------------------------
|
||||
// ---- device-level dispatch ----------------------------------
|
||||
const DeviceLoader = struct {
|
||||
device: vk.VkDevice,
|
||||
get_device_proc_addr: std.meta.Child(vk.PFN_vkGetDeviceProcAddr),
|
||||
|
|
@ -555,7 +548,6 @@ pub fn init(
|
|||
get_physical_device_memory_properties(physical_device, &memory_properties);
|
||||
|
||||
return .{
|
||||
.platform = platform,
|
||||
.instance = instance,
|
||||
.physical_device = physical_device,
|
||||
.device = device,
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
const Self = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const vk = @import("vulkan").c;
|
||||
const vk = @import("c.zig").c;
|
||||
|
||||
const Device = @import("Device.zig");
|
||||
|
||||
|
|
@ -1,7 +1,30 @@
|
|||
//! Vulkan loader bindings.
|
||||
//! Vulkan bindings.
|
||||
//!
|
||||
//! Lightweight `@cImport` wrapper around the system Vulkan headers,
|
||||
//! shaped after `pkg/opengl/`. `c` is the raw C API; higher-level
|
||||
//! Zig helpers go alongside as the renderer needs them.
|
||||
//! Shaped after `pkg/opengl/`: `c` is the raw C API (a thin `@cImport`
|
||||
//! wrapper around the system Vulkan headers); the per-resource files
|
||||
//! alongside provide opinionated typed wrappers the renderer
|
||||
//! consumes as primitives.
|
||||
//!
|
||||
//! The Vulkan renderer in `src/renderer/vulkan/` builds renderer
|
||||
//! policy on top of these (Pipeline / RenderPass / Frame / Target
|
||||
//! etc.); anything that's pure Vulkan-API plumbing belongs here.
|
||||
//!
|
||||
//! Vulkan core API + the dmabuf-related extensions the renderer relies
|
||||
//! on for zero-copy presentation:
|
||||
//!
|
||||
//! - VK_KHR_external_memory / VK_KHR_external_memory_fd
|
||||
//! - VK_EXT_external_memory_dma_buf
|
||||
//! - VK_EXT_image_drm_format_modifier
|
||||
//!
|
||||
//! VK_USE_PLATFORM_* macros are intentionally NOT set in `c.zig` —
|
||||
//! libghostty talks to its host purely via dmabuf fds (handed back to
|
||||
//! the apprt's `ghostty_platform_vulkan_s.present` callback), so it
|
||||
//! never sees a `wl_display` or `xcb_connection`. That keeps the
|
||||
//! binding portable and lets the host (Qt RHI) do all the
|
||||
//! platform-specific compositing.
|
||||
|
||||
pub const c = @import("c.zig").c;
|
||||
pub const Device = @import("Device.zig");
|
||||
pub const Sampler = @import("Sampler.zig");
|
||||
pub const CommandPool = @import("CommandPool.zig");
|
||||
pub const DescriptorPool = @import("DescriptorPool.zig");
|
||||
|
|
|
|||
|
|
@ -11,26 +11,35 @@
|
|||
//! `Frame.complete` waits on the fence before handing the fd to
|
||||
//! the platform `present` callback.
|
||||
//!
|
||||
//! Submodules:
|
||||
//! - `vulkan/Device.zig` — host-handle wrapper, dispatch table.
|
||||
//! - `vulkan/Sampler.zig` — VkSampler.
|
||||
//! - `vulkan/Texture.zig` — VkImage + memory + view + staging upload.
|
||||
//! - `vulkan/Target.zig` — dmabuf-exportable render target
|
||||
//! (direct or legacy_copy mode).
|
||||
//! - `vulkan/buffer.zig` — Buffer(T) host-coherent.
|
||||
//! - `vulkan/CommandPool.zig` — VkCommandPool + one-shot helper.
|
||||
//! - `vulkan/Pipeline.zig` — VkPipeline + layout (dynamic rendering).
|
||||
//! - `vulkan/RenderPass.zig` — dynamic-rendering pass + step recorder.
|
||||
//! - `vulkan/Frame.zig` — per-draw context (fence-paced).
|
||||
//! - `vulkan/shaders.zig` — GLSL→SPIR-V→VkShaderModule + the
|
||||
//! OpenGL-GLSL → Vulkan-GLSL rewriter.
|
||||
//! Submodules — pure Vulkan-API wrappers live in `pkg/vulkan/`
|
||||
//! (mirror of `pkg/opengl/`); renderer-policy modules live alongside
|
||||
//! this file under `vulkan/`.
|
||||
//!
|
||||
//! In `pkg/vulkan/` (re-exported from this file as
|
||||
//! `Vulkan.{Device,Sampler,CommandPool,DescriptorPool}`):
|
||||
//! - `Device.zig` — host-handle wrapper + dispatch table.
|
||||
//! - `Sampler.zig` — VkSampler.
|
||||
//! - `CommandPool.zig` — VkCommandPool + one-shot helper.
|
||||
//! - `DescriptorPool.zig`— per-frame descriptor pool.
|
||||
//!
|
||||
//! In `src/renderer/vulkan/`:
|
||||
//! - `Texture.zig` — VkImage + memory + view + staging upload.
|
||||
//! - `Target.zig` — dmabuf-exportable render target
|
||||
//! (direct or legacy_copy mode).
|
||||
//! - `buffer.zig` — Buffer(T) host-coherent + recycle pool.
|
||||
//! - `Pipeline.zig` — VkPipeline + layout (dynamic rendering).
|
||||
//! - `RenderPass.zig` — dynamic-rendering pass + step recorder.
|
||||
//! - `Frame.zig` — per-draw context (fence-paced).
|
||||
//! - `shaders.zig` — GLSL→SPIR-V→VkShaderModule + the
|
||||
//! OpenGL-GLSL → Vulkan-GLSL rewriter.
|
||||
|
||||
pub const Vulkan = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const vk = @import("vulkan").c;
|
||||
const vulkan = @import("vulkan");
|
||||
const vk = vulkan.c;
|
||||
|
||||
const apprt = @import("../apprt.zig");
|
||||
const configpkg = @import("../config.zig");
|
||||
|
|
@ -39,15 +48,24 @@ const rendererpkg = @import("../renderer.zig");
|
|||
const shadertoy = @import("shadertoy.zig");
|
||||
|
||||
pub const GraphicsAPI = Vulkan;
|
||||
pub const Device = @import("vulkan/Device.zig");
|
||||
pub const Sampler = @import("vulkan/Sampler.zig");
|
||||
// Device-dispatch primitives live in `pkg/vulkan/` so they can be
|
||||
// reused by anything that needs a typed Vulkan binding (mirrors how
|
||||
// `pkg/opengl/` houses Buffer/Program/Texture/etc.). The renderer
|
||||
// re-exports them from this top-level so call sites continue to write
|
||||
// `Vulkan.Device`, `Vulkan.Sampler`, etc.
|
||||
pub const Device = vulkan.Device;
|
||||
pub const Sampler = vulkan.Sampler;
|
||||
pub const CommandPool = vulkan.CommandPool;
|
||||
pub const DescriptorPool = vulkan.DescriptorPool;
|
||||
|
||||
// Renderer-policy primitives stay in `src/renderer/vulkan/` (dmabuf
|
||||
// export, our pipeline + render-pass wiring, frame fence pacing, the
|
||||
// GLSL→SPIR-V loader).
|
||||
pub const Texture = @import("vulkan/Texture.zig");
|
||||
pub const Target = @import("vulkan/Target.zig");
|
||||
pub const CommandPool = @import("vulkan/CommandPool.zig");
|
||||
pub const Pipeline = @import("vulkan/Pipeline.zig");
|
||||
pub const RenderPass = @import("vulkan/RenderPass.zig");
|
||||
pub const Frame = @import("vulkan/Frame.zig");
|
||||
pub const DescriptorPool = @import("vulkan/DescriptorPool.zig");
|
||||
pub const shaders = @import("vulkan/shaders.zig");
|
||||
|
||||
const bufferpkg = @import("vulkan/buffer.zig");
|
||||
|
|
@ -370,7 +388,7 @@ pub fn init(alloc: Allocator, opts: rendererpkg.Options) !Vulkan {
|
|||
else => @compileError("unsupported app runtime for Vulkan (embedded-only)"),
|
||||
apprt.embedded => switch (opts.rt_surface.platform) {
|
||||
.vulkan => |platform| {
|
||||
device = try Device.init(alloc, platform);
|
||||
device = try Device.init(alloc, try bootstrapFromPlatform(platform));
|
||||
log.info(
|
||||
"Vulkan device ready (api=0x{x})",
|
||||
.{device.?.api_version},
|
||||
|
|
@ -547,11 +565,13 @@ pub fn initTarget(self: *const Vulkan, width: usize, height: usize) !Target {
|
|||
// concern only.
|
||||
//
|
||||
// Per-surface platform: pulled from rt_surface so the `present`
|
||||
// callback's `userdata` points at THIS surface's window. The
|
||||
// process-global Device has its own `platform` copy from
|
||||
// whichever surface first initialized it; splits and tabs would
|
||||
// otherwise route their dmabuf frames to the wrong window.
|
||||
const platform = surfacePlatform(self.rt_surface);
|
||||
// callback's `userdata` points at THIS surface's window. Splits
|
||||
// and tabs share the process-wide Device but each owns its own
|
||||
// platform copy — without per-surface routing here, all dmabuf
|
||||
// frames would funnel through whichever surface initialized the
|
||||
// device first.
|
||||
const platform = surfacePlatform(self.rt_surface) orelse
|
||||
return error.UnsupportedPlatform;
|
||||
return try Target.init(.{
|
||||
.device = devicePtr(),
|
||||
.format = vk.VK_FORMAT_B8G8R8A8_SRGB,
|
||||
|
|
@ -561,9 +581,44 @@ pub fn initTarget(self: *const Vulkan, width: usize, height: usize) !Target {
|
|||
});
|
||||
}
|
||||
|
||||
/// Translate the apprt's `Platform.Vulkan` callback struct into the
|
||||
/// neutral `Device.HostBootstrap` the binding expects. Resolves the
|
||||
/// host's handles + the root proc-addr resolver up-front so the
|
||||
/// binding stays free of any apprt type. Any null host handle ->
|
||||
/// `error.HostHandleMissing`.
|
||||
fn bootstrapFromPlatform(
|
||||
platform: apprt.embedded.Platform.Vulkan,
|
||||
) Device.Error!Device.HostBootstrap {
|
||||
const instance_handle = platform.instance(platform.userdata) orelse
|
||||
return error.HostHandleMissing;
|
||||
const physical_device_handle = platform.physical_device(platform.userdata) orelse
|
||||
return error.HostHandleMissing;
|
||||
const device_handle = platform.device(platform.userdata) orelse
|
||||
return error.HostHandleMissing;
|
||||
const queue_handle = platform.queue(platform.userdata) orelse
|
||||
return error.HostHandleMissing;
|
||||
const get_instance_proc_addr_raw = platform.get_instance_proc_addr(
|
||||
platform.userdata,
|
||||
"vkGetInstanceProcAddr",
|
||||
) orelse return error.HostHandleMissing;
|
||||
|
||||
return .{
|
||||
.instance = @ptrCast(instance_handle),
|
||||
.physical_device = @ptrCast(physical_device_handle),
|
||||
.device = @ptrCast(device_handle),
|
||||
.queue = @ptrCast(queue_handle),
|
||||
.queue_family_index = platform.queue_family_index(platform.userdata),
|
||||
.get_instance_proc_addr_raw = get_instance_proc_addr_raw,
|
||||
};
|
||||
}
|
||||
|
||||
/// Extract the Vulkan platform callbacks from a surface, when the
|
||||
/// surface was created with the Vulkan platform tag. Returns null
|
||||
/// otherwise (smoke test / OpenGL surfaces).
|
||||
/// when the surface was tagged with a non-Vulkan platform — the
|
||||
/// caller is expected to reject the surface with
|
||||
/// `error.UnsupportedPlatform`. (`Vulkan.init` already does the same
|
||||
/// reject up-front, so reaching this function with a non-Vulkan
|
||||
/// platform implies a surface plumbed through after that gate.)
|
||||
fn surfacePlatform(rt_surface: *apprt.Surface) ?apprt.embedded.Platform.Vulkan {
|
||||
// `init()` already gates non-embedded runtimes with a
|
||||
// `@compileError`, so reaching this function on anything other
|
||||
|
|
@ -867,4 +922,3 @@ pub fn initAtlasTexture(
|
|||
null,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,11 +33,12 @@
|
|||
const Self = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const vk = @import("vulkan").c;
|
||||
const vulkan = @import("vulkan");
|
||||
const vk = vulkan.c;
|
||||
|
||||
const Device = @import("Device.zig");
|
||||
const Device = vulkan.Device;
|
||||
const DescriptorPool = vulkan.DescriptorPool;
|
||||
const Target = @import("Target.zig");
|
||||
const DescriptorPool = @import("DescriptorPool.zig");
|
||||
const RenderPass = @import("RenderPass.zig");
|
||||
|
||||
const Vulkan = @import("../Vulkan.zig");
|
||||
|
|
|
|||
|
|
@ -22,10 +22,11 @@
|
|||
const Self = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const vk = @import("vulkan").c;
|
||||
const vulkan = @import("vulkan");
|
||||
const vk = vulkan.c;
|
||||
|
||||
const Device = @import("Device.zig");
|
||||
const DescriptorPool = @import("DescriptorPool.zig");
|
||||
const Device = vulkan.Device;
|
||||
const DescriptorPool = vulkan.DescriptorPool;
|
||||
|
||||
const log = std.log.scoped(.vulkan);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,37 +1,39 @@
|
|||
# Vulkan renderer backend (fork-only, in progress)
|
||||
# Vulkan renderer backend
|
||||
|
||||
This directory will hold the Vulkan analogues of the per-backend
|
||||
files that live in `../opengl/` and `../metal/`:
|
||||
This directory holds the **renderer-policy** Vulkan files for libghostty.
|
||||
Pure Vulkan-API wrappers (Device dispatch table, Sampler, CommandPool,
|
||||
DescriptorPool) live in `pkg/vulkan/`, mirroring how `pkg/opengl/`
|
||||
relates to `src/renderer/opengl/`.
|
||||
|
||||
| File | Counterpart in `../opengl/` | Notes |
|
||||
| -------------- | ----------------------------------- | ------------------------------------------------------------------ |
|
||||
| `buffer.zig` | `opengl/buffer.zig` | Vertex / uniform buffers backed by `VkBuffer` + `VkDeviceMemory`. |
|
||||
| `Pipeline.zig` | `opengl/Pipeline.zig` | Graphics pipeline + descriptor set layout creation. |
|
||||
| `RenderPass.zig` | `opengl/RenderPass.zig` | `VkRenderPass` + framebuffer setup for the cell-bg / text passes. |
|
||||
| `Sampler.zig` | `opengl/Sampler.zig` | `VkSampler` (linear for atlases, nearest for cells). |
|
||||
| `Target.zig` | `opengl/Target.zig` | Render target image + view (exportable for dmabuf handoff). |
|
||||
| `Texture.zig` | `opengl/Texture.zig` | `VkImage` + `VkImageView` + upload helpers for the glyph atlas. |
|
||||
| `Frame.zig` | `opengl/Frame.zig` | Per-frame command buffer + sync primitives (semaphores / fences). |
|
||||
| `shaders.zig` | `opengl/shaders.zig` | Loader for the SPIR-V blobs (built at compile time via glslang). |
|
||||
## File layout
|
||||
|
||||
The renderer's top-level lives one directory up at
|
||||
`../Vulkan.zig` and is the single module imported by
|
||||
`src/renderer.zig` when `build_config.renderer == .vulkan`. That file
|
||||
currently fails at comptime with a pointer back to the
|
||||
`qt-vulkan-renderer` branch — see its header comment for the full
|
||||
contract `GenericRenderer(Vulkan)` expects this directory's modules
|
||||
to satisfy.
|
||||
Renderer policy (this directory):
|
||||
|
||||
## Binding
|
||||
| File | OpenGL counterpart | Notes |
|
||||
| ------------------- | ------------------------- | ------------------------------------------------------------------ |
|
||||
| `Target.zig` | `opengl/Target.zig` | Render image + dmabuf export (direct or legacy_copy mode). |
|
||||
| `Texture.zig` | `opengl/Texture.zig` | `VkImage` + `VkImageView` + upload helpers for the glyph atlas. |
|
||||
| `buffer.zig` | `opengl/buffer.zig` | `Buffer(T)` host-coherent + per-renderer-thread recycle pool. |
|
||||
| `Pipeline.zig` | `opengl/Pipeline.zig` | Graphics pipeline + descriptor set layout creation. |
|
||||
| `RenderPass.zig` | `opengl/RenderPass.zig` | Dynamic-rendering pass + step recorder. |
|
||||
| `Frame.zig` | `opengl/Frame.zig` | Per-draw command buffer + fence-paced submit-then-wait. |
|
||||
| `shaders.zig` | `opengl/shaders.zig` | GLSL → SPIR-V via glslang + the OpenGL-GLSL → Vulkan-GLSL rewrite. |
|
||||
|
||||
The Vulkan C API ships as the `vulkan` Zig module from `pkg/vulkan/`
|
||||
(thin `@cImport` of the system `vulkan/vulkan.h`). It is registered
|
||||
in `build.zig.zon` as a lazy dependency and only pulled in when
|
||||
`-Drenderer=vulkan` is selected, at which point `libvulkan` is also
|
||||
linked (see `src/build/SharedDeps.zig`). The system needs
|
||||
`vulkan-headers` (`/usr/include/vulkan/vulkan.h`) and `libvulkan.so`
|
||||
present — both are stock on every Linux distro and already required
|
||||
by the Qt RHI side of the renderer.
|
||||
Pure Vulkan-API wrappers (in `pkg/vulkan/`):
|
||||
|
||||
| File | OpenGL counterpart | Notes |
|
||||
| --------------------- | ------------------------ | ------------------------------------------------------------------ |
|
||||
| `Device.zig` | (no analogue — GL ctx) | Host-provided VkInstance/Device/Queue + function dispatch table. |
|
||||
| `Sampler.zig` | `pkg/opengl/Sampler.zig` | `VkSampler` (linear for atlases, nearest for cells). |
|
||||
| `CommandPool.zig` | (none) | `VkCommandPool` + one-shot record/submit helper. |
|
||||
| `DescriptorPool.zig` | (none) | Per-frame `VkDescriptorPool`. |
|
||||
|
||||
The renderer's top-level lives one directory up at `../Vulkan.zig`
|
||||
and is the single module imported by `src/renderer.zig` when
|
||||
`build_config.renderer == .vulkan`. It re-exports the `pkg/vulkan/`
|
||||
types as `Vulkan.Device`, `Vulkan.Sampler`, etc., so call sites use a
|
||||
single `Vulkan.*` namespace regardless of where each type physically
|
||||
lives.
|
||||
|
||||
## Why dmabuf, not Vulkan swapchains?
|
||||
|
||||
|
|
@ -39,8 +41,7 @@ The Qt frontend wants to keep `GhosttySurface` as a `QWidget` so that
|
|||
splits (`QSplitter`), tabs (`QTabWidget`), and translucent composition
|
||||
keep working. That rules out `QVulkanWindow`. Instead libghostty
|
||||
exports the rendered `VkImage` memory as a dmabuf fd
|
||||
(`VK_KHR_external_memory_fd`); the Qt side imports it as a
|
||||
`QRhiTexture` in a `QRhiWidget` and composites it like any other
|
||||
GPU-backed widget. This gives us Vulkan GPU rendering without losing
|
||||
the widget tree — the path 3 ("zero-copy GPU interop") described in
|
||||
the session-log on the `qt-vulkan-renderer` branch.
|
||||
(`VK_KHR_external_memory_fd` + `VK_EXT_image_drm_format_modifier`); the
|
||||
Qt side imports it via `zwp_linux_dmabuf_v1` and attaches it to a
|
||||
`wl_subsurface` parented to the top-level `wl_surface`. The compositor
|
||||
scans the buffer out directly — no readback, no QImage round trip.
|
||||
|
|
|
|||
|
|
@ -16,12 +16,13 @@
|
|||
const Self = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const vk = @import("vulkan").c;
|
||||
const vulkan = @import("vulkan");
|
||||
const vk = vulkan.c;
|
||||
|
||||
const DescriptorPool = @import("DescriptorPool.zig");
|
||||
const Device = @import("Device.zig");
|
||||
const Device = vulkan.Device;
|
||||
const DescriptorPool = vulkan.DescriptorPool;
|
||||
const Sampler = vulkan.Sampler;
|
||||
const Pipeline = @import("Pipeline.zig");
|
||||
const Sampler = @import("Sampler.zig");
|
||||
const Target = @import("Target.zig");
|
||||
const Texture = @import("Texture.zig");
|
||||
const bufferpkg = @import("buffer.zig");
|
||||
|
|
@ -175,9 +176,7 @@ pub fn begin(opts: Options) Self {
|
|||
if (opts.attachments.len == 0) return self;
|
||||
|
||||
const attach = opts.attachments[0];
|
||||
const view: vk.VkImageView, const image: vk.VkImage,
|
||||
const width: u32, const height: u32,
|
||||
const old_layout: vk.VkImageLayout = switch (attach.target) {
|
||||
const view: vk.VkImageView, const image: vk.VkImage, const width: u32, const height: u32, const old_layout: vk.VkImageLayout = switch (attach.target) {
|
||||
.texture => |t| .{ t.view, t.image, @intCast(t.width), @intCast(t.height), t.layout },
|
||||
.target => |t| .{ t.view, t.image, t.width, t.height, t.layout },
|
||||
};
|
||||
|
|
@ -256,9 +255,12 @@ pub fn begin(opts: Options) Self {
|
|||
src_stage,
|
||||
vk.VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
0,
|
||||
0, null,
|
||||
0, null,
|
||||
1, &barrier,
|
||||
0,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
1,
|
||||
&barrier,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -650,9 +652,12 @@ pub fn complete(self: *const Self) void {
|
|||
vk.VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
dst_stage,
|
||||
0,
|
||||
0, null,
|
||||
0, null,
|
||||
1, &barrier,
|
||||
0,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
1,
|
||||
&barrier,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ const std = @import("std");
|
|||
const vk = @import("vulkan").c;
|
||||
|
||||
const apprt = @import("../../apprt.zig");
|
||||
const Device = @import("Device.zig");
|
||||
const Device = @import("vulkan").Device;
|
||||
|
||||
const log = std.log.scoped(.vulkan);
|
||||
|
||||
|
|
@ -87,14 +87,13 @@ pub const Options = struct {
|
|||
/// TRANSFER_SRC_BIT`). Rarely needed.
|
||||
extra_usage: vk.VkImageUsageFlags = 0,
|
||||
|
||||
/// Per-surface platform callbacks. `Device.platform` is also a
|
||||
/// `Platform.Vulkan`, but it's the singleton's copy — its
|
||||
/// `userdata` points at whichever surface initialized the
|
||||
/// device first. Splits/tabs share the device but each gets its
|
||||
/// own platform with the right `userdata`, so `present()` reaches
|
||||
/// the right window. Falls back to `device.platform` when
|
||||
/// null (e.g. smoke test).
|
||||
platform: ?apprt.embedded.Platform.Vulkan = null,
|
||||
/// Per-surface platform callbacks. The host's process-wide
|
||||
/// VkDevice is shared across splits/tabs, but each surface gets
|
||||
/// its own platform copy with the right `userdata`, so
|
||||
/// `present()` reaches the right window — and `pickModifier`
|
||||
/// asks the right host (compositor and host can in principle
|
||||
/// differ across surfaces, e.g. mixed-DPI multi-screen).
|
||||
platform: apprt.embedded.Platform.Vulkan,
|
||||
};
|
||||
|
||||
pub const Error = error{
|
||||
|
|
@ -105,9 +104,8 @@ pub const Error = error{
|
|||
|
||||
device: *const Device,
|
||||
|
||||
/// Per-surface platform — see `Options.platform`. Null means "use
|
||||
/// `device.platform`" (the singleton's copy from the first surface).
|
||||
platform: ?apprt.embedded.Platform.Vulkan = null,
|
||||
/// Per-surface platform — see `Options.platform`.
|
||||
platform: apprt.embedded.Platform.Vulkan,
|
||||
|
||||
/// Which present strategy this target uses. Decides whether
|
||||
/// `recordPresentBarrier` emits a copy.
|
||||
|
|
@ -148,7 +146,7 @@ pub fn init(opts: Options) Error!Self {
|
|||
vk.VK_FORMAT_FEATURE_TRANSFER_SRC_BIT |
|
||||
vk.VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT;
|
||||
|
||||
const picked = try pickModifier(dev, opts.format, drm_format, required_features);
|
||||
const picked = try pickModifier(dev, opts.platform, opts.format, drm_format, required_features);
|
||||
if (picked) |m| {
|
||||
const tag: []const u8 = if (m == DRM_FORMAT_MOD_LINEAR)
|
||||
"LINEAR"
|
||||
|
|
@ -187,6 +185,7 @@ pub fn init(opts: Options) Error!Self {
|
|||
/// COLOR_ATTACHMENT for every modifier).
|
||||
fn pickModifier(
|
||||
dev: *const Device,
|
||||
platform: apprt.embedded.Platform.Vulkan,
|
||||
format: vk.VkFormat,
|
||||
drm_format: u32,
|
||||
required_features: vk.VkFormatFeatureFlags,
|
||||
|
|
@ -198,8 +197,8 @@ fn pickModifier(
|
|||
// work for AMD/Intel LINEAR but the compositor attach would
|
||||
// fail, so treat it as "no intersection."
|
||||
var host_mods: [MAX_MODIFIERS]u64 = undefined;
|
||||
const host_returned = dev.platform.get_supported_modifiers(
|
||||
dev.platform.userdata,
|
||||
const host_returned = platform.get_supported_modifiers(
|
||||
platform.userdata,
|
||||
drm_format,
|
||||
&host_mods,
|
||||
MAX_MODIFIERS,
|
||||
|
|
@ -763,9 +762,12 @@ fn recordDirectBarrier(self: *Self, cb: vk.VkCommandBuffer) void {
|
|||
vk.VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
vk.VK_PIPELINE_STAGE_HOST_BIT,
|
||||
0,
|
||||
0, null,
|
||||
0, null,
|
||||
1, &img_barrier,
|
||||
0,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
1,
|
||||
&img_barrier,
|
||||
);
|
||||
|
||||
self.layout = vk.VK_IMAGE_LAYOUT_GENERAL;
|
||||
|
|
@ -800,9 +802,12 @@ fn recordCopyToDmabuf(self: *Self, cb: vk.VkCommandBuffer) void {
|
|||
vk.VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
vk.VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
0,
|
||||
0, null,
|
||||
0, null,
|
||||
1, &img_barrier,
|
||||
0,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
1,
|
||||
&img_barrier,
|
||||
);
|
||||
|
||||
// Copy image → buffer. BGRA8, packed (stride = width*4).
|
||||
|
|
@ -849,9 +854,12 @@ fn recordCopyToDmabuf(self: *Self, cb: vk.VkCommandBuffer) void {
|
|||
vk.VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
vk.VK_PIPELINE_STAGE_HOST_BIT,
|
||||
0,
|
||||
0, null,
|
||||
1, &buf_barrier,
|
||||
0, null,
|
||||
0,
|
||||
null,
|
||||
1,
|
||||
&buf_barrier,
|
||||
0,
|
||||
null,
|
||||
);
|
||||
|
||||
// Track the new image layout so the next frame's RenderPass.begin
|
||||
|
|
@ -861,11 +869,9 @@ fn recordCopyToDmabuf(self: *Self, cb: vk.VkCommandBuffer) void {
|
|||
}
|
||||
|
||||
pub fn present(self: *const Self) void {
|
||||
// Prefer the per-surface platform — its `userdata` points at THIS
|
||||
// surface's GhosttySurface, so present reaches the right window.
|
||||
// Fall back to the device's singleton copy when no platform was
|
||||
// attached (only the smoke test does this).
|
||||
const platform = if (self.platform) |p| p else self.device.platform;
|
||||
// Per-surface platform — its `userdata` points at THIS surface's
|
||||
// GhosttySurface, so present reaches the right window.
|
||||
const platform = self.platform;
|
||||
// `image_backed` is the host's signal that this fd is importable
|
||||
// by a 2D-image consumer (Wayland linux-dmabuf-v1, Vulkan
|
||||
// external image, etc.). True in `.direct` mode where the fd was
|
||||
|
|
|
|||
|
|
@ -27,10 +27,11 @@
|
|||
const Self = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const vk = @import("vulkan").c;
|
||||
const vulkan = @import("vulkan");
|
||||
const vk = vulkan.c;
|
||||
|
||||
const Device = @import("Device.zig");
|
||||
const CommandPool = @import("CommandPool.zig");
|
||||
const Device = vulkan.Device;
|
||||
const CommandPool = vulkan.CommandPool;
|
||||
const bufferpkg = @import("buffer.zig");
|
||||
|
||||
const log = std.log.scoped(.vulkan);
|
||||
|
|
@ -278,8 +279,7 @@ pub fn replaceRegion(
|
|||
else => 0,
|
||||
};
|
||||
const src_stage: vk.VkPipelineStageFlags = switch (old_layout) {
|
||||
vk.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL =>
|
||||
vk.VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
|
||||
vk.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL => vk.VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
|
||||
else => vk.VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
|
||||
};
|
||||
{
|
||||
|
|
@ -306,9 +306,12 @@ pub fn replaceRegion(
|
|||
src_stage,
|
||||
vk.VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
0, // dependencyFlags
|
||||
0, null, // memory barriers
|
||||
0, null, // buffer memory barriers
|
||||
1, &barrier,
|
||||
0,
|
||||
null, // memory barriers
|
||||
0,
|
||||
null, // buffer memory barriers
|
||||
1,
|
||||
&barrier,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -370,9 +373,12 @@ pub fn replaceRegion(
|
|||
vk.VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
vk.VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
|
||||
0,
|
||||
0, null,
|
||||
0, null,
|
||||
1, &barrier,
|
||||
0,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
1,
|
||||
&barrier,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,9 +23,10 @@
|
|||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const vk = @import("vulkan").c;
|
||||
const vulkan = @import("vulkan");
|
||||
const vk = vulkan.c;
|
||||
|
||||
const Device = @import("Device.zig");
|
||||
const Device = vulkan.Device;
|
||||
|
||||
const log = std.log.scoped(.vulkan);
|
||||
|
||||
|
|
|
|||
|
|
@ -20,13 +20,14 @@
|
|||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const vk = @import("vulkan").c;
|
||||
const vulkan = @import("vulkan");
|
||||
const vk = vulkan.c;
|
||||
const glslang = @import("glslang");
|
||||
|
||||
const Device = @import("Device.zig");
|
||||
const Device = vulkan.Device;
|
||||
const Sampler = vulkan.Sampler;
|
||||
const DescriptorPool = vulkan.DescriptorPool;
|
||||
const Pipeline = @import("Pipeline.zig");
|
||||
const Sampler = @import("Sampler.zig");
|
||||
const DescriptorPool = @import("DescriptorPool.zig");
|
||||
const math = @import("../../math.zig");
|
||||
|
||||
const log = std.log.scoped(.vulkan);
|
||||
|
|
@ -817,7 +818,6 @@ pub const Shaders = struct {
|
|||
/// linear sampling, clamp-to-edge — the standard 2D mode.
|
||||
image_sampler: ?Sampler = null,
|
||||
|
||||
|
||||
defunct: bool = false,
|
||||
|
||||
/// The compiled `VkShaderModule`s for the renderer's built-in
|
||||
|
|
@ -838,7 +838,7 @@ pub const Shaders = struct {
|
|||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
device: *const @import("Device.zig"),
|
||||
device: *const Device,
|
||||
// SPIR-V binaries (4-byte-aligned) from
|
||||
// `shadertoy.loadFromFiles` with `target = .spv`. The Vulkan
|
||||
// backend bypasses the spirv-cross GLSL roundtrip the other
|
||||
|
|
@ -1366,7 +1366,7 @@ pub const Shaders = struct {
|
|||
/// (Globals UBO, bg_cells SSBO, individual sampler) so a helper
|
||||
/// keeps the call sites short.
|
||||
fn createSingleBindingDsl(
|
||||
device: *const @import("Device.zig"),
|
||||
device: *const Device,
|
||||
binding: u32,
|
||||
descriptor_type: vk.VkDescriptorType,
|
||||
stage_flags: vk.VkShaderStageFlags,
|
||||
|
|
|
|||
Loading…
Reference in New Issue