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. Heres the options I have though of:
1. Struct with a u64 and methods
pub fn GpuPtr(comptime T: type, ...alignement 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, alignForward, cast, slice type, ...
};
}
You cant accidentaly deref it so thats 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
const data_gpu: *Data = @ptrCast(@alignCast(try device.rawAlloc(...)));
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)),
};
}
pub fn GpuPtr(comptime T: type) type {
return *u32 -> *GpuOpaque(u32)
}
GPU pointers become GpuPtr(*u32). Deref still compiles but its harder to mess
up. This doesnt 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 posibility 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
Heres the what the code would look like approximatly:
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;
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.