I’m making an abstraction over Vulkan based on No Graphics API and I’ve been debating how to represent gpu side pointers on cpu, here’s a small snippet of what it looks like right now in my user code so you get the idea:
const Data = extern struct {
particles: *anyopaque, // gpu pointer, read by the shader
count: u32,
};
const particles_gpu = try device.rawAlloc(count * @sizeOf(Particle), .of(Particle), .gpu);
defer device.rawFree(particles_gpu);
const data_gpu = try device.rawAlloc(@sizeOf(Data), .of(Data), .default);
defer device.rawFree(data_gpu);
// deviceToHostPointer gives you a cpu mapped pointer
const data_cpu: *Data = @ptrCast(@alignCast(device.deviceToHostPointer(data_gpu)));
data_cpu.* = .{ .particles = particles_gpu, .count = count };
// pass the gpu address to the shader
cmd.dispatch(device, data_gpu, .{ count / 64, 1, 1 });
and one line of pointer arithmetic:
const second_half = @ptrFromInt(@intFromPtr(particles_gpu) + (count / 2) * @sizeOf(Particle));
currently gpu pointers are represented as an *anyopaque but that’s just a placeholder. Gpu pointers are used a lot with this kind of api so I want it to be ergonomic. Here’s the options I have though of:
1. Struct with a u64 and methods
pub fn GpuPtr(comptime T: type, ...alignment and other pointer attributes etc) type {
return struct {
addr: u64,
pub fn add(p: @This(), n: u64) @This() {
return .{ .addr = p.addr + n * @sizeOf(T) };
}
// sub, alignCast, cast, slice type, ...
};
}
You cant accidentally deref it so that’s good but I have to reimplement everything
pointers do as methods. And you lose a lot of the ergonomics of zig pointers.
2. Just keep using pointers
You keep all the ergonomics. But reads and writes are a bug. And you can pass cpu pointers to functions that expect gpu pointers and vice versa very easily. This doesnt work for 32bit hosts like wasm32.
3. Keep using pointers, but wrap the destination type to make it somewhat opaque
pub fn GpuOpaque(comptime T: type) type {
return extern struct {
bytes: [@sizeOf(T)]u8 align(@alignOf(T)),
};
}
// *u32 -> *GpuOpaque(u32)
GPU pointers become *GpuOpaque(T). Deref still compiles but it’s harder to mess up. This doesn’t work for 32bit hosts like wasm32.
Do you guys know better options I don’t know about?
What if we could use physical_storage_buffer on the cpu side?
One possibility at the language level would be to make *addresspace(.physical_storage_buffer) T representable in CPU code (but deref would be a compile error). This would also work for 32bit hosts if 15232’s last requirement is implemented
Here’s the what the code would look like approximately:
const Data = extern struct {
particles: [*]addrspace(.physical_storage_buffer) Particle, // gpu pointer, read by the shader
count: u32,
};
const particles_gpu: []addrspace(.physical_storage_buffer) Particle = try device.alloc(Particle, count, .gpu);
defer device.free(particles_gpu);
const data_gpu: *addrspace(.physical_storage_buffer) Data = try device.create(Data, .default);
defer device.free(data_gpu);
// deviceToHostPointer gives you a cpu mapped pointer
const data_cpu = device.deviceToHostPointer(data_gpu);
data_cpu.* = .{ .particles = particles_gpu.ptr, .count = particles_gpu.len };
// pass the gpu address to the shader
cmd.dispatch(device, data_gpu, .{ count / 64, 1, 1 });
and the line of pointer arithmetic:
const second_half = particles_gpu.ptr + count / 2;