diff --git a/compile_commands.json b/compile_commands.json
index f944c8f..75428b9 100644
--- a/compile_commands.json
+++ b/compile_commands.json
@@ -1,21 +1,51 @@
 [
 {
   "directory": "/home/sam/Documents/Projects/brimstone",
-  "arguments": ["/usr/bin/gcc", "-c", "-g", "-O0", "-Isrc", "-o", "build/.objs/brimstone/linux/arm64/debug/src/brimstone/io/file.c.o", "src/brimstone/io/file.c"],
+  "arguments": ["/usr/bin/gcc", "-c", "-g", "-O0", "-Isrc", "-DDEBUG", "-I", "/home/sam/.xmake/packages/s/stc/v4.2/bfec6d3335d54b48969cc50946a9b5ac/include", "-I", "/home/sam/.xmake/packages/c/cglm/v0.9.4/02efbe15d36b48b6bf756a5d63445178/include", "-o", "build/.objs/brimstone/linux/arm64/debug/src/brimstone/io/file.c.o", "src/brimstone/io/file.c"],
   "file": "src/brimstone/io/file.c"
 },
 {
   "directory": "/home/sam/Documents/Projects/brimstone",
-  "arguments": ["/usr/bin/gcc", "-c", "-g", "-O0", "-Isrc", "-o", "build/.objs/brimstone/linux/arm64/debug/src/brimstone/shaders/shaders.c.o", "src/brimstone/shaders/shaders.c"],
-  "file": "src/brimstone/shaders/shaders.c"
+  "arguments": ["/usr/bin/gcc", "-c", "-g", "-O0", "-Isrc", "-DDEBUG", "-I", "/home/sam/.xmake/packages/s/stc/v4.2/bfec6d3335d54b48969cc50946a9b5ac/include", "-I", "/home/sam/.xmake/packages/c/cglm/v0.9.4/02efbe15d36b48b6bf756a5d63445178/include", "-o", "build/.objs/brimstone/linux/arm64/debug/src/brimstone/io/copy_pass.c.o", "src/brimstone/io/copy_pass.c"],
+  "file": "src/brimstone/io/copy_pass.c"
 },
 {
   "directory": "/home/sam/Documents/Projects/brimstone",
-  "arguments": ["/usr/bin/gcc", "-c", "-g", "-O0", "-Isrc", "-o", "build/.objs/brimstone/linux/arm64/debug/src/brimstone/device/device.c.o", "src/brimstone/device/device.c"],
-  "file": "src/brimstone/device/device.c"
+  "arguments": ["/usr/bin/gcc", "-c", "-g", "-O0", "-Isrc", "-DDEBUG", "-I", "/home/sam/.xmake/packages/s/stc/v4.2/bfec6d3335d54b48969cc50946a9b5ac/include", "-I", "/home/sam/.xmake/packages/c/cglm/v0.9.4/02efbe15d36b48b6bf756a5d63445178/include", "-o", "build/.objs/brimstone/linux/arm64/debug/src/brimstone/graphics/transfer_buffer.c.o", "src/brimstone/graphics/transfer_buffer.c"],
+  "file": "src/brimstone/graphics/transfer_buffer.c"
 },
 {
   "directory": "/home/sam/Documents/Projects/brimstone",
-  "arguments": ["/usr/bin/gcc", "-c", "-g", "-O0", "-Isrc", "-o", "build/.objs/brimstone/linux/arm64/debug/src/brimstone/main.c.o", "src/brimstone/main.c"],
+  "arguments": ["/usr/bin/gcc", "-c", "-g", "-O0", "-Isrc", "-DDEBUG", "-I", "/home/sam/.xmake/packages/s/stc/v4.2/bfec6d3335d54b48969cc50946a9b5ac/include", "-I", "/home/sam/.xmake/packages/c/cglm/v0.9.4/02efbe15d36b48b6bf756a5d63445178/include", "-o", "build/.objs/brimstone/linux/arm64/debug/src/brimstone/graphics/upload.c.o", "src/brimstone/graphics/upload.c"],
+  "file": "src/brimstone/graphics/upload.c"
+},
+{
+  "directory": "/home/sam/Documents/Projects/brimstone",
+  "arguments": ["/usr/bin/gcc", "-c", "-g", "-O0", "-Isrc", "-DDEBUG", "-I", "/home/sam/.xmake/packages/s/stc/v4.2/bfec6d3335d54b48969cc50946a9b5ac/include", "-I", "/home/sam/.xmake/packages/c/cglm/v0.9.4/02efbe15d36b48b6bf756a5d63445178/include", "-o", "build/.objs/brimstone/linux/arm64/debug/src/brimstone/graphics/command_buffer.c.o", "src/brimstone/graphics/command_buffer.c"],
+  "file": "src/brimstone/graphics/command_buffer.c"
+},
+{
+  "directory": "/home/sam/Documents/Projects/brimstone",
+  "arguments": ["/usr/bin/gcc", "-c", "-g", "-O0", "-Isrc", "-DDEBUG", "-I", "/home/sam/.xmake/packages/s/stc/v4.2/bfec6d3335d54b48969cc50946a9b5ac/include", "-I", "/home/sam/.xmake/packages/c/cglm/v0.9.4/02efbe15d36b48b6bf756a5d63445178/include", "-o", "build/.objs/brimstone/linux/arm64/debug/src/brimstone/graphics/shaders.c.o", "src/brimstone/graphics/shaders.c"],
+  "file": "src/brimstone/graphics/shaders.c"
+},
+{
+  "directory": "/home/sam/Documents/Projects/brimstone",
+  "arguments": ["/usr/bin/gcc", "-c", "-g", "-O0", "-Isrc", "-DDEBUG", "-I", "/home/sam/.xmake/packages/s/stc/v4.2/bfec6d3335d54b48969cc50946a9b5ac/include", "-I", "/home/sam/.xmake/packages/c/cglm/v0.9.4/02efbe15d36b48b6bf756a5d63445178/include", "-o", "build/.objs/brimstone/linux/arm64/debug/src/brimstone/graphics/device.c.o", "src/brimstone/graphics/device.c"],
+  "file": "src/brimstone/graphics/device.c"
+},
+{
+  "directory": "/home/sam/Documents/Projects/brimstone",
+  "arguments": ["/usr/bin/gcc", "-c", "-g", "-O0", "-Isrc", "-DDEBUG", "-I", "/home/sam/.xmake/packages/s/stc/v4.2/bfec6d3335d54b48969cc50946a9b5ac/include", "-I", "/home/sam/.xmake/packages/c/cglm/v0.9.4/02efbe15d36b48b6bf756a5d63445178/include", "-o", "build/.objs/brimstone/linux/arm64/debug/src/brimstone/graphics/pipeline.c.o", "src/brimstone/graphics/pipeline.c"],
+  "file": "src/brimstone/graphics/pipeline.c"
+},
+{
+  "directory": "/home/sam/Documents/Projects/brimstone",
+  "arguments": ["/usr/bin/gcc", "-c", "-g", "-O0", "-Isrc", "-DDEBUG", "-I", "/home/sam/.xmake/packages/s/stc/v4.2/bfec6d3335d54b48969cc50946a9b5ac/include", "-I", "/home/sam/.xmake/packages/c/cglm/v0.9.4/02efbe15d36b48b6bf756a5d63445178/include", "-o", "build/.objs/brimstone/linux/arm64/debug/src/brimstone/init/systems.c.o", "src/brimstone/init/systems.c"],
+  "file": "src/brimstone/init/systems.c"
+},
+{
+  "directory": "/home/sam/Documents/Projects/brimstone",
+  "arguments": ["/usr/bin/gcc", "-c", "-g", "-O0", "-Isrc", "-DDEBUG", "-I", "/home/sam/.xmake/packages/s/stc/v4.2/bfec6d3335d54b48969cc50946a9b5ac/include", "-I", "/home/sam/.xmake/packages/c/cglm/v0.9.4/02efbe15d36b48b6bf756a5d63445178/include", "-o", "build/.objs/brimstone/linux/arm64/debug/src/brimstone/main.c.o", "src/brimstone/main.c"],
   "file": "src/brimstone/main.c"
 }]
diff --git a/shader.frag b/shader.frag
deleted file mode 100644
index 084e7df..0000000
--- a/shader.frag
+++ /dev/null
@@ -1,10 +0,0 @@
-#version 460
-
-layout(location = 0) in vec4 vertColor;
-
-layout(location = 0) out vec4 fragColor;
-
-void main() {
-    fragColor = vertColor;
-}
-
diff --git a/shader.frag.spv b/shader.frag.spv
deleted file mode 100644
index 17f5fec..0000000
Binary files a/shader.frag.spv and /dev/null differ
diff --git a/shader.spv b/shader.spv
deleted file mode 100644
index 4bc0c18..0000000
Binary files a/shader.spv and /dev/null differ
diff --git a/shader.vert b/shader.vert
deleted file mode 100644
index 90a49da..0000000
--- a/shader.vert
+++ /dev/null
@@ -1,12 +0,0 @@
-#version 460
-
-layout(location = 0) in vec3 aPos;
-layout(location = 1) in vec3 aColor;
-
-layout(location = 0) out vec4 vertColor;
-
-void main() {
-    vertColor = vec4(aColor, 1.0f);
-
-    gl_Position = vec4(aPos, 1.0f);
-}
diff --git a/shader.vert.spv b/shader.vert.spv
deleted file mode 100644
index 731fc5f..0000000
Binary files a/shader.vert.spv and /dev/null differ
diff --git a/shaders/basic.frag b/shaders/basic.frag
new file mode 100644
index 0000000..06f11b4
--- /dev/null
+++ b/shaders/basic.frag
@@ -0,0 +1,13 @@
+#version 460
+
+layout(location = 0) in vec4 vertColor;
+layout(location = 1) in vec2 vertTexCoords;
+
+layout(location = 0) out vec4 fragColor;
+
+layout(set = 2, binding = 0) uniform sampler2D Sampler;
+
+void main() {
+    fragColor = texture(Sampler, vertTexCoords) * vertColor;
+}
+
diff --git a/shaders/basic.vert b/shaders/basic.vert
new file mode 100644
index 0000000..e5698ce
--- /dev/null
+++ b/shaders/basic.vert
@@ -0,0 +1,21 @@
+#version 460
+
+layout(location = 0) in vec3 aPos;
+layout(location = 1) in vec3 aColor;
+layout(location = 2) in vec2 aTexCoords;
+
+layout(location = 0) out vec4 vertColor;
+layout(location = 1) out vec2 vertTexCoords;
+
+layout(set = 1, binding = 0) uniform UBO {
+    mat4 viewproj;
+    mat4 model;
+};
+
+void main() {
+    vertTexCoords = aTexCoords;
+    vertColor = vec4(aColor, 1.0f);
+    
+    mat4 mvp = viewproj * model;
+    gl_Position = mvp * vec4(aPos, 1.0f);
+}
diff --git a/src/brimstone/graphics/command_buffer.c b/src/brimstone/graphics/command_buffer.c
index a240b7e..ed68476 100644
--- a/src/brimstone/graphics/command_buffer.c
+++ b/src/brimstone/graphics/command_buffer.c
@@ -1,13 +1,10 @@
-#include <SDL3/SDL_gpu.h>
 #include <pch.h>
 
 SDL_GPUCommandBuffer* CommandBufferAcquire(SDL_GPUDevice* device) {
     SDL_assert(device != NULL);
 
     SDL_GPUCommandBuffer* command_buffer = SDL_AcquireGPUCommandBuffer(device);
-    if(command_buffer == NULL) {
-        SDL_Log("Failed to acquire command buffer: %s", SDL_GetError());
-    }
+    LogAssertSDL(command_buffer != NULL, "Failed to acquire GPU command buffer");
 
     return command_buffer;
 }
@@ -16,9 +13,7 @@ bool CommandBufferSubmit(SDL_GPUCommandBuffer* command_buffer) {
     SDL_assert(command_buffer != NULL);
 
     bool success = SDL_SubmitGPUCommandBuffer(command_buffer);
-    if(!success) {
-        SDL_Log("Failed to submit command buffer: %s", SDL_GetError());
-    }
+    LogAssertSDL(success, "Failed to submit command buffer");
 
     return success;
 }
diff --git a/src/brimstone/graphics/device.c b/src/brimstone/graphics/device.c
index 33304b1..224c293 100644
--- a/src/brimstone/graphics/device.c
+++ b/src/brimstone/graphics/device.c
@@ -1,14 +1,12 @@
-#include <SDL3/SDL_gpu.h>
-#include <SDL3/SDL_log.h>
 #include <pch.h>
 
 SDL_GPUDevice* DeviceCreate() {
+    // TODO: once SDL3 and adjacent tools have stable releases shadercross and such will be easier to add
+    // and other backends and shader formats can be used
     SDL_GPUDevice* device = SDL_CreateGPUDevice(SDL_GPU_SHADERFORMAT_SPIRV, true, NULL);
-    if(device == NULL) {
-        SDL_Log("Failed to create device: %s", SDL_GetError());
-    } else {
-        SDL_Log("Created GPU device using %s", SDL_GetGPUDeviceDriver(device));
-    }
+    LogAssertSDL(device != NULL, "Failed to create device");
+
+    SDL_Log("Created GPU device using %s", SDL_GetGPUDeviceDriver(device));
 
     return device;
 }
diff --git a/src/brimstone/graphics/pipeline.c b/src/brimstone/graphics/pipeline.c
index 87bfb57..9b5e839 100644
--- a/src/brimstone/graphics/pipeline.c
+++ b/src/brimstone/graphics/pipeline.c
@@ -27,7 +27,7 @@ SDL_GPUGraphicsPipeline* GraphicsPipelineCreate(SDL_GPUDevice* device, SDL_Windo
 			.pitch = sizeof(Vertex),
 		    },
                 },
-		.num_vertex_attributes = 2,
+		.num_vertex_attributes = 3,
 		.vertex_attributes = (SDL_GPUVertexAttribute[]){
                     {
 		        .buffer_slot = 0,
@@ -40,6 +40,12 @@ SDL_GPUGraphicsPipeline* GraphicsPipelineCreate(SDL_GPUDevice* device, SDL_Windo
                         .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3,
                         .location = 1,
                         .offset = offsetof(Vertex, color),
+                    },
+                    {
+                        .buffer_slot = 0,
+                        .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2,
+                        .location = 2,
+                        .offset = offsetof(Vertex, texcoords),
                     }
                 }
 	    },
@@ -47,9 +53,18 @@ SDL_GPUGraphicsPipeline* GraphicsPipelineCreate(SDL_GPUDevice* device, SDL_Windo
 	    .vertex_shader = vertex_shader,
 	    .fragment_shader = fragment_shader,
         });
-    if(pipeline == NULL) {
-        SDL_Log("Failed to create pipeline: %s", SDL_GetError());
-    }
+    LogAssertSDL(pipeline != NULL, "Failed to create pipeline");
 
     return pipeline;
 }
+
+void GraphicsPipelineBind(SDL_GPURenderPass* render_pass, SDL_GPUGraphicsPipeline* pipeline) {
+    SDL_BindGPUGraphicsPipeline(render_pass, pipeline);
+}
+
+void GraphicsPipelineFree(SDL_GPUDevice* device, SDL_GPUGraphicsPipeline* pipeline) {
+    SDL_assert(device != NULL);
+    SDL_assert(pipeline != NULL);
+
+    SDL_ReleaseGPUGraphicsPipeline(device, pipeline);
+}
diff --git a/src/brimstone/graphics/pipeline.h b/src/brimstone/graphics/pipeline.h
index c2cca64..f9b4f6c 100644
--- a/src/brimstone/graphics/pipeline.h
+++ b/src/brimstone/graphics/pipeline.h
@@ -5,5 +5,7 @@
 
 SDL_GPUGraphicsPipeline* GraphicsPipelineCreate(SDL_GPUDevice* device, SDL_Window* window,
     SDL_GPUShader* vertex_shader, SDL_GPUShader* fragment_shader);
+void GraphicsPipelineBind(SDL_GPURenderPass* render_pass, SDL_GPUGraphicsPipeline* pipeline);
+void GraphicsPipelineFree(SDL_GPUDevice* device, SDL_GPUGraphicsPipeline* pipeline);
 
 #endif
diff --git a/src/brimstone/graphics/shaders.c b/src/brimstone/graphics/shaders.c
index 6f7130c..c84a102 100644
--- a/src/brimstone/graphics/shaders.c
+++ b/src/brimstone/graphics/shaders.c
@@ -1,9 +1,12 @@
 #include <pch.h>
 
-SDL_GPUShader* ShaderLoad(SDL_GPUDevice* device, const char* filename, SDL_GPUShaderStage stage) {
+SDL_GPUShader* ShaderLoad(SDL_GPUDevice* device, const char* filename, SDL_GPUShaderStage stage,
+    uint32_t num_uniform_buffers, uint32_t num_samplers) {
     SDL_assert(device != NULL);
 
-    FileData file = FileRead(filename);
+    FileData file = FileReadBinary(filename);
+    LogAssertErrnof(file.data != NULL, "Failed to read shader file: %s", filename);
+
     SDL_GPUShader* shader = SDL_CreateGPUShader(device,
         &(SDL_GPUShaderCreateInfo){
             .code = file.data,
@@ -11,10 +14,20 @@ SDL_GPUShader* ShaderLoad(SDL_GPUDevice* device, const char* filename, SDL_GPUSh
             .stage = stage,
             .format = SDL_GPU_SHADERFORMAT_SPIRV,
             .entrypoint = "main",
+            .num_uniform_buffers = num_uniform_buffers,
+            .num_samplers = num_samplers,
         });
-    if(shader == NULL) {
-        SDL_Log("Failed to create shader: %s", SDL_GetError());
-    }
+
+    FileFree(file);
+
+    LogAssertSDL(shader != NULL, "Failed to create shader");
 
     return shader;
 }
+
+void ShaderFree(SDL_GPUDevice* device, SDL_GPUShader* shader) {
+    SDL_assert(device != NULL);
+    SDL_assert(shader != NULL);
+
+    SDL_ReleaseGPUShader(device, shader);
+}
diff --git a/src/brimstone/graphics/shaders.h b/src/brimstone/graphics/shaders.h
index 1f24c9d..747c901 100644
--- a/src/brimstone/graphics/shaders.h
+++ b/src/brimstone/graphics/shaders.h
@@ -3,6 +3,9 @@
 
 #include <SDL3/SDL_gpu.h>
 
-SDL_GPUShader* ShaderLoad(SDL_GPUDevice* device, const char* filename, SDL_GPUShaderStage stage);
+// SDL_GPUShader* ShaderLoad(SDL_GPUDevice* device, const char* filename, SDL_GPUShaderStage stage);
+SDL_GPUShader* ShaderLoad(SDL_GPUDevice* device, const char* filename, SDL_GPUShaderStage stage,
+    uint32_t num_uniform_buffers, uint32_t num_samplers);
+void ShaderFree(SDL_GPUDevice* device, SDL_GPUShader* shader);
 
 #endif
diff --git a/src/brimstone/graphics/transfer_buffer.c b/src/brimstone/graphics/transfer_buffer.c
new file mode 100644
index 0000000..49ad05b
--- /dev/null
+++ b/src/brimstone/graphics/transfer_buffer.c
@@ -0,0 +1,33 @@
+#include <pch.h>
+
+SDL_GPUTransferBuffer* TransferBufferCreate(
+    SDL_GPUDevice* device, SDL_GPUTransferBufferUsage usage, size_t size) {
+    SDL_assert(device != NULL);
+
+    SDL_GPUTransferBuffer* transfer_buffer = SDL_CreateGPUTransferBuffer(device,
+        &(SDL_GPUTransferBufferCreateInfo){
+            .usage = usage,
+            .size = size,
+        });
+    LogAssertSDL(transfer_buffer != NULL, "Failed to create GPU transfer buffer");
+
+    return transfer_buffer;
+}
+
+void* TransferBufferMap(SDL_GPUDevice* device, SDL_GPUTransferBuffer* transfer_buffer) {
+    void* mapped_buffer = SDL_MapGPUTransferBuffer(device, transfer_buffer, false);
+    LogAssertSDL(mapped_buffer != NULL, "Failed to map GPU transfer buffer");
+
+    return mapped_buffer;
+}
+
+void TransferBufferUnmap(SDL_GPUDevice* device, SDL_GPUTransferBuffer* transfer_buffer) {
+    SDL_UnmapGPUTransferBuffer(device, transfer_buffer);
+}
+
+void TransferBufferFree(SDL_GPUDevice* device, SDL_GPUTransferBuffer* transfer_buffer) {
+    SDL_assert(device != NULL);
+    SDL_assert(transfer_buffer != NULL);
+
+    SDL_ReleaseGPUTransferBuffer(device, transfer_buffer);
+}
diff --git a/src/brimstone/graphics/transfer_buffer.h b/src/brimstone/graphics/transfer_buffer.h
new file mode 100644
index 0000000..07f7738
--- /dev/null
+++ b/src/brimstone/graphics/transfer_buffer.h
@@ -0,0 +1,12 @@
+#ifndef TRANSFER_BUFFER_H
+#define TRANSFER_BUFFER_H
+
+#include <SDL3/SDL_gpu.h>
+
+SDL_GPUTransferBuffer* TransferBufferCreate(
+    SDL_GPUDevice* device, SDL_GPUTransferBufferUsage usage, size_t size);
+void* TransferBufferMap(SDL_GPUDevice* device, SDL_GPUTransferBuffer* transfer_buffer);
+void TransferBufferUnmap(SDL_GPUDevice* device, SDL_GPUTransferBuffer* transfer_buffer);
+void TransferBufferFree(SDL_GPUDevice* device, SDL_GPUTransferBuffer* transfer_buffer);
+
+#endif
diff --git a/src/brimstone/graphics/upload.c b/src/brimstone/graphics/upload.c
index 143a5cd..8d12dc8 100644
--- a/src/brimstone/graphics/upload.c
+++ b/src/brimstone/graphics/upload.c
@@ -1,41 +1,36 @@
-#include "brimstone/graphics/command_buffer.h"
-#include <SDL3/SDL_gpu.h>
 #include <pch.h>
 
-bool UploadToBuffer(SDL_GPUDevice* device, const void* data, size_t byte_size, SDL_GPUBuffer* buffer) {
+UploadState UploadBegin(SDL_GPUDevice* device, const void* data, size_t byte_size) {
+    UploadState state = { 0 };
+
+    state.transfer_buffer = TransferBufferCreate(device, SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD, byte_size);
+    void* mapped_buffer = TransferBufferMap(device, state.transfer_buffer);
+    memcpy(mapped_buffer, data, byte_size);
+    TransferBufferUnmap(device, state.transfer_buffer);
+
+    state.command_buffer = CommandBufferAcquire(device);
+    state.copy_pass = CopyPassBegin(state.command_buffer);
+
+    return state;
+}
+
+void UploadEnd(SDL_GPUDevice* device, const UploadState* state) {
+    CopyPassEnd(state->copy_pass);
+    CommandBufferSubmit(state->command_buffer);
+    TransferBufferFree(device, state->transfer_buffer);
+}
+
+void UploadToBuffer(SDL_GPUDevice* device, const void* data, size_t byte_size, SDL_GPUBuffer* buffer) {
     SDL_assert(device != NULL);
     SDL_assert(data != NULL);
+    SDL_assert(byte_size > 0);
     SDL_assert(buffer != NULL);
 
-    SDL_GPUTransferBuffer* transfer_buffer = SDL_CreateGPUTransferBuffer(device,
-        &(SDL_GPUTransferBufferCreateInfo){
-            .usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD,
-            .size = byte_size,
-        });
-    if(transfer_buffer == NULL) {
-        SDL_Log("Failed to create transfer buffer: %s", SDL_GetError());
-        return false;
-    }
+    UploadState upload_state = UploadBegin(device, data, byte_size);
 
-    void* mapped_buffer = SDL_MapGPUTransferBuffer(device, transfer_buffer, false);
-    if(mapped_buffer == NULL) {
-        SDL_Log("Failed to map GPU transfer buffer: %s", SDL_GetError());
-        return false;
-    }
-
-    memcpy(mapped_buffer, data, byte_size);
-
-    SDL_UnmapGPUTransferBuffer(device, transfer_buffer);
-
-    SDL_GPUCommandBuffer* copy_command_buffer = CommandBufferAcquire(device);
-    SDL_assert(copy_command_buffer != NULL);
-
-    SDL_GPUCopyPass* copy_pass = CopyPassBegin(copy_command_buffer);
-    SDL_assert(copy_pass != NULL);
-
-    SDL_UploadToGPUBuffer(copy_pass,
+    SDL_UploadToGPUBuffer(upload_state.copy_pass,
         &(SDL_GPUTransferBufferLocation){
-            .transfer_buffer = transfer_buffer,
+            .transfer_buffer = upload_state.transfer_buffer,
             .offset = 0,
         },
         &(SDL_GPUBufferRegion){
@@ -45,9 +40,35 @@ bool UploadToBuffer(SDL_GPUDevice* device, const void* data, size_t byte_size, S
         },
         false);
 
-    CopyPassEnd(copy_pass);
-    bool submit_success = CommandBufferSubmit(copy_command_buffer);
-    SDL_ReleaseGPUTransferBuffer(device, transfer_buffer);
-
-    return submit_success;
+    UploadEnd(device, &upload_state);
+}
+
+void UploadToTexture(SDL_GPUDevice* device, SDL_Surface* surface, SDL_GPUTexture* texture) {
+    UploadToTextureRaw(device, surface->pixels, surface->w, surface->h, texture);
+}
+
+void UploadToTextureRaw(
+    SDL_GPUDevice* device, const void* pixels, int width, int height, SDL_GPUTexture* texture) {
+    SDL_assert(device != NULL);
+    SDL_assert(pixels != NULL);
+    SDL_assert(width > 0);
+    SDL_assert(height > 0);
+    SDL_assert(texture != NULL);
+
+    UploadState upload_state = UploadBegin(device, pixels, width * height * 4);
+
+    SDL_UploadToGPUTexture(upload_state.copy_pass,
+        &(SDL_GPUTextureTransferInfo){
+            .transfer_buffer = upload_state.transfer_buffer,
+            .offset = 0,
+        },
+        &(SDL_GPUTextureRegion){
+            .texture = texture,
+            .w = width,
+            .h = height,
+            .d = 1,
+        },
+        false);
+
+    UploadEnd(device, &upload_state);
 }
diff --git a/src/brimstone/graphics/upload.h b/src/brimstone/graphics/upload.h
index 6e00f22..b296f35 100644
--- a/src/brimstone/graphics/upload.h
+++ b/src/brimstone/graphics/upload.h
@@ -3,6 +3,15 @@
 
 #include <SDL3/SDL_gpu.h>
 
-bool UploadToBuffer(SDL_GPUDevice* device, const void* data, size_t byte_size, SDL_GPUBuffer* buffer);
+typedef struct {
+    SDL_GPUTransferBuffer* transfer_buffer;
+    SDL_GPUCopyPass* copy_pass;
+    SDL_GPUCommandBuffer* command_buffer;
+} UploadState;
+
+void UploadToBuffer(SDL_GPUDevice* device, const void* data, size_t byte_size, SDL_GPUBuffer* buffer);
+void UploadToTexture(SDL_GPUDevice* device, SDL_Surface* surface, SDL_GPUTexture* texture);
+void UploadToTextureRaw(
+    SDL_GPUDevice* device, const void* pixels, int width, int height, SDL_GPUTexture* texture);
 
 #endif
diff --git a/src/brimstone/graphics/vertex.h b/src/brimstone/graphics/vertex.h
index d685077..284d4f8 100644
--- a/src/brimstone/graphics/vertex.h
+++ b/src/brimstone/graphics/vertex.h
@@ -8,6 +8,9 @@ typedef struct {
     struct {
         float r, g, b;
     } color;
+    struct {
+        float x, y;
+    } texcoords;
 } Vertex;
 
 #endif
diff --git a/src/brimstone/init/systems.c b/src/brimstone/init/systems.c
index 2fe182f..07b84b4 100644
--- a/src/brimstone/init/systems.c
+++ b/src/brimstone/init/systems.c
@@ -1,8 +1,6 @@
 #include <pch.h>
 
 void InitSystems() {
-    if(!SDL_Init(SDL_INIT_VIDEO)) {
-        SDL_Log("Failed to initialize SDL: %s", SDL_GetError());
-        exit(EXIT_FAILURE);
-    }
+    bool success = SDL_Init(SDL_INIT_VIDEO);
+    LogAssertSDL(success, "Failed to initialize SDL");
 }
diff --git a/src/brimstone/io/copy_pass.c b/src/brimstone/io/copy_pass.c
index c1f2f15..d8830f8 100644
--- a/src/brimstone/io/copy_pass.c
+++ b/src/brimstone/io/copy_pass.c
@@ -4,9 +4,7 @@ SDL_GPUCopyPass* CopyPassBegin(SDL_GPUCommandBuffer* copy_command_buffer) {
     SDL_assert(copy_command_buffer != NULL);
 
     SDL_GPUCopyPass* copy_pass = SDL_BeginGPUCopyPass(copy_command_buffer);
-    if(copy_pass == NULL) {
-        SDL_Log("Failed to begin copy pass: %s", SDL_GetError());
-    }
+    LogAssertSDL(copy_pass != NULL, "Failed to begin copy pass");
 
     return copy_pass;
 }
diff --git a/src/brimstone/io/file.c b/src/brimstone/io/file.c
index 16ded48..cdfad6a 100644
--- a/src/brimstone/io/file.c
+++ b/src/brimstone/io/file.c
@@ -1,32 +1,42 @@
-#include "pch.h"
+#include <pch.h>
 
-FileData FileRead(const char* filename) {
-    FILE* f = fopen(filename, "r");
-    if(f == NULL) {
+FileData FileRead(const char* filename, const char* mode) {
+    FILE* file = fopen(filename, mode);
+    if(file == NULL) {
         perror(filename);
         return (FileData){ 0 };
     }
-    fseek(f, 0, SEEK_END);
-    size_t length = ftell(f);
-    rewind(f);
+    fseek(file, 0, SEEK_END);
+    size_t length = ftell(file);
+    rewind(file);
 
     uint8_t* buffer = malloc(length);
     if(buffer == NULL) {
         perror("Failed to allocate memory");
         return (FileData){ 0 };
     }
-    size_t read_count = fread(buffer, sizeof(char), length, f);
+
+    size_t read_count = fread(buffer, sizeof(char), length, file);
+    fclose(file);
     if(read_count != length) {
         perror("Failed to read entire file");
         return (FileData){ 0 };
     }
 
     return (FileData){
-        .data = buffer,
         .length = length,
+        .data = buffer,
     };
 }
 
+FileData FileReadText(const char* filename) {
+    return FileRead(filename, "r");
+}
+
+FileData FileReadBinary(const char* filename) {
+    return FileRead(filename, "rb");
+}
+
 void FileFree(FileData file) {
     free(file.data);
 }
diff --git a/src/brimstone/io/file.h b/src/brimstone/io/file.h
index b596f90..9c388a7 100644
--- a/src/brimstone/io/file.h
+++ b/src/brimstone/io/file.h
@@ -5,11 +5,14 @@
 #include <stdint.h>
 
 typedef struct {
-    uint8_t* data;
     size_t length;
+    uint8_t* data;
 } FileData;
 
-FileData FileRead(const char* filename);
+FileData FileRead(const char* filename, const char* mode);
+FileData FileReadText(const char* filename);
+FileData FileReadBinary(const char* filename);
+
 void FileFree(FileData file);
 
 #endif
diff --git a/src/brimstone/main.c b/src/brimstone/main.c
index 896d36f..ffac10f 100644
--- a/src/brimstone/main.c
+++ b/src/brimstone/main.c
@@ -1,43 +1,78 @@
-#include "brimstone/graphics/pipeline.h"
 #include <pch.h>
 
 int main(int argc, char** argv) {
     InitSystems();
 
     SDL_GPUDevice* device = DeviceCreate();
-    SDL_assert(device != NULL);
-
-    SDL_Window* window = SDL_CreateWindow("Brimstone", 800, 600, 0);
-    if(window == NULL) {
-        SDL_Log("SDL_CreateWindow: %s", SDL_GetError());
-        exit(EXIT_FAILURE);
-    }
-    SDL_ClaimWindowForGPUDevice(device, window);
+    SDL_Window* window = SDL_CreateWindow("Brimstone", 800, 600, SDL_WINDOW_RESIZABLE);
+    LogAssertSDL(window != NULL, "Failed to create window");
+    bool success = SDL_ClaimWindowForGPUDevice(device, window);
+    LogAssertSDL(success, "Failed to claim window for device");
 
     Vertex vertices[] = {
-        { -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f },
-        { 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f },
-        { 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f },
+        { { -0.5f, 0.5f, 0.0f }, { 1.0f, 1.0f, 1.0f }, { 0.0f, 0.0f } },   // top left
+        { { -0.5f, -0.5f, 0.0f }, { 1.0f, 0.0f, 0.0f }, { 0.0f, 1.0f } },  // bottom left
+        { { 0.5f, -0.5f, 0.0f }, { 1.0f, 0.0f, 0.0f }, { 1.0f, 1.0f } },   // bottom right
+        { { 0.5f, 0.5f, 0.0f }, { 1.0f, 1.0f, 1.0f }, { 1.0f, 0.0f } },    // top right
     };
+    int indices[] = { 0, 1, 2, 2, 3, 0 };
 
     SDL_GPUBuffer* vertex_buffer = SDL_CreateGPUBuffer(device,
         &(SDL_GPUBufferCreateInfo){
             .usage = SDL_GPU_BUFFERUSAGE_VERTEX,
             .size = sizeof(vertices),
         });
+    LogAssertSDL(vertex_buffer != NULL, "Failed to create GPU buffer");
+    UploadToBuffer(device, vertices, sizeof(vertices), vertex_buffer);
 
-    bool upload_successful = UploadToBuffer(device, vertices, sizeof(vertices), vertex_buffer);
-    SDL_assert(upload_successful);
+    SDL_GPUBuffer* index_buffer = SDL_CreateGPUBuffer(device,
+        &(SDL_GPUBufferCreateInfo){
+            .usage = SDL_GPU_BUFFERUSAGE_INDEX,
+            .size = sizeof(indices),
+        });
+    LogAssertSDL(index_buffer != NULL, "Failed to create GPU buffer");
+    UploadToBuffer(device, indices, sizeof(indices), index_buffer);
 
-    SDL_GPUShader* vertex_shader = ShaderLoad(device, "shader.vert.spv", SDL_GPU_SHADERSTAGE_VERTEX);
-    SDL_assert(vertex_shader != NULL);
+    SDL_GPUShader* vertex_shader = ShaderLoad(
+        device, "shaders/basic.vert.spv", SDL_GPU_SHADERSTAGE_VERTEX, 1, 0);
 
-    SDL_GPUShader* fragment_shader = ShaderLoad(device, "shader.frag.spv", SDL_GPU_SHADERSTAGE_FRAGMENT);
-    SDL_assert(fragment_shader != NULL);
+    SDL_GPUShader* fragment_shader = ShaderLoad(
+        device, "shaders/basic.frag.spv", SDL_GPU_SHADERSTAGE_FRAGMENT, 0, 1);
 
     SDL_GPUGraphicsPipeline* pipeline = GraphicsPipelineCreate(
         device, window, vertex_shader, fragment_shader);
-    SDL_assert(pipeline != NULL);
+
+    ShaderFree(device, vertex_shader);
+    ShaderFree(device, fragment_shader);
+
+    const char* path = "image.jxl";
+    SDL_Surface* image = IMG_Load(path);
+    LogAssertSDLf(image != NULL, "Failed to load image: %s", path);
+
+    if(image->format != SDL_PIXELFORMAT_ABGR8888) {
+        SDL_Surface* converted = SDL_ConvertSurface(image, SDL_PIXELFORMAT_ABGR8888);
+        SDL_DestroySurface(image);
+        image = converted;
+    }
+    SDL_GPUTexture* texture = SDL_CreateGPUTexture(device,
+        &(SDL_GPUTextureCreateInfo){
+            .type = SDL_GPU_TEXTURETYPE_2D,
+            .format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM,
+            .width = image->w,
+            .height = image->h,
+            .layer_count_or_depth = 1,
+            .num_levels = 1,
+            .usage = SDL_GPU_TEXTUREUSAGE_SAMPLER,
+        });
+    LogAssertSDL(texture != NULL, "Failed to create texture");
+    UploadToTexture(device, image, texture);
+    SDL_GPUSampler* sampler = SDL_CreateGPUSampler(device, &(SDL_GPUSamplerCreateInfo){ 0 });
+    LogAssertSDL(sampler != NULL, "Failed to create sampler");
+
+    struct {
+        mat4 viewproj;
+        mat4 model;
+    } ubo;
 
     bool running = true;
     SDL_Event event;
@@ -50,29 +85,44 @@ int main(int argc, char** argv) {
             }
         }
 
-        SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device);
-        if(cmd == NULL) {
-            SDL_Log("SDL_AcquireGPUCommandBuffer: %s", SDL_GetError());
-            exit(EXIT_FAILURE);
-        }
+        int width, height;
+        SDL_GetWindowSize(window, &width, &height);
+
+        mat4 view = GLM_MAT4_IDENTITY_INIT;
+        mat4 projection = GLM_MAT4_IDENTITY_INIT;
+
+        glm_perspective(glm_rad(70.0f), (float)width / (float)height, 0.1f, 1000.0f, projection);
+        glm_translate(view, (vec3){ 0.0f, 0.0f, -2.0f });
+        glm_mat4_mul(projection, view, ubo.viewproj);
+
+        SDL_GPUCommandBuffer* cmd = CommandBufferAcquire(device);
 
         SDL_GPUTexture* swapchain;
         uint32_t swapchain_width, swapchain_height;
-        SDL_AcquireGPUSwapchainTexture(cmd, window, &swapchain, &swapchain_width, &swapchain_height);
+        success = SDL_AcquireGPUSwapchainTexture(
+            cmd, window, &swapchain, &swapchain_width, &swapchain_height);
+        LogAssertSDL(success, "Failed to acquire swapchain texture");
+        if(swapchain == NULL) {
+            SDL_CancelGPUCommandBuffer(cmd);
+            continue;
+        }
+
+        glm_mat4_identity(ubo.model);
+        /*glm_rotate(
+            ubo.model, glm_rad(sinf(SDL_GetTicks() / 200.0f) * 50.0f), (vec3){ 0.5f, 1.0f, 0.2f });*/
+
+        SDL_PushGPUVertexUniformData(cmd, 0, &ubo, sizeof(ubo));
 
         SDL_GPURenderPass* render_pass = SDL_BeginGPURenderPass(cmd,
             &(SDL_GPUColorTargetInfo){
                 .texture = swapchain,
                 .load_op = SDL_GPU_LOADOP_CLEAR,
-                .clear_color = (SDL_FColor){ .r = 0.1f, .g = 0.3f, .b = 0.4f, .a = 1.0f },
+                .clear_color = (SDL_FColor){ 0.1f, 0.3f, 0.4f, 1.0f },
             },
             1, NULL);
-        if(render_pass == NULL) {
-            SDL_Log("SDL_BeginGPURenderPass: %s", SDL_GetError());
-            exit(EXIT_FAILURE);
-        }
+        LogAssertSDL(render_pass != NULL, "Failed to begin render pass");
 
-        SDL_BindGPUGraphicsPipeline(render_pass, pipeline);
+        GraphicsPipelineBind(render_pass, pipeline);
         SDL_BindGPUVertexBuffers(render_pass, 0,
             (SDL_GPUBufferBinding[]){
                 {
@@ -81,15 +131,25 @@ int main(int argc, char** argv) {
                 },
             },
             1);
-        SDL_DrawGPUPrimitives(render_pass, 3, 1, 0, 0);
-
+        SDL_BindGPUIndexBuffer(render_pass,
+            &(SDL_GPUBufferBinding){
+                .buffer = index_buffer,
+                .offset = 0,
+            },
+            1);
+        SDL_BindGPUFragmentSamplers(render_pass, 0,
+            &(SDL_GPUTextureSamplerBinding){
+                .texture = texture,
+                .sampler = sampler,
+            },
+            1);
+        SDL_DrawGPUIndexedPrimitives(render_pass, sizeof(indices), 1, 0, 0, 0);
         SDL_EndGPURenderPass(render_pass);
 
-        if(!SDL_SubmitGPUCommandBuffer(cmd)) {
-            SDL_Log("SDL_SubmitGPUCommandBuffer: %s", SDL_GetError());
-            exit(EXIT_FAILURE);
-        }
+        CommandBufferSubmit(cmd);
     }
 
+    GraphicsPipelineFree(device, pipeline);
+
     return 0;
 }
diff --git a/src/brimstone/misc/log.h b/src/brimstone/misc/log.h
new file mode 100644
index 0000000..a8585f7
--- /dev/null
+++ b/src/brimstone/misc/log.h
@@ -0,0 +1,41 @@
+#ifndef LOG_H
+#define LOG_H
+
+#include <SDL3/SDL_assert.h>
+#include <SDL3/SDL_error.h>
+#include <SDL3/SDL_log.h>
+#include <errno.h>
+
+#define LogInfof(fmt, ...) SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, fmt, __VA_ARGS__)
+#define LogDebugf(fmt, ...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, fmt, __VA_ARGS__)
+#define LogWarnf(fmt, ...) SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, fmt, __VA_ARGS__)
+#define LogErrorf(fmt, ...) SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, fmt, __VA_ARGS__)
+#define LogCriticalf(fmt, ...) SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, fmt, __VA_ARGS__)
+#define LogTracef(fmt, ...) SDL_LogTrace(SDL_LOG_CATEGORY_APPLICATION, fmt, __VA_ARGS__)
+#define LogVerbosef(fmt, ...) SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, fmt, __VA_ARGS__)
+
+#define LogAssertf(condition, fmt, ...)                                                                 \
+    do {                                                                                                \
+        if(!(condition)) {                                                                              \
+            LogErrorf(fmt, __VA_ARGS__);                                                                \
+            SDL_assert(condition);                                                                      \
+        }                                                                                               \
+    } while(0)
+
+#define LogAssert(condition, msg) LogAssertf(condition, "%s", msg)
+#define LogAssertSDLf(condition, fmt, ...) LogAssertf(condition, fmt ": %s", __VA_ARGS__, SDL_GetError())
+#define LogAssertErrnof(condition, fmt, ...)                                                            \
+    LogAssertf(condition, fmt ": %s", __VA_ARGS__, strerror(errno))
+
+#define LogAssertSDL(condition, msg) LogAssertSDLf(condition, "%s", msg)
+#define LogAssertErrno(condition, msg) LogAssertErrnof(condition, "%s", msg)
+
+#define LogInfo(msg) LogInfof("%s", msg)
+#define LogDebug(msg) LogDebugf("%s", msg)
+#define LogWarn(msg) LogWarnf("%s", msg)
+#define LogError(msg) LogErrorf("%s", msg)
+#define LogCritical(msg) LogCritical("%s", msg)
+#define LogTrace(msg) LogTrace("%s", msg)
+#define LogVerbose(msg) LogVerbose("%s", msg)
+
+#endif
diff --git a/src/brimstone/misc/malloc.c b/src/brimstone/misc/malloc.c
new file mode 100644
index 0000000..5d1f790
--- /dev/null
+++ b/src/brimstone/misc/malloc.c
@@ -0,0 +1,21 @@
+#include <pch.h>
+
+/*#define i_type TagMap
+#define i_key_str
+#define i_val void*
+#include <stc/cmap.h>
+
+TagMap tags = { 0 };*/
+
+void* Malloc(size_t size) {
+    return MallocTag(size, "untagged");
+}
+
+void* MallocTag(size_t size, const char* tag) {
+    MallocHeader* ptr = malloc(sizeof(MallocHeader) + size);
+    LogAssertErrnof(ptr != NULL, "Failed to allocate %zu bytes (%s)", size, tag);
+
+    ptr->size = size;
+
+    return ptr + 1;
+}
diff --git a/src/brimstone/misc/malloc.h b/src/brimstone/misc/malloc.h
new file mode 100644
index 0000000..c3de6b3
--- /dev/null
+++ b/src/brimstone/misc/malloc.h
@@ -0,0 +1,13 @@
+#ifndef MALLOC_H
+#define MALLOC_H
+
+#include <stddef.h>
+
+typedef struct {
+    size_t size;
+} MallocHeader;
+
+void* Malloc(size_t size);
+void* MallocTag(size_t size, const char* tag);
+
+#endif
diff --git a/src/pch.h b/src/pch.h
index dfc7a79..83dbd2e 100644
--- a/src/pch.h
+++ b/src/pch.h
@@ -1,31 +1,27 @@
 #ifndef PCH_H
 #define PCH_H
 
-#include <assert.h>
 #include <stddef.h>
 #include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
 
 #include <SDL3/SDL.h>
-#include <SDL3/SDL_assert.h>
-#include <SDL3/SDL_error.h>
-#include <SDL3/SDL_events.h>
-#include <SDL3/SDL_gpu.h>
-#include <SDL3/SDL_init.h>
-#include <SDL3/SDL_log.h>
-#include <SDL3/SDL_main.h>
-#include <SDL3/SDL_pixels.h>
-#include <SDL3/SDL_video.h>
+#include <SDL3_image/SDL_image.h>
 
 #include <brimstone/graphics/command_buffer.h>
 #include <brimstone/graphics/device.h>
 #include <brimstone/graphics/pipeline.h>
 #include <brimstone/graphics/shaders.h>
+#include <brimstone/graphics/transfer_buffer.h>
 #include <brimstone/graphics/upload.h>
 #include <brimstone/graphics/vertex.h>
 #include <brimstone/init/systems.h>
 #include <brimstone/io/copy_pass.h>
 #include <brimstone/io/file.h>
+#include <brimstone/misc/log.h>
+#include <brimstone/misc/malloc.h>
+
+#include <cglm/cglm.h>
 
 #endif
diff --git a/xmake.lua b/xmake.lua
index 8608525..df5a4d2 100644
--- a/xmake.lua
+++ b/xmake.lua
@@ -1,12 +1,21 @@
 add_rules("mode.debug", "mode.release")
 
-add_requires("stc")
+add_requires("stc", "cglm")
+add_requires("glslang", {configs = {binaryonly = true}})
 
 target("brimstone")
     set_kind("binary")
-    set_rundir(".")
-    add_files("src/**.c")
-    add_links("SDL3")
-    add_includedirs("src")
     set_pcheader("src/pch.h")
-    add_packages("stc")
+    set_rundir("build")
+    
+    add_files("src/**.c")
+    add_links("SDL3", "SDL3_image")
+    add_includedirs("src")
+    add_packages("stc", "glslang", "cglm")
+
+    add_rules("utils.glsl2spv", {outputdir = "build/shaders"})
+    add_files("shaders/**.vert", "shaders/**.frag")
+
+    if is_mode("debug") then
+        add_defines("DEBUG")
+    end