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
parent
e936f6d2d4
commit
e9c8cb0080
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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` — GLSL→SPIR-V→VkShaderModule.
|
||||
|
||||
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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue