diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..482e79c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+*.o
+a.out
+*.spv
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..0c71d9e
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,17 @@
+EXE = a.out
+SPIRV != find -type f '(' -name '*.frag' -o -name '*.vert' ')' | sed 's/$$/.spv/'
+
+.PHONY: all run clean
+
+all: ${EXE} ${SPIRV}
+run: ${EXE} ${SPIRV}
+ ./${EXE}
+
+clean:
+ rm -fv ${EXE} ${SPIRV}
+
+${EXE}: *.c
+ ${CC} -Os -Wall -std=c23 $< -o $@ -lSDL3
+
+%.spv: %
+ glslc -O -c "$<" -o "$@"
diff --git a/sdl_gpu_test.c b/sdl_gpu_test.c
new file mode 100644
index 0000000..d77e137
--- /dev/null
+++ b/sdl_gpu_test.c
@@ -0,0 +1,219 @@
+/* sdl_gpu_test.c -- -lSDL3 */
+
+#define SDL_MAIN_USE_CALLBACKS 1
+#include <SDL3/SDL_main.h>
+#include <SDL3/SDL_gpu.h>
+#include <stdio.h>
+#include <err.h>
+
+SDL_Window *win;
+SDL_GPUDevice *gpudev;
+SDL_GPUGraphicsPipeline *pipeline_fill;
+SDL_GPUBuffer *buf_vert;
+SDL_GPUTransferBuffer *buf_vert_xfer;
+
+float vertices[][2] = {
+ { 0.5, -0.5 },
+ { 0.5, 0.5 },
+ { -0.5, -0.5 },
+
+ { -0.5, -0.5 },
+ { -0.5, 0.5 },
+ { 0.5, 0.5 },
+};
+
+SDL_GPUShader* load_shader(
+ SDL_GPUDevice* dev,
+ const char *path,
+ SDL_GPUShaderStage stage,
+ Uint32 samplers,
+ Uint32 uniformbufs,
+ Uint32 storagebufs,
+ Uint32 storagetextures)
+{
+ size_t code_size;
+ void *code = SDL_LoadFile(path, &code_size);
+ if (!code) {
+ fprintf(stderr, "couldn't read file: %s\n", SDL_GetError());
+ return NULL;
+ }
+
+ SDL_GPUShaderCreateInfo info = {
+ .code = code,
+ .code_size = code_size,
+ .entrypoint = "main",
+ .format = SDL_GPU_SHADERFORMAT_SPIRV,
+ .stage = stage,
+ .num_samplers = samplers,
+ .num_uniform_buffers = uniformbufs,
+ .num_storage_buffers = storagebufs,
+ .num_storage_textures = storagetextures
+ };
+
+ SDL_GPUShader *sh = SDL_CreateGPUShader(dev, &info);
+ if (!sh) {
+ fprintf(stderr, "failed to create shader: %s\n", SDL_GetError());
+ return NULL;
+ }
+
+ SDL_free(code);
+ return sh;
+}
+
+#define CHECK(t, msg) do { if (!(t)) { fprintf(stderr, "%s: %s\n", msg, SDL_GetError()); return SDL_APP_FAILURE; } } while(0)
+
+SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) {
+ (void)argc;
+ (void)argv;
+ (void)appstate;
+ /* TODO: support more than just SPIR-V */
+ win = SDL_CreateWindow("Hello, world!", 1024, 768, SDL_WINDOW_RESIZABLE | SDL_WINDOW_VULKAN);
+ CHECK(win, "failed to create window");
+ gpudev = SDL_CreateGPUDevice(SDL_GPU_SHADERFORMAT_SPIRV, 0, NULL);
+ CHECK(gpudev, "failed to create gpu device");
+ CHECK(SDL_ClaimWindowForGPUDevice(gpudev, win), "failed to claim window");
+
+ SDL_GPUShader *shader_vert = load_shader(gpudev, "shader/tri.vert.spv", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0, 0, 0);
+ CHECK(shader_vert, "load vertex shader");
+ SDL_GPUShader *shader_frag = load_shader(gpudev, "shader/tri.frag.spv", SDL_GPU_SHADERSTAGE_FRAGMENT, 0, 0, 0, 0);
+ CHECK(shader_frag, "load fragment shader");
+
+ SDL_GPUBufferCreateInfo buf_desc = {
+ .usage = SDL_GPU_BUFFERUSAGE_VERTEX,
+ .size = sizeof(vertices),
+ .props = 0
+ };
+ buf_vert = SDL_CreateGPUBuffer(gpudev, &buf_desc);
+ CHECK(buf_vert, "vertex buffer");
+
+ SDL_GPUTransferBufferCreateInfo buf_xfer_desc = {
+ .usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD,
+ .size = sizeof(buf_vert),
+ .props = 0
+ };
+ buf_vert_xfer = SDL_CreateGPUTransferBuffer(gpudev, &buf_xfer_desc);
+ CHECK(buf_vert_xfer, "vertex transfer buffer");
+
+ void *map = SDL_MapGPUTransferBuffer(gpudev, buf_vert_xfer, false);
+ SDL_memcpy(map, vertices, sizeof(vertices));
+ SDL_UnmapGPUTransferBuffer(gpudev, buf_vert_xfer);
+
+ SDL_GPUCommandBuffer *cmdbuf = SDL_AcquireGPUCommandBuffer(gpudev);
+ CHECK(cmdbuf, "gpu command buffer acquisition");
+ SDL_GPUCopyPass *copypass = SDL_BeginGPUCopyPass(cmdbuf);
+ CHECK(copypass, "gpu copy pass");
+
+ SDL_GPUTransferBufferLocation src = {
+ .transfer_buffer = buf_vert_xfer,
+ .offset = 0
+ };
+
+ SDL_GPUBufferRegion dest = {
+ .buffer = buf_vert,
+ .offset = 0,
+ .size = sizeof(vertices)
+ };
+
+ SDL_UploadToGPUBuffer(copypass, &src, &dest, false);
+ SDL_EndGPUCopyPass(copypass);
+ SDL_SubmitGPUCommandBuffer(cmdbuf);
+ SDL_ReleaseGPUTransferBuffer(gpudev, buf_vert_xfer);
+
+ SDL_GPUGraphicsPipelineCreateInfo pipeline_info = {
+ .target_info = {
+ .num_color_targets = 1,
+ .color_target_descriptions = (SDL_GPUColorTargetDescription[]) {{
+ .format = SDL_GetGPUSwapchainTextureFormat(gpudev, win)
+ }},
+ },
+ .primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST,
+ .vertex_shader = shader_vert,
+ .fragment_shader = shader_frag,
+ .vertex_input_state = {
+ .num_vertex_buffers = 1,
+ .vertex_buffer_descriptions = &(SDL_GPUVertexBufferDescription) {
+ .slot = 0,
+ .pitch = sizeof(float) * 2,
+ .input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX,
+ .instance_step_rate = 0,
+ },
+ .num_vertex_attributes = 1,
+ .vertex_attributes = (SDL_GPUVertexAttribute[]) {
+ {
+ .location = 0,
+ .buffer_slot = 0,
+ .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2,
+ .offset = 0
+ }
+ }
+ },
+ .props = 0
+ };
+
+ pipeline_info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL;
+ pipeline_fill = SDL_CreateGPUGraphicsPipeline(gpudev, &pipeline_info);
+ CHECK(pipeline_fill, "graphics pipeline");
+
+ SDL_ReleaseGPUShader(gpudev, shader_frag);
+ SDL_ReleaseGPUShader(gpudev, shader_vert);
+
+ return SDL_APP_CONTINUE;
+}
+
+SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) {
+ (void)appstate;
+ switch (event->type) {
+ case SDL_EVENT_QUIT:
+ puts("Quit!");
+ return SDL_APP_SUCCESS;
+ default:
+ break;
+ }
+ return SDL_APP_CONTINUE;
+}
+
+SDL_AppResult SDL_AppIterate(void *appstate) {
+ (void)appstate;
+
+ SDL_GPUCommandBuffer *cmdbuf = SDL_AcquireGPUCommandBuffer(gpudev);
+ if (!cmdbuf) {
+ fprintf(stderr, "failed to acquire GPU command buffer: %s\n", SDL_GetError());
+ return SDL_APP_FAILURE;
+ }
+
+ SDL_GPUTexture *swapchain;
+ if (!SDL_WaitAndAcquireGPUSwapchainTexture(cmdbuf, win, &swapchain, NULL, NULL)) {
+ fprintf(stderr, "failed to acquire swapchain texture: %s\n", SDL_GetError());
+ return SDL_APP_FAILURE;
+ }
+
+ if (!swapchain) {
+ fprintf(stderr, "null swapchain texture: %s\n", SDL_GetError());
+ SDL_SubmitGPUCommandBuffer(cmdbuf);
+ return SDL_APP_FAILURE;
+ }
+
+ SDL_GPUColorTargetInfo color_target_info = { 0 };
+ color_target_info.texture = swapchain;
+ color_target_info.clear_color = (SDL_FColor) { 0, 0, 0, 1 };
+ color_target_info.load_op = SDL_GPU_LOADOP_CLEAR;
+ color_target_info.store_op = SDL_GPU_STOREOP_STORE;
+
+ SDL_GPURenderPass *render_pass = SDL_BeginGPURenderPass(cmdbuf, &color_target_info, 1, NULL);
+ SDL_BindGPUGraphicsPipeline(render_pass, pipeline_fill);
+ SDL_BindGPUVertexBuffers(render_pass, 0, &(SDL_GPUBufferBinding) { .buffer = buf_vert, .offset = 0 }, 1);
+ SDL_DrawGPUPrimitives(render_pass, sizeof(vertices) / (sizeof(float)*2), 1, 0, 0);
+ SDL_EndGPURenderPass(render_pass);
+
+ SDL_SubmitGPUCommandBuffer(cmdbuf);
+ return SDL_APP_CONTINUE;
+}
+
+void SDL_AppQuit(void *appstate, SDL_AppResult result) {
+ (void)appstate;
+ (void)result;
+ SDL_ReleaseGPUGraphicsPipeline(gpudev, pipeline_fill);
+ SDL_ReleaseWindowFromGPUDevice(gpudev, win);
+ SDL_DestroyGPUDevice(gpudev);
+ SDL_DestroyWindow(win);
+}
diff --git a/shader/tri.frag b/shader/tri.frag
new file mode 100644
index 0000000..5599421
--- /dev/null
+++ b/shader/tri.frag
@@ -0,0 +1,8 @@
+#version 450
+
+layout(location = 0) in vec4 in_pos;
+layout(location = 0) out vec4 out_color;
+
+void main(void) {
+ out_color = vec4(in_pos.xy + vec2(0.5,0.5), 0, 1.0);
+}
diff --git a/shader/tri.vert b/shader/tri.vert
new file mode 100644
index 0000000..701d7c6
--- /dev/null
+++ b/shader/tri.vert
@@ -0,0 +1,9 @@
+#version 450
+
+layout(location = 0) in vec2 in_pos;
+layout(location = 0) out vec4 out_pos;
+
+void main() {
+ gl_Position = vec4(in_pos.x, in_pos.y, 0.0, 1.0);
+ out_pos = gl_Position;
+}
|