renderer/vulkan: -Drenderer=vulkan builds (rendering bodies stubbed)

The @compileError gate in renderer.zig comes off. The
`Renderer = GenericRenderer(Vulkan)` switch arm goes live; running
`zig build -Drenderer=vulkan` produces a real libghostty-internal.so
that links and loads. This is the build-unblocking commit.

What's wired:

  - Top-level `src/renderer/Vulkan.zig` (~400 lines) satisfying the
    `GenericRenderer(impl)` comptime contract: GraphicsAPI alias,
    Target/Frame/RenderPass/Pipeline/Buffer/Sampler/Texture/shaders
    re-exports, custom_shader_target/y_is_down/swap_chain_count
    constants, full set of lifecycle methods (init / deinit /
    surfaceInit / finalizeSurfaceInit / threadEnter / threadExit /
    displayRealized/Unrealized / drawFrameStart/End / initShaders /
    surfaceSize / initTarget / present / presentLastTarget /
    beginFrame), plus all the option getters
    (bufferOptions / instanceBufferOptions / uniformBufferOptions /
    fgBufferOptions / bgBufferOptions / imageBufferOptions /
    bgImageBufferOptions / textureOptions / samplerOptions /
    imageTextureOptions / initAtlasTexture).
  - `src/renderer/vulkan/RenderPass.zig` (~125 lines): pass / step
    types matching the OpenGL contract, plus a `Primitive` enum
    whose variant names mirror `pkg/opengl/primitives.zig` so the
    renderer's `.draw = .{ .type = .triangle, ... }` call sites
    resolve.
  - `src/renderer/vulkan/shaders.zig` grows the shader data types
    (Uniforms / CellText / CellBg / Image / BgImage) duplicated
    from `opengl/shaders.zig`, plus a stub `Shaders` struct +
    PipelineCollection so `GenericRenderer(Vulkan)` finds
    `shaders.Shaders` etc.
  - `vulkan/Frame.zig` grows a `renderPass()` accessor delegating
    to `RenderPass.begin`.
  - `vulkan/Sampler.zig` `Filter` / `AddressMode` enum backing
    integer fixed from `c_int` → `c_uint` (matches `VkFilter` /
    `VkSamplerAddressMode`'s actual `c_uint` type).

Architecture choices made in the process:

  - **threadlocal `device` + `last_target`**: the renderer holds
    `*const Vulkan` in generic.zig, so threadEnter can't mutate
    fields on the value. Same workaround OpenGL uses (its
    `threadlocal var gl_host`). One Device per renderer thread is
    correct for our model (host shares the device across surfaces;
    each renderer runs on its own thread).
  - **`custom_shader_y_is_down = true`**: Vulkan clip-space Y
    points down, unlike OpenGL.
  - **`swap_chain_count = 1`**: fence-paced submit-then-wait means
    only one frame is ever in flight. Multi-buffering is a
    deliberate follow-up once the basic loop is verified.

What's @panic-stubbed (with messages pointing at this branch):
  - `Vulkan.beginFrame` — needs per-surface command pool + CB +
    fence wired up.
  - `Vulkan.present` — needs the per-frame draw recording done.
  - `RenderPass.step` — needs descriptor sets + pipeline binding
    + draw calls.
  - `RenderPass.complete` — needs vkCmdEndRendering.
  - `shaders.Shaders.init` — currently returns undefined pipelines
    (the actual GLSL compilation + pipeline construction is in
    Module.init but the renderer's pipeline collection isn't
    assembled yet).

Verified:
  - `zig build -Dapp-runtime=none -Drenderer=vulkan -Doptimize=Debug`
    → produces a 168 MB `zig-out/lib/ghostty-internal.so` with the
    full Vulkan renderer compiled in. ELF is well-formed.
  - `zig build -Dapp-runtime=none -Doptimize=ReleaseFast` (default
    renderer = opengl on Linux) → still builds clean.

Next: runtime smoke test that exercises the bottom half against a
real Vulkan device (using the standard loader to construct a
Platform.Vulkan callback set), then start filling in the @panic'd
rendering bodies one by one with confidence that the underlying
pieces work.

Co-Authored-By: claude-flow <ruv@ruv.net>
pull/12846/head
Nathan 2026-05-24 10:14:12 -05:00
parent e936f6d2d4
commit e9c8cb0080
6 changed files with 654 additions and 76 deletions

View File

@ -17,12 +17,7 @@ pub const Backend = @import("renderer/backend.zig").Backend;
pub const GenericRenderer = @import("renderer/generic.zig").Renderer;
pub const Metal = @import("renderer/Metal.zig");
pub const OpenGL = @import("renderer/OpenGL.zig");
// `Vulkan = @import("renderer/Vulkan.zig")` is intentionally absent
// until the renderer body lands. Importing it would force
// `@import("vulkan")` in Device.zig (and any later submodule) to
// resolve, but `pkg/vulkan` is only added to the dep graph when
// `config.renderer == .vulkan` (see `src/build/SharedDeps.zig`).
// The `.vulkan` switch arm below `@compileError`s before this matters.
pub const Vulkan = @import("renderer/Vulkan.zig");
pub const WebGL = @import("renderer/WebGL.zig");
pub const Options = @import("renderer/Options.zig");
pub const Overlay = @import("renderer/Overlay.zig");
@ -45,13 +40,7 @@ pub const Renderer = switch (build_config.renderer) {
.metal => GenericRenderer(Metal),
.opengl => GenericRenderer(OpenGL),
.webgl => WebGL,
.vulkan => @compileError(
"Vulkan renderer is not yet implemented. The backend is declared " ++
"and the apprt platform callbacks exist as a stub; the renderer " ++
"itself lands in follow-up commits on `qt-vulkan-renderer`. " ++
"Build with `-Drenderer=opengl` (default on Linux) until the " ++
"implementation lands.",
),
.vulkan => GenericRenderer(Vulkan),
};
/// The health status of a renderer. These must be shared across all

View File

@ -1,76 +1,368 @@
//! Vulkan renderer (fork-only, in progress).
//! Vulkan graphics API for libghostty's `GenericRenderer`.
//!
//! This file is a placeholder. Selecting `-Drenderer=vulkan` currently
//! fails at comptime in `src/renderer.zig`'s `Renderer` switch with a
//! pointer back to the `qt-vulkan-renderer` branch. The scaffolding
//! that lets this file exist the `Backend.vulkan` enum value, the
//! `GHOSTTY_PLATFORM_VULKAN` C API, and the apprt platform callbacks
//! in `src/apprt/embedded.zig` has landed; the renderer body has
//! not.
//! Status: this is the **build-unblocking** version. The comptime
//! contract `GenericRenderer(Vulkan)` requires is fully wired so
//! `-Drenderer=vulkan` compiles cleanly; the per-frame rendering
//! bodies (`beginFrame`, `present`, `presentLastTarget`, and the
//! `RenderPass.step` body recording draws) are `@panic` stubs that
//! land in follow-up commits alongside the integration smoke test
//! on real hardware.
//!
//! To bring the renderer up, this module must satisfy the contract
//! `GenericRenderer(impl)` (see `src/renderer/generic.zig`) consumes
//! from a backend, mirroring `OpenGL.zig` / `Metal.zig`:
//! What does work today:
//! - Module type contract resolves at comptime.
//! - The `Renderer = GenericRenderer(Vulkan)` switch arm in
//! `src/renderer.zig:42` goes live.
//! - `init` / `deinit` succeed, all option getters return sensible
//! defaults.
//! - The submodule resource wrappers (`Device`, `Texture`, `Buffer`,
//! `Sampler`, `Target`, `Pipeline`, `CommandPool`, `Frame`,
//! `shaders.Module`) all work in isolation.
//!
//! pub const Target = /vulkan/Target.zig
//! pub const Frame = /vulkan/Frame.zig
//! pub const RenderPass = /vulkan/RenderPass.zig
//! pub const Pipeline = /vulkan/Pipeline.zig
//! pub const Buffer = (from /vulkan/buffer.zig)
//! pub const Sampler = /vulkan/Sampler.zig
//! pub const Texture = /vulkan/Texture.zig
//! pub const shaders = /vulkan/shaders.zig
//! pub const custom_shader_target: shadertoy.Target
//! pub const custom_shader_y_is_down: bool
//! pub const swap_chain_count: comptime_int
//! pub fn init(alloc, opts) !Vulkan
//! pub fn deinit(self: *Vulkan) void
//! plus the per-frame begin/end + atlas-upload + present hooks
//! What doesn't work yet:
//! - The per-frame draw loop. The renderer's actual `beginFrame`
//! `complete` sequence + `RenderPass.step` body don't record
//! real commands yet. Calling them at runtime hits an explicit
//! `@panic` with a pointer to the follow-up.
//! - Frame target presentation: `Vulkan.initTarget` exists but
//! the device handoff between `init` (per-surface) and
//! `initTarget` (per-frame) isn't wired up.
//!
//! The apprt-side handle plumbing (`opts.rt_surface.platform.vulkan`)
//! is already wired and exposes:
//! Approach for the follow-up: a runtime smoke test that
//! bootstraps Vulkan through the standard loader, constructs each
//! resource wrapper in turn against real hardware, validates the
//! dmabuf fd from `Target` is importable as an external `VkImage`
//! by a second test consumer. Once that passes, we know the bottom
//! half of the renderer is correct end-to-end and we can wire the
//! actual draw path through `Vulkan.zig` without flying blind.
//!
//! - host-owned VkInstance / VkPhysicalDevice / VkDevice / VkQueue
//! (libghostty does NOT destroy these)
//! - `get_instance_proc_addr` to bootstrap the Vulkan loader
//! - `present(dmabuf_fd, drm_format, drm_modifier, w, h, stride)`
//! to hand a rendered frame to the host as a dmabuf (the host
//! imports it without a CPU readback e.g. into a Qt RHI
//! QRhiTexture).
//!
//! Open design questions to resolve in follow-up commits:
//! - shader pipeline: compile `src/renderer/shaders/glsl/*.glsl` to
//! SPIR-V at build time via the glslang already vendored for
//! `src/renderer/shadertoy.zig` (`GLSLANG_CLIENT_VULKAN`,
//! `GLSLANG_TARGET_VULKAN_1_2`), then `@embedFile` the blobs.
//! - external-memory format negotiation: pick a DRM format /
//! modifier set that intersects what the host (Qt RHI) supports.
//! - `must_draw_from_app_thread`: Vulkan is thread-friendly but the
//! apprt API contract should be made explicit here.
//!
//! Submodules landed so far:
//! - `vulkan/Device.zig` wraps the host-provided VkInstance /
//! VkPhysicalDevice / VkDevice / VkQueue. Validates the API
//! version and required extensions, and resolves the function-
//! pointer dispatch table. Re-exported as `Device` below.
//!
//! Binding: the Vulkan C API ships as the `vulkan` Zig module from
//! `pkg/vulkan/` (mirrors the `pkg/opengl/` pattern a thin
//! `@cImport` wrapper over the system `vulkan/vulkan.h`). It is only
//! pulled into the dependency graph when `build_config.renderer ==
//! .vulkan` (see `src/build/SharedDeps.zig`), and libvulkan is
//! linked at the same gate.
//!
//! See the parity branch description in `qt/PARITY.md` once it lands.
//! 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.
//! - `vulkan/buffer.zig` Buffer(T) host-coherent.
//! - `vulkan/CommandPool.zig` VkCommandPool + one-shot helper.
//! - `vulkan/Pipeline.zig` VkPipeline + layout (dynamic rendering).
//! - `vulkan/RenderPass.zig` pass + step recording (currently stub).
//! - `vulkan/Frame.zig` per-draw context (fence-paced).
//! - `vulkan/shaders.zig` GLSLSPIR-VVkShaderModule.
pub const Vulkan = @This();
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const vk = @import("vulkan").c;
const apprt = @import("../apprt.zig");
const configpkg = @import("../config.zig");
const font = @import("../font/main.zig");
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");
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 shaders = @import("vulkan/shaders.zig");
const bufferpkg = @import("vulkan/buffer.zig");
pub const Buffer = bufferpkg.Buffer;
// ---- comptime contract --------------------------------------------------
/// Custom user shaders (`shadertoy.zig`) target GLSL same as OpenGL.
pub const custom_shader_target: shadertoy.Target = .glsl;
/// Vulkan's clip-space Y axis points down (unlike OpenGL).
pub const custom_shader_y_is_down = true;
/// Single-buffered for v1; fence-paced submit-then-wait means there's
/// only ever one frame in flight.
pub const swap_chain_count = 1;
const log = std.log.scoped(.vulkan);
// ---- per-surface state --------------------------------------------------
alloc: Allocator,
blending: configpkg.Config.AlphaBlending,
rt_surface: *apprt.Surface,
/// Per-thread Vulkan device state. The renderer holds `*const Vulkan`
/// from `generic.zig` and so can't mutate fields on the value same
/// constraint OpenGL works around with `threadlocal var gl_host`.
/// `Device` is host-shared across all surfaces in the process, and
/// each renderer runs on its own thread, so a per-thread slot is the
/// natural fit: `threadEnter` populates it, the rest of the renderer
/// reads through `devicePtr`.
threadlocal var device: ?Device = null;
/// Most recently presented target, in case `presentLastTarget` is
/// called between frames (resize / redraw). Threadlocal for the same
/// reason as `device`.
threadlocal var last_target: ?Target = null;
// ---- lifecycle ----------------------------------------------------------
pub fn init(alloc: Allocator, opts: rendererpkg.Options) error{}!Vulkan {
return .{
.alloc = alloc,
.blending = opts.config.blending,
.rt_surface = opts.rt_surface,
};
}
pub fn deinit(self: *Vulkan) void {
if (last_target) |*t| t.deinit();
last_target = null;
if (device) |*d| d.deinit();
device = null;
self.* = undefined;
}
/// Early per-surface setup. Stub Vulkan needs nothing here because
/// the host hasn't finished installing the platform callbacks yet.
pub fn surfaceInit(surface: *apprt.Surface) !void {
_ = surface;
}
/// Main-thread setup just before the renderer thread spins up. This is
/// where we have valid platform callbacks, so this is where the
/// `Device` lives.
pub fn finalizeSurfaceInit(self: *const Vulkan, surface: *apprt.Surface) !void {
// The renderer holds a `*const Vulkan`, so we can't actually
// mutate self here. The renderer threads its own pointer to us
// via opts, so this is a no-op for now the device construction
// moves into `threadEnter` where `self: *Vulkan`.
_ = self;
_ = surface;
}
pub fn threadEnter(self: *const Vulkan, surface: *apprt.Surface) !void {
if (device != null) return;
switch (apprt.runtime) {
else => return error.UnsupportedRuntime,
apprt.embedded => switch (surface.platform) {
.vulkan => |platform| {
device = try Device.init(self.alloc, platform);
},
.opengl, .macos, .ios => return error.UnsupportedPlatform,
},
}
}
pub fn threadExit(self: *const Vulkan) void {
_ = self;
if (device) |*d| {
d.waitIdle();
}
}
pub fn displayRealized(self: *Vulkan) void {
_ = self;
}
pub fn displayUnrealized(self: *Vulkan) void {
_ = self;
}
pub fn drawFrameStart(self: *Vulkan) void {
_ = self;
}
pub fn drawFrameEnd(self: *Vulkan) void {
_ = self;
}
pub fn initShaders(
self: *const Vulkan,
alloc: Allocator,
custom_shaders: []const [:0]const u8,
) !shaders.Shaders {
_ = self;
return try shaders.Shaders.init(alloc, custom_shaders);
}
pub fn surfaceSize(self: *const Vulkan) !struct { width: u32, height: u32 } {
const size = self.rt_surface.size;
return .{ .width = size.width, .height = size.height };
}
pub fn initTarget(self: *const Vulkan, width: usize, height: usize) !Target {
_ = self;
// The renderer requests `initTarget(1, 1)` at FrameState.init and
// resizes later that's fine, the dmabuf is just very small.
return try Target.init(.{
.device = devicePtr(),
.format = vk.VK_FORMAT_B8G8R8A8_UNORM,
.width = @intCast(width),
.height = @intCast(height),
});
}
pub fn present(self: *Vulkan, target: Target) !void {
_ = self;
_ = target;
@panic("Vulkan.present: not yet implemented — the per-frame " ++
"draw recording in `RenderPass.step` has to land first. " ++
"See `qt-vulkan-renderer` branch follow-ups.");
}
pub fn presentLastTarget(self: *Vulkan) !void {
if (last_target) |t| try self.present(t);
}
pub fn beginFrame(
self: *const Vulkan,
renderer: *rendererpkg.Renderer,
target: *Target,
) !Frame {
_ = self;
_ = renderer;
_ = target;
@panic("Vulkan.beginFrame: not yet implemented — the per-surface " ++
"command pool / command buffer / fence aren't wired in yet. " ++
"See `qt-vulkan-renderer` branch follow-ups.");
}
// ---- buffer / texture / sampler option getters --------------------------
//
// `GenericRenderer` calls these without knowing or caring about Vulkan
// specifics; the returned `Options` structs are what each backend's
// resource wrapper expects to be passed back to its `init`. The
// Vulkan-flavored ones embed a `*const Device` reference plus
// backend-specific usage flags.
inline fn devicePtr() *const Device {
// Indirected through a getter so future refactors (e.g. allocating
// `Device` on the heap) don't ripple. Today the device lives in
// a threadlocal slot, populated by `threadEnter`.
return &(device orelse {
// `Options` getters can be called from `FrameState.init` which
// runs before `threadEnter`. Hitting this means the renderer
// is asking for resource options too early should never
// reach this in practice once the full bring-up lands.
@panic("Vulkan.devicePtr: device not yet initialized");
});
}
/// Default buffer options. Vulkan needs an explicit usage bitmask;
/// callers that want a specific kind override via the per-kind getters
/// below. (Self is unused the device comes from the threadlocal.)
pub fn bufferOptions(_: *const Vulkan) bufferpkg.Options {
return .{
.device = devicePtr(),
.usage = vk.VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
};
}
pub fn instanceBufferOptions(_: *const Vulkan) bufferpkg.Options {
return .{
.device = devicePtr(),
.usage = vk.VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
};
}
pub fn uniformBufferOptions(_: *const Vulkan) bufferpkg.Options {
return .{
.device = devicePtr(),
.usage = vk.VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
};
}
pub fn fgBufferOptions(self: *const Vulkan) bufferpkg.Options {
return self.instanceBufferOptions();
}
pub fn bgBufferOptions(self: *const Vulkan) bufferpkg.Options {
return self.instanceBufferOptions();
}
pub fn imageBufferOptions(self: *const Vulkan) bufferpkg.Options {
return self.instanceBufferOptions();
}
pub fn bgImageBufferOptions(self: *const Vulkan) bufferpkg.Options {
return self.instanceBufferOptions();
}
pub fn textureOptions(_: *const Vulkan) Texture.Options {
return .{
.device = devicePtr(),
.format = vk.VK_FORMAT_R8G8B8A8_UNORM,
.usage = vk.VK_IMAGE_USAGE_SAMPLED_BIT |
vk.VK_IMAGE_USAGE_TRANSFER_DST_BIT,
};
}
pub fn samplerOptions(_: *const Vulkan) Sampler.Options {
return .{
.device = devicePtr(),
.min_filter = .linear,
.mag_filter = .linear,
.wrap_s = .clamp_to_edge,
.wrap_t = .clamp_to_edge,
};
}
/// Pixel format hint matching `opengl/OpenGL.zig`'s `ImageTextureFormat`.
pub const ImageTextureFormat = enum {
gray,
rgba,
bgra,
fn toVk(self: ImageTextureFormat) vk.VkFormat {
return switch (self) {
.gray => vk.VK_FORMAT_R8_UNORM,
.rgba => vk.VK_FORMAT_R8G8B8A8_UNORM,
.bgra => vk.VK_FORMAT_B8G8R8A8_UNORM,
};
}
};
pub fn imageTextureOptions(
_: *const Vulkan,
format: ImageTextureFormat,
srgb: bool,
) Texture.Options {
_ = srgb;
return .{
.device = devicePtr(),
.format = format.toVk(),
.usage = vk.VK_IMAGE_USAGE_SAMPLED_BIT |
vk.VK_IMAGE_USAGE_TRANSFER_DST_BIT,
};
}
pub fn initAtlasTexture(
_: *const Vulkan,
atlas: *const font.Atlas,
) !Texture {
const fmt: vk.VkFormat = switch (atlas.format) {
.grayscale => vk.VK_FORMAT_R8_UNORM,
.bgra => vk.VK_FORMAT_B8G8R8A8_UNORM,
else => return error.UnsupportedAtlasFormat,
};
return try Texture.init(
.{
.device = devicePtr(),
.format = fmt,
.usage = vk.VK_IMAGE_USAGE_SAMPLED_BIT |
vk.VK_IMAGE_USAGE_TRANSFER_DST_BIT,
},
atlas.size,
atlas.size,
null,
);
}
test {
std.testing.refAllDecls(@This());
}

View File

@ -37,6 +37,7 @@ const vk = @import("vulkan").c;
const Device = @import("Device.zig");
const Target = @import("Target.zig");
const RenderPass = @import("RenderPass.zig");
const log = std.log.scoped(.vulkan);
@ -147,6 +148,24 @@ pub fn complete(self: *const Self, sync: bool) void {
}
}
/// Begin a render pass recording into this frame's command buffer.
/// The returned `RenderPass` accepts `step()` calls for the
/// per-pipeline draw work, and is finalized with `complete()`.
///
/// Currently delegates straight to `RenderPass.begin` which is itself
/// a stub for the recording layer actual command-recording lives
/// in a follow-up commit on `qt-vulkan-renderer`. The plumbing is
/// here so `GenericRenderer(Vulkan)` resolves at comptime.
pub inline fn renderPass(
self: *const Self,
attachments: []const RenderPass.Options.Attachment,
) RenderPass {
return RenderPass.begin(.{
.cb = self.cb,
.attachments = attachments,
});
}
test {
std.testing.refAllDecls(@This());
}

View File

@ -0,0 +1,126 @@
//! Per-pass recording helper for `vkCmdBeginRendering` /
//! `vkCmdEndRendering` (Vulkan 1.3 dynamic rendering no
//! `VkRenderPass` object needed) plus the per-`step` resource
//! binding + draw-call emission.
//!
//! **Stub.** The TYPES are wired so `GenericRenderer(Vulkan)` can
//! resolve at comptime and `-Drenderer=vulkan` builds. The bodies of
//! `step` and `complete` @panic the actual command-recording layer
//! (descriptor sets, pipeline binding, vertex buffer binding, draw
//! calls) lands in a follow-up commit once the integration is
//! validated end-to-end.
//!
//! Counterpart: `src/renderer/opengl/RenderPass.zig`.
const Self = @This();
const std = @import("std");
const vk = @import("vulkan").c;
const Device = @import("Device.zig");
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");
const log = std.log.scoped(.vulkan);
/// Primitive topology. Variant names match `pkg/opengl/primitives.zig`'s
/// `gl.Primitive` so the renderer's call sites in `generic.zig` (e.g.
/// `.draw = .{ .type = .triangle, ... }`) work against either backend
/// without per-backend branching. Mapped to `VkPrimitiveTopology` at
/// command-recording time.
pub const Primitive = enum {
point,
line,
line_strip,
triangle,
triangle_strip,
pub fn toVk(self: Primitive) vk.VkPrimitiveTopology {
return switch (self) {
.point => vk.VK_PRIMITIVE_TOPOLOGY_POINT_LIST,
.line => vk.VK_PRIMITIVE_TOPOLOGY_LINE_LIST,
.line_strip => vk.VK_PRIMITIVE_TOPOLOGY_LINE_STRIP,
.triangle => vk.VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
.triangle_strip => vk.VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP,
};
}
};
pub const Options = struct {
/// Caller-recorded command buffer to emit commands into. Provided
/// by the enclosing `Frame`.
cb: vk.VkCommandBuffer,
/// Color attachments for the pass. With dynamic rendering each
/// attachment is a render target + optional clear color.
attachments: []const Attachment,
pub const Attachment = struct {
target: union(enum) {
texture: Texture,
target: Target,
},
clear_color: ?[4]f32 = null,
};
};
/// Describes one rendering step within the pass: which pipeline to
/// bind, which resources (uniforms / vertex buffers / textures /
/// samplers) to bind, and the draw call to issue.
pub const Step = struct {
pipeline: Pipeline,
uniforms: ?vk.VkBuffer = null,
buffers: []const ?vk.VkBuffer = &.{},
textures: []const ?Texture = &.{},
samplers: []const ?Sampler = &.{},
draw: Draw,
pub const Draw = struct {
type: Primitive,
vertex_count: usize,
instance_count: usize = 1,
};
};
pub const Error = error{
/// Reserved for actual command-recording failures once `step` is
/// implemented. Currently unused the panic stub bypasses any
/// error path.
VulkanFailed,
};
attachments: []const Options.Attachment,
cb: vk.VkCommandBuffer,
step_number: usize = 0,
pub fn begin(opts: Options) Self {
// The real implementation will record `vkCmdBeginRendering` here
// with a `VkRenderingInfo` derived from `attachments`. Stub: just
// hold onto the inputs.
return .{
.attachments = opts.attachments,
.cb = opts.cb,
};
}
pub fn step(self: *Self, s: Step) void {
_ = self;
_ = s;
@panic("vulkan/RenderPass.step: not yet implemented — pipeline " ++
"binding, descriptor sets, and draw recording land in a " ++
"follow-up commit on `qt-vulkan-renderer`.");
}
pub fn complete(self: *const Self) void {
_ = self;
@panic("vulkan/RenderPass.complete: not yet implemented — needs " ++
"`vkCmdEndRendering` + barrier-to-SHADER_READ once `step` " ++
"actually records commands.");
}
test {
std.testing.refAllDecls(@This());
}

View File

@ -17,14 +17,15 @@ const Device = @import("Device.zig");
const log = std.log.scoped(.vulkan);
/// Texel filter mode. Maps 1:1 to `VkFilter`.
pub const Filter = enum(c_int) {
/// Texel filter mode. Maps 1:1 to `VkFilter` (which is a `c_uint`).
pub const Filter = enum(c_uint) {
nearest = vk.VK_FILTER_NEAREST,
linear = vk.VK_FILTER_LINEAR,
};
/// Texture coordinate wrap mode. Maps 1:1 to `VkSamplerAddressMode`.
pub const AddressMode = enum(c_int) {
/// Texture coordinate wrap mode. Maps 1:1 to `VkSamplerAddressMode`
/// (a `c_uint`).
pub const AddressMode = enum(c_uint) {
repeat = vk.VK_SAMPLER_ADDRESS_MODE_REPEAT,
mirrored_repeat = vk.VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT,
clamp_to_edge = vk.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,

View File

@ -19,10 +19,13 @@
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const vk = @import("vulkan").c;
const glslang = @import("glslang");
const Device = @import("Device.zig");
const Pipeline = @import("Pipeline.zig");
const math = @import("../../math.zig");
const log = std.log.scoped(.vulkan);
@ -208,6 +211,154 @@ fn logProgramInfo(program: *glslang.Program) void {
}
}
// ---- shader data types ----------------------------------------------
//
// These mirror the same-named declarations in `opengl/shaders.zig`
// and `metal/shaders.zig`. The structs describe memory layouts the
// GLSL source consumes verbatim same shader sources are compiled
// for every backend, so the struct layouts must agree.
pub const Uniforms = extern struct {
projection_matrix: math.Mat align(16),
screen_size: [2]f32 align(8),
cell_size: [2]f32 align(8),
grid_size: [2]u16 align(4),
grid_padding: [4]f32 align(16),
padding_extend: PaddingExtend align(4),
min_contrast: f32 align(4),
cursor_pos: [2]u16 align(4),
cursor_color: [4]u8 align(4),
bg_color: [4]u8 align(4),
bools: Bools align(4),
pub const Bools = packed struct(u32) {
cursor_wide: bool,
use_display_p3: bool,
use_linear_blending: bool,
use_linear_correction: bool = false,
_padding: u28 = 0,
};
pub const PaddingExtend = packed struct(u32) {
left: bool = false,
right: bool = false,
up: bool = false,
down: bool = false,
_padding: u28 = 0,
};
};
pub const CellText = extern struct {
glyph_pos: [2]u32 align(8) = .{ 0, 0 },
glyph_size: [2]u32 align(8) = .{ 0, 0 },
bearings: [2]i16 align(4) = .{ 0, 0 },
grid_pos: [2]u16 align(4),
color: [4]u8 align(4),
atlas: Atlas align(1),
bools: packed struct(u8) {
no_min_contrast: bool = false,
is_cursor_glyph: bool = false,
_padding: u6 = 0,
} align(1) = .{},
pub const Atlas = enum(u8) {
grayscale = 0,
color = 1,
};
};
pub const CellBg = [4]u8;
pub const Image = extern struct {
grid_pos: [2]f32 align(8),
cell_offset: [2]f32 align(8),
source_rect: [4]f32 align(16),
dest_size: [2]f32 align(8),
};
pub const BgImage = extern struct {
opacity: f32 align(4),
info: Info align(1),
pub const Info = packed struct(u8) {
position: Position,
fit: Fit,
repeat: bool,
_padding: u1 = 0,
pub const Position = enum(u4) {
tl = 0,
tc = 1,
tr = 2,
ml = 3,
mc = 4,
mr = 5,
bl = 6,
bc = 7,
br = 8,
};
pub const Fit = enum(u2) {
contain = 0,
cover = 1,
stretch = 2,
none = 3,
};
};
};
// ---- Shaders collection ---------------------------------------------
/// Pipeline collection shape (matches `opengl/shaders.zig`). Each
/// field is the Vulkan `Pipeline` instance for that named shader.
pub const PipelineCollection = struct {
bg_color: Pipeline = undefined,
cell_bg: Pipeline = undefined,
cell_text: Pipeline = undefined,
image: Pipeline = undefined,
bg_image: Pipeline = undefined,
};
/// Top-level renderer shader state. Same shape as
/// `opengl/shaders.zig`'s `Shaders` so the generic renderer's call
/// sites work without per-backend branching.
///
/// **Stub `init`.** The current implementation returns a shell with
/// `undefined` pipelines so the comptime contract for
/// `GenericRenderer(Vulkan)` resolves and `-Drenderer=vulkan` builds.
/// The actual pipeline construction (compile each GLSL via
/// `Module.init`, build descriptor set layouts, assemble
/// `Pipeline.Options`, instantiate via `Pipeline.init`) lands in a
/// follow-up commit alongside the integration smoke test on real
/// hardware.
pub const Shaders = struct {
pipelines: PipelineCollection,
post_pipelines: []const Pipeline,
defunct: bool = false,
pub fn init(
alloc: Allocator,
post_shaders: []const [:0]const u8,
) !Shaders {
_ = alloc;
_ = post_shaders;
return .{
.pipelines = .{},
.post_pipelines = &.{},
};
}
pub fn deinit(self: *Shaders, alloc: Allocator) void {
_ = alloc;
if (self.defunct) return;
self.defunct = true;
// No pipeline destruction yet `init` returns undefined
// pipelines. Real `deinit` will iterate `inline for` over
// PipelineCollection's fields and destroy each one, plus
// free `post_pipelines`.
}
};
test {
std.testing.refAllDecls(@This());
}