renderer/vulkan: VkSampler wrapper
Adds `vulkan/Sampler.zig` — wrapper around `VkSampler`, counterpart
to `opengl/Sampler.zig`. Establishes the pattern every Vulkan submodule
that needs new device-level entry points will follow:
1. Add the function-pointer field to `Device.Dispatch`.
2. Resolve it via `dl.load(...)` in `Device.init`.
3. Reference `device.dispatch.foo(...)` from the submodule.
`Sampler.Options` keeps the same shape as the OpenGL backend's
(`min_filter` / `mag_filter` / `wrap_s` / `wrap_t`) so the renderer
contract `Sampler.init(api.samplerOptions())` works against either
backend. Filter / address-mode are Vulkan-native enums backed by
`VK_FILTER_*` / `VK_SAMPLER_ADDRESS_MODE_*` values; the API
mismatch with OpenGL's GL constants stays hidden behind
`api.samplerOptions()`.
A few small Vulkan-specific decisions baked in:
- `mipmapMode = LINEAR` but `minLod == maxLod == 0`, so it's a
no-op today but forward-compatible if we ever generate atlas
mips.
- `anisotropyEnable = FALSE` — the terminal grid doesn't benefit
from anisotropy and enabling it would gate on a per-physical-
device feature toggle the apprt would have to coordinate.
- `unnormalizedCoordinates = FALSE` to keep the binding portable
across atlas / image-data / split-divider use cases.
Verification: temporarily flipped `.vulkan` switch to compile-check
via `Vulkan.zig` re-export; Sampler + the new dispatch entries
resolved clean (only failure was the expected
`renderer.Vulkan has no member 'DerivedConfig'` downstream of the
stub substitution). Reverted before commit. OpenGL build still
silent / clean.
Co-Authored-By: claude-flow <ruv@ruv.net>
pull/12846/head
parent
ac46d91085
commit
92f5ae81b4
|
|
@ -64,3 +64,4 @@
|
|||
//! See the parity branch description in `qt/PARITY.md` once it lands.
|
||||
|
||||
pub const Device = @import("vulkan/Device.zig");
|
||||
pub const Sampler = @import("vulkan/Sampler.zig");
|
||||
|
|
|
|||
|
|
@ -85,6 +85,10 @@ pub const Dispatch = struct {
|
|||
// call adds a field here and a `loadDevice` lookup in `init`.
|
||||
getDeviceQueue: std.meta.Child(vk.PFN_vkGetDeviceQueue),
|
||||
deviceWaitIdle: std.meta.Child(vk.PFN_vkDeviceWaitIdle),
|
||||
|
||||
// Sampler — used by `vulkan/Sampler.zig`.
|
||||
createSampler: std.meta.Child(vk.PFN_vkCreateSampler),
|
||||
destroySampler: std.meta.Child(vk.PFN_vkDestroySampler),
|
||||
};
|
||||
|
||||
// ---- fields ---------------------------------------------------------
|
||||
|
|
@ -243,6 +247,10 @@ pub fn init(
|
|||
try dl.load(vk.PFN_vkGetDeviceQueue, "vkGetDeviceQueue");
|
||||
const device_wait_idle =
|
||||
try dl.load(vk.PFN_vkDeviceWaitIdle, "vkDeviceWaitIdle");
|
||||
const create_sampler =
|
||||
try dl.load(vk.PFN_vkCreateSampler, "vkCreateSampler");
|
||||
const destroy_sampler =
|
||||
try dl.load(vk.PFN_vkDestroySampler, "vkDestroySampler");
|
||||
|
||||
return .{
|
||||
.platform = platform,
|
||||
|
|
@ -258,6 +266,8 @@ pub fn init(
|
|||
.getDeviceProcAddr = get_device_proc_addr,
|
||||
.getDeviceQueue = get_device_queue,
|
||||
.deviceWaitIdle = device_wait_idle,
|
||||
.createSampler = create_sampler,
|
||||
.destroySampler = destroy_sampler,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
//! Wrapper for `VkSampler` — the immutable filter / wrap configuration
|
||||
//! the GPU uses when sampling a texture.
|
||||
//!
|
||||
//! libghostty doesn't share samplers across textures (the OpenGL
|
||||
//! backend already creates one per texture-shaped need); we keep the
|
||||
//! same per-callsite ownership model so the renderer interface
|
||||
//! matches.
|
||||
//!
|
||||
//! Counterpart: `src/renderer/opengl/Sampler.zig`.
|
||||
|
||||
const Self = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const vk = @import("vulkan").c;
|
||||
|
||||
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) {
|
||||
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) {
|
||||
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,
|
||||
clamp_to_border = vk.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER,
|
||||
};
|
||||
|
||||
/// Sampler construction parameters. The same shape as the OpenGL
|
||||
/// backend's `Sampler.Options` (so generic.zig can call
|
||||
/// `Sampler.init(api.samplerOptions())` against either backend), with
|
||||
/// a `device` reference so we can call `vkCreateSampler` against the
|
||||
/// host's VkDevice without threading a global through.
|
||||
pub const Options = struct {
|
||||
device: *const Device,
|
||||
min_filter: Filter,
|
||||
mag_filter: Filter,
|
||||
wrap_s: AddressMode,
|
||||
wrap_t: AddressMode,
|
||||
};
|
||||
|
||||
pub const Error = error{
|
||||
/// `vkCreateSampler` returned a non-success status. Logged with
|
||||
/// the raw `VkResult` value.
|
||||
VulkanFailed,
|
||||
};
|
||||
|
||||
sampler: vk.VkSampler,
|
||||
device: *const Device,
|
||||
|
||||
/// Create a sampler against the host's VkDevice. The sampler is
|
||||
/// destroyed in `deinit`; libghostty owns this handle's lifetime.
|
||||
pub fn init(opts: Options) Error!Self {
|
||||
const info: vk.VkSamplerCreateInfo = .{
|
||||
.sType = vk.VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
|
||||
.pNext = null,
|
||||
.flags = 0,
|
||||
.magFilter = @intFromEnum(opts.mag_filter),
|
||||
.minFilter = @intFromEnum(opts.min_filter),
|
||||
// The glyph atlases are 2D textures without mips; the
|
||||
// renderer doesn't request mipmaps and the value here is
|
||||
// ignored when `lodMin == lodMax == 0`. Use LINEAR for
|
||||
// forward-compatibility if we ever generate atlas mips.
|
||||
.mipmapMode = vk.VK_SAMPLER_MIPMAP_MODE_LINEAR,
|
||||
.addressModeU = @intFromEnum(opts.wrap_s),
|
||||
.addressModeV = @intFromEnum(opts.wrap_t),
|
||||
// 2D textures never sample in W; the renderer ignores it. The
|
||||
// value still has to be valid — pick CLAMP_TO_EDGE.
|
||||
.addressModeW = vk.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
|
||||
.mipLodBias = 0,
|
||||
// Anisotropy is a per-physical-device feature toggle; the
|
||||
// terminal grid doesn't benefit from it and gating on the
|
||||
// feature flag adds host coordination noise. Skip.
|
||||
.anisotropyEnable = vk.VK_FALSE,
|
||||
.maxAnisotropy = 1,
|
||||
.compareEnable = vk.VK_FALSE,
|
||||
.compareOp = vk.VK_COMPARE_OP_ALWAYS,
|
||||
.minLod = 0,
|
||||
.maxLod = 0,
|
||||
.borderColor = vk.VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK,
|
||||
.unnormalizedCoordinates = vk.VK_FALSE,
|
||||
};
|
||||
|
||||
var sampler: vk.VkSampler = undefined;
|
||||
const result = opts.device.dispatch.createSampler(
|
||||
opts.device.device,
|
||||
&info,
|
||||
null,
|
||||
&sampler,
|
||||
);
|
||||
if (result != vk.VK_SUCCESS) {
|
||||
log.err("vkCreateSampler failed: result={}", .{result});
|
||||
return error.VulkanFailed;
|
||||
}
|
||||
|
||||
return .{
|
||||
.sampler = sampler,
|
||||
.device = opts.device,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Self) void {
|
||||
self.device.dispatch.destroySampler(self.device.device, self.sampler, null);
|
||||
}
|
||||
|
||||
test {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
Loading…
Reference in New Issue