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
ntomsic 2026-05-25 18:44:26 -05:00
parent 2ddf143c15
commit 3ec5f35bd7
14 changed files with 271 additions and 181 deletions

View File

@ -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");

View File

@ -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");

View File

@ -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,

View File

@ -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");

View File

@ -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");

View File

@ -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` GLSLSPIR-VVkShaderModule + 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` GLSLSPIR-VVkShaderModule + 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
// GLSLSPIR-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,
);
}

View File

@ -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");

View File

@ -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);

View File

@ -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.

View File

@ -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,
);
}

View File

@ -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

View File

@ -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,
);
}

View File

@ -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);

View File

@ -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,