From bde72b0a1576cc30a14513ac4dedbbd08eb647bf Mon Sep 17 00:00:00 2001 From: Noah Metz Date: Sun, 2 Aug 2026 22:20:56 -0600 Subject: [PATCH] Converted to render graph, and made hex region list dynamic --- client/Makefile | 19 +- client/include/camera.h | 2 + client/include/draw.h | 9 + client/include/engine.h | 2 + client/include/hex.h | 30 +- client/include/render_graph.h | 104 +++++++ client/include/ui.h | 7 + client/shader/hex.vert | 2 +- client/shader/hex2.frag | 7 + client/shader/hex2.vert | 5 + client/shader/hex_common.glsl | 6 +- client/shader/hex_highlight.vert | 2 +- client/shader/point.vert | 2 +- client/src/camera.c | 16 +- client/src/draw.c | 242 +++------------- client/src/editor.c | 4 +- client/src/engine.c | 17 +- client/src/hex.c | 103 +++++-- client/src/render_graph.c | 468 +++++++++++++++++++++++++++++++ 19 files changed, 794 insertions(+), 253 deletions(-) create mode 100644 client/include/render_graph.h create mode 100644 client/shader/hex2.frag create mode 100644 client/shader/hex2.vert create mode 100644 client/src/render_graph.c diff --git a/client/Makefile b/client/Makefile index 5d05a41..5ce6eaa 100644 --- a/client/Makefile +++ b/client/Makefile @@ -5,18 +5,20 @@ LDFLAGS = -lfreetype -lz -lglfw -lvulkan -ldl -Xlinker -rpath -Xlinker /opt/home CFLAGS += $(shell pkg-config --cflags lua) LDFLAGS += $(shell pkg-config --libs lua) -ENGINE_SOURCES = src/engine.c src/draw.c src/ui.c src/ui_lua.c src/events.c src/camera.c src/gpu.c src/hex.c src/hsv.c lib/spng.c lib/vma.cpp +ENGINE_SOURCES = src/engine.c src/draw.c src/render_graph.c src/ui.c src/ui_lua.c src/events.c src/camera.c src/gpu.c src/hex.c src/hsv.c lib/spng.c lib/vma.cpp APP_SOURCES = src/main.c $(ENGINE_SOURCES) EDITOR_SOURCES = src/editor_main.c src/editor.c src/editor_lua.c $(ENGINE_SOURCES) TEST_SOURCES = test/hsv.c $(ENGINE_SOURCES) TEST_EDITOR_SOURCES = test/editor.c src/editor.c src/editor_lua.c $(ENGINE_SOURCES) +PERF_SOURCES = test/perf.c $(ENGINE_SOURCES) APP_OBJECTS = $(addsuffix .o, $(basename $(APP_SOURCES))) EDITOR_OBJECTS = $(addsuffix .o, $(basename $(EDITOR_SOURCES))) TEST_OBJECTS = $(addsuffix .o, $(basename $(TEST_SOURCES))) TEST_EDITOR_OBJECTS = $(addsuffix .o, $(basename $(TEST_EDITOR_SOURCES))) +PERF_OBJECTS = $(addsuffix .o, $(basename $(PERF_SOURCES))) -ALL_OBJECTS = $(sort $(APP_OBJECTS) $(EDITOR_OBJECTS) $(TEST_OBJECTS) $(TEST_EDITOR_OBJECTS)) +ALL_OBJECTS = $(sort $(APP_OBJECTS) $(EDITOR_OBJECTS) $(TEST_OBJECTS) $(TEST_EDITOR_OBJECTS) $(PERF_OBJECTS)) DEP_FILES = $(ALL_OBJECTS:.o=.d) VERT_SPV = $(addsuffix .vert.spv, $(basename $(wildcard shader/*.vert))) @@ -66,10 +68,17 @@ test/test_hsv: $(TEST_OBJECTS) test/test_editor: $(TEST_EDITOR_OBJECTS) | $(SPV_FILES) $(CXX) $(CFLAGS) $(LDFLAGS) -o $@ $^ -.PHONY: test -test: test/test_hsv test/test_editor +test/test_perf: $(PERF_OBJECTS) + $(CXX) $(CFLAGS) $(LDFLAGS) -o $@ $^ + +.PHONY: test perf +test: test/test_hsv test/test_editor test/test_perf ./test/test_hsv ./test/test_editor + ./test/test_perf + +perf: test/test_perf + ./test/test_perf %.o: %.cpp $(CXX) $(CFLAGS) -std=c++17 -Wno-nullability-completeness -Wno-unused-parameter -Wno-missing-field-initializers -Wno-unused-private-field -Wno-unused-variable -c -o $@ $< @@ -89,7 +98,7 @@ compile_commands.json: Makefile clean: rm -f $(SPV_FILES) rm -f $(ALL_OBJECTS) $(DEP_FILES) - rm -f roleplay editor test/test_hsv test/test_editor + rm -f roleplay editor test/test_hsv test/test_editor test/test_perf rm -rf $(EXTRA_DEBUG_REQUIREMENTS) clean_compdb: diff --git a/client/include/camera.h b/client/include/camera.h index 36c41fe..5bdbf85 100644 --- a/client/include/camera.h +++ b/client/include/camera.h @@ -62,6 +62,8 @@ typedef struct CameraStruct { VkBuffer gpu_buffer[MAX_FRAMES_IN_FLIGHT]; VmaAllocation gpu_buffer_memory[MAX_FRAMES_IN_FLIGHT]; VkDeviceAddress gpu_address[MAX_FRAMES_IN_FLIGHT]; + + uint32_t rg_node; // handle returned by render_graph_add_camera_node, UINT32_MAX when not registered } Camera; VkResult create_camera(RenderContext* gpu, Camera* camera); diff --git a/client/include/draw.h b/client/include/draw.h index c915e63..f706cdd 100644 --- a/client/include/draw.h +++ b/client/include/draw.h @@ -5,7 +5,16 @@ #include "ui.h" #include "hex.h" +// Forward declaration so draw_frame can accept RenderGraph* without pulling in +// the full render_graph.h include chain here. +typedef struct RenderGraphStruct RenderGraph; + +void record_hex_draw(VkCommandBuffer, HexContext*, VkDeviceAddress, double, uint32_t); +void record_container_draw(VkCommandBuffer, RenderContext*, UIContext*, double, uint32_t); +void record_ui_compute(VkCommandBuffer, UIContext*, uint32_t); + VkResult draw_frame( + RenderGraph* graph, RenderContext* context, UIContext* ui, HexContext* hex, diff --git a/client/include/engine.h b/client/include/engine.h index dca394b..491b2e2 100644 --- a/client/include/engine.h +++ b/client/include/engine.h @@ -7,6 +7,7 @@ #include "hex.h" #include "events.h" #include "camera.h" +#include "render_graph.h" typedef struct ClientContextStruct ClientContext; @@ -53,6 +54,7 @@ struct ClientContextStruct { HexContext hex; EventBus events; Camera camera; + RenderGraph render_graph; void* app_data; app_frame_function app_frame; diff --git a/client/include/hex.h b/client/include/hex.h index 32824db..26ba5f8 100644 --- a/client/include/hex.h +++ b/client/include/hex.h @@ -7,15 +7,13 @@ #define MAX_RAYS 10 // Initial capacity, not a hard cap - ensure_highlight_capacity/ -// ensure_point_capacity (hex.c) grow HexContext.highlights/points on -// demand, same "start small, double when a fixed pool fills up" pattern as -// ui.h's CONTAINER_MIN_DRAWABLES/CONTAINER_MIN_STRINGS. The last slot of -// whatever the current capacity is stays reserved for the hover preview -// (see editor.c). +// ensure_point_capacity/ensure_region_capacity (hex.c) grow their +// respective buffers on demand, same "start small, double when full" +// pattern as ui.h's CONTAINER_MIN_DRAWABLES/CONTAINER_MIN_STRINGS. +// The last highlight/point slot is reserved for the hover preview (editor.c). #define MIN_HIGHLIGHTS 16 #define MIN_POINTS 16 - -#define MAX_LOADED_REGIONS 2500 +#define MIN_REGIONS 16 #define REGION_SIZE 10 #define REGION_HEX_COUNT (3*REGION_SIZE*(REGION_SIZE-1)+1) #define HEX_X 0.75 @@ -100,26 +98,31 @@ typedef struct GPUHexContextStruct { VkDeviceAddress rays; VkDeviceAddress points; VkDeviceAddress highlights; - VkDeviceAddress regions[MAX_LOADED_REGIONS]; + VkDeviceAddress regions; // device address of VkDeviceAddress[] region list } GPUHexContext; typedef struct HexContextStruct { VkDeviceAddress address[MAX_FRAMES_IN_FLIGHT]; VkBuffer context[MAX_FRAMES_IN_FLIGHT]; - VmaAllocation context_memory[MAX_FRAMES_IN_FLIGHT]; + VmaAllocation context_memory[MAX_FRAMES_IN_FLIGHT]; VkBuffer rays[MAX_FRAMES_IN_FLIGHT]; VkBuffer points[MAX_FRAMES_IN_FLIGHT]; VkBuffer highlights[MAX_FRAMES_IN_FLIGHT]; + VkBuffer regions_buf[MAX_FRAMES_IN_FLIGHT]; VmaAllocation rays_memory[MAX_FRAMES_IN_FLIGHT]; VmaAllocation points_memory[MAX_FRAMES_IN_FLIGHT]; VmaAllocation highlights_memory[MAX_FRAMES_IN_FLIGHT]; + VmaAllocation regions_mem[MAX_FRAMES_IN_FLIGHT]; - // Current allocated size of points[]/highlights[] (in elements, same for - // both frames-in-flight) - grows via ensure_point_capacity/ - // ensure_highlight_capacity, independent of each other. + // Current allocated size (in elements) for each sub-buffer. + // All grow on demand via ensure_*_capacity; regions also tracks the + // highest occupied slot (max_active_slot) to bound the draw call. uint32_t cap_points; uint32_t cap_highlights; + uint32_t cap_regions; + uint32_t active_region_count; + uint32_t max_active_slot; GPURay rays_buffer[MAX_RAYS]; @@ -130,7 +133,7 @@ typedef struct HexContextStruct { GraphicsPipeline point_pipeline; GraphicsPipeline ray_pipeline; - HexRegion* regions[MAX_LOADED_REGIONS]; + HexRegion** regions; GPUHexContext data; } HexContext; @@ -160,6 +163,7 @@ void destroy_hex_context( // see editor.c), so this is just retire-old/create-new/patch-address. VkResult ensure_highlight_capacity(HexContext* context, RenderContext* gpu, uint32_t needed); VkResult ensure_point_capacity(HexContext* context, RenderContext* gpu, uint32_t needed); +VkResult ensure_region_capacity(HexContext* context, RenderContext* gpu, uint32_t needed); VkResult set_hex_region( HexRegion* region, diff --git a/client/include/render_graph.h b/client/include/render_graph.h new file mode 100644 index 0000000..9d4010d --- /dev/null +++ b/client/include/render_graph.h @@ -0,0 +1,104 @@ +#ifndef RENDER_GRAPH_H +#define RENDER_GRAPH_H + +#include +#include +#include "gpu.h" + +typedef struct UIContextStruct UIContext; +typedef struct HexContextStruct HexContext; + +typedef uint32_t RGResource; +#define RG_RESOURCE_SWAPCHAIN UINT32_MAX + +typedef struct RGResourceUsageStruct { + RGResource resource[MAX_FRAMES_IN_FLIGHT]; + VkPipelineStageFlags stage; + VkAccessFlags access; + VkImageLayout layout; +} RGResourceUsage; + +typedef enum { + RG_NODE_INPUT = 0, + RG_NODE_COMPUTE = 1, + RG_NODE_RENDER = 2, + RG_NODE_DISPLAY = 3, +} RGNodeType; + +typedef void (*RGRecordFn)( + VkCommandBuffer, + void* userdata, + RenderContext*, + UIContext*, + HexContext*, + uint32_t image_index, + uint32_t frame, + double time); + +typedef struct RGBarrierStruct { + VkImageMemoryBarrier barrier; + VkPipelineStageFlags src_stage; + VkPipelineStageFlags dst_stage; + RGResource resource[MAX_FRAMES_IN_FLIGHT]; +} RGBarrier; + +#define RG_MAX_NODES 64 +#define RG_MAX_RESOURCES_PER_NODE 16 + +typedef struct RGNodeStruct { + RGNodeType type; + RGResourceUsage reads[RG_MAX_RESOURCES_PER_NODE]; + uint32_t read_count; + RGResourceUsage writes[RG_MAX_RESOURCES_PER_NODE]; + uint32_t write_count; + RGBarrier pre_barriers[RG_MAX_RESOURCES_PER_NODE]; + uint32_t pre_barrier_count; + RGRecordFn record; + void* userdata; + bool active; +} RGNode; + +typedef struct RenderGraphStruct { + RGNode nodes[RG_MAX_NODES]; + uint32_t node_count; + uint32_t topo_order[RG_MAX_NODES]; + uint32_t topo_count; + bool compiled; +} RenderGraph; + +// Returns stable node handle (index). Copies reads/writes arrays internally. +uint32_t render_graph_add_node( + RenderGraph*, + RGNodeType, + RGResourceUsage* reads, uint32_t read_count, + RGResourceUsage* writes, uint32_t write_count, + RGRecordFn, void* userdata); + +// Permanent-node helpers — call once at startup from run_app. +// Each registers the appropriate node with the correct callback and resource declarations. +uint32_t render_graph_add_ui_compute_node(RenderGraph*); +uint32_t render_graph_add_ui_composite_node(RenderGraph*); +uint32_t render_graph_add_display_node(RenderGraph*); + +// Convenience: registers a camera's hex-draw RENDER node with correct resource declarations. +struct CameraStruct; +uint32_t render_graph_add_camera_node(RenderGraph*, struct CameraStruct*); + +void render_graph_remove_node(RenderGraph*, uint32_t handle); + +// Derives topo order and image barriers from declared resource usages. +void render_graph_compile(RenderGraph*, UIContext* ui); + +// Records all nodes into their respective command buffers in topo order. +// COMPUTE nodes → compute_cmd. RENDER/DISPLAY nodes → graphics_cmd. +// Caller handles queue submit and semaphore chain. +VkResult render_graph_execute( + RenderGraph*, + VkCommandBuffer graphics_cmd, + VkCommandBuffer compute_cmd, + RenderContext*, UIContext*, HexContext*, + uint32_t image_index, double time); + +void render_graph_invalidate(RenderGraph*); + +#endif diff --git a/client/include/ui.h b/client/include/ui.h index 9b6fda9..cf3223b 100644 --- a/client/include/ui.h +++ b/client/include/ui.h @@ -134,6 +134,7 @@ typedef struct EventBusStruct EventBus; // Forward declaration only (no #include "camera.h") - see camera.h's // matching note on why these two headers don't include each other. typedef struct CameraStruct Camera; +typedef struct RenderGraphStruct RenderGraph; struct ContainerStruct { VkBuffer container[MAX_FRAMES_IN_FLIGHT]; @@ -270,6 +271,12 @@ struct UIContextStruct { uint32_t active_element; double cursor[2]; + + // Non-owning pointer to the client's render graph; set by run_app after + // render_graph init. camera_display / camera_undisplay use this to + // register / remove camera RENDER nodes without a signature change. + RenderGraph* render_graph; + bool render_graph_dirty; }; VkResult create_ui_context( diff --git a/client/shader/hex.vert b/client/shader/hex.vert index 82bc537..728715c 100644 --- a/client/shader/hex.vert +++ b/client/shader/hex.vert @@ -16,7 +16,7 @@ vec4 int2color(uint color_int) { void main() { int region_index = gl_InstanceIndex/region_hex_count; - Region region = pc.context.regions[region_index]; + Region region = pc.context.regions.r[region_index]; if(uvec2(region) == uvec2(0)) { gl_Position = vec4(0, 0, 0, 0); return; diff --git a/client/shader/hex2.frag b/client/shader/hex2.frag new file mode 100644 index 0000000..5d47d6d --- /dev/null +++ b/client/shader/hex2.frag @@ -0,0 +1,7 @@ +#version 450 + +layout(location = 0) out vec4 color; + +void main() { + color = vec4(1, 1, 1, 1); +} diff --git a/client/shader/hex2.vert b/client/shader/hex2.vert new file mode 100644 index 0000000..99f475b --- /dev/null +++ b/client/shader/hex2.vert @@ -0,0 +1,5 @@ +#version 450 + +void main() { + gl_Position = vec4(0, 0, 0, 1); +} diff --git a/client/shader/hex_common.glsl b/client/shader/hex_common.glsl index 3132ab0..ad1216b 100644 --- a/client/shader/hex_common.glsl +++ b/client/shader/hex_common.glsl @@ -54,12 +54,16 @@ layout(std430, buffer_reference) readonly buffer RayList { Ray r[]; }; +layout(std430, buffer_reference) readonly buffer RegionList { + Region r[]; +}; + layout(std430, buffer_reference) readonly buffer HexContext { uint current_map; RayList rays; PointList points; HighlightList highlights; - Region regions[]; + RegionList regions; }; layout(std430, push_constant) uniform PushConstant { diff --git a/client/shader/hex_highlight.vert b/client/shader/hex_highlight.vert index 1e1eee9..a892e20 100644 --- a/client/shader/hex_highlight.vert +++ b/client/shader/hex_highlight.vert @@ -18,7 +18,7 @@ void main() { return; } if(hex_index != 0xFFFFFFFF) { - Region region = pc.context.regions[pc.context.highlights.h[gl_InstanceIndex].region]; + Region region = pc.context.regions.r[pc.context.highlights.h[gl_InstanceIndex].region]; color = pc.context.highlights.h[gl_InstanceIndex].color; float raise = pc.context.highlights.h[gl_InstanceIndex].offset; diff --git a/client/shader/point.vert b/client/shader/point.vert index 1757f82..16ac360 100644 --- a/client/shader/point.vert +++ b/client/shader/point.vert @@ -8,7 +8,7 @@ layout(location = 0) flat out vec4 color; void main() { uint hex_index = pc.context.points.p[gl_InstanceIndex].hex; uint vertex_index = pc.context.points.p[gl_InstanceIndex].vertex; - Region region = pc.context.regions[pc.context.points.p[gl_InstanceIndex].region]; + Region region = pc.context.regions.r[pc.context.points.p[gl_InstanceIndex].region]; float raise = pc.context.points.p[gl_InstanceIndex].offset; if(hex_index != 0xFFFFFFFF) { color = pc.context.points.p[gl_InstanceIndex].color; diff --git a/client/src/camera.c b/client/src/camera.c index 9c98efa..bb26466 100644 --- a/client/src/camera.c +++ b/client/src/camera.c @@ -1,5 +1,6 @@ #include "camera.h" #include "ui.h" +#include "render_graph.h" #include #include @@ -142,6 +143,13 @@ VkResult camera_display(Camera* camera, Container* container, RenderContext* gpu VK_RESULT(camera_init_texture_target(gpu, ui, camera, w, h)); } + if(ui->render_graph != NULL) { + camera->rg_node = render_graph_add_camera_node(ui->render_graph, camera); + ui->render_graph_dirty = true; + } else { + camera->rg_node = UINT32_MAX; + } + GPUDrawable bg = { .pos = {0.0f, 0.0f}, .size = {container->data.size[0], container->data.size[1]}, @@ -176,8 +184,12 @@ VkResult camera_display(Camera* camera, Container* container, RenderContext* gpu } VkResult camera_undisplay(Camera* camera, Container* container, RenderContext* gpu, UIContext* ui) { - (void)camera; - (void)ui; + if(ui->render_graph != NULL && camera->rg_node != UINT32_MAX) { + render_graph_remove_node(ui->render_graph, camera->rg_node); + camera->rg_node = UINT32_MAX; + ui->render_graph_dirty = true; + } + VkResult result = VK_SUCCESS; if(container->camera_background_drawable != UINT32_MAX) { result = ui_destroy_drawable(container, container->camera_background_drawable, gpu); diff --git a/client/src/draw.c b/client/src/draw.c index 9dd1abc..b7e7144 100644 --- a/client/src/draw.c +++ b/client/src/draw.c @@ -1,4 +1,5 @@ #include "draw.h" +#include "render_graph.h" #include "hex.h" #include "camera.h" #include "vulkan/vulkan_core.h" @@ -13,7 +14,8 @@ void record_hex_draw(VkCommandBuffer command_buffer, HexContext* hex, VkDeviceAd vkCmdPushConstants(command_buffer, hex->graphics.layout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(HexPushConstant), &push); vkCmdBindPipeline(command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, hex->graphics.pipeline); - vkCmdDraw(command_buffer, 18, REGION_HEX_COUNT*MAX_LOADED_REGIONS, 0, 0); + if(hex->active_region_count > 0) + vkCmdDraw(command_buffer, 18, REGION_HEX_COUNT*(hex->max_active_slot + 1), 0, 0); vkCmdBindPipeline(command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, hex->point_pipeline.pipeline); vkCmdDraw(command_buffer, 1, hex->cap_points, 0, 0); @@ -33,11 +35,7 @@ static void bind_ui_pipeline(VkCommandBuffer command_buffer, UIContext* ui) { vkCmdBindDescriptorSets(command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, ui->pipeline.layout, 3, 1, &ui->textures, 0, NULL); } -// UI-only pass over container_order (back to front). Camera scenes are -// rendered to offscreen textures in draw_frame before this is called; each -// camera-attached container samples its texture via a background IMAGE -// drawable created by camera_display (camera.c). -static void record_container_draw( +void record_container_draw( VkCommandBuffer command_buffer, RenderContext* gpu, UIContext* ui, @@ -75,6 +73,7 @@ void record_ui_compute(VkCommandBuffer command_buffer, UIContext* ui, uint32_t f } VkResult draw_frame( + RenderGraph* graph, RenderContext* context, UIContext* ui, HexContext* hex, @@ -87,23 +86,23 @@ VkResult draw_frame( VK_RESULT(vkWaitForFences(context->device, 1, fences, VK_TRUE, UINT64_MAX)); VK_RESULT(vkResetFences(context->device, 1, fences)); - // This frame slot's previous submission has completed, so buffers retired - // MAX_FRAMES_IN_FLIGHT frames ago can no longer be referenced by the GPU destroy_retired(context); VkCommandBufferBeginInfo begin_info = { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, - }; + }; TransferBuffer* transfer = &frame->transfers[frame->transfer_index % 2]; - if(transfer->count > 0) { + bool has_transfers = (transfer->count > 0); + + if(has_transfers) { VkCommandBuffer transfer_commands = frame->transfer_commands; VK_RESULT(vkResetCommandBuffer(transfer_commands, 0)); VK_RESULT(vkBeginCommandBuffer(transfer_commands, &begin_info)); VkDeviceSize src_offset = 0; for(uint32_t tid = 0; tid < transfer->count; tid++) { -#ifdef DEBUG_GPU_TRANSFERS +#ifdef DEBUG_GPU_TRANSFERS fprintf(stderr, "Transferring %lld bytes: ", transfer->infos[tid].size); for(VkDeviceSize i = 0; i < transfer->infos[tid].size; i++) { fprintf(stderr, "%02X", ((uint8_t*)transfer->mapped)[src_offset + i]); @@ -139,19 +138,33 @@ VkResult draw_frame( .pNext = &transfer_timeline, }; VK_RESULT(vkQueueSubmit(context->transfer_queue.handle, 1, &transfer_submit, VK_NULL_HANDLE)); + } - VkCommandBuffer compute_commands = frame->compute_commands; - VK_RESULT(vkResetCommandBuffer(compute_commands, 0)); - VK_RESULT(vkBeginCommandBuffer(compute_commands, &begin_info)); - VkMemoryBarrier compute_barrier = { - .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, - .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, - .dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, - }; - vkCmdPipelineBarrier(compute_commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &compute_barrier, 0, NULL, 0, NULL); - record_ui_compute(compute_commands, ui, context->current_frame); - VK_RESULT(vkEndCommandBuffer(compute_commands)); + uint32_t image_index; + result = vkAcquireNextImageKHR(context->device, context->swapchain, UINT64_MAX, frame->image, VK_NULL_HANDLE, &image_index); + if(result == VK_ERROR_OUT_OF_DATE_KHR) { + VK_RESULT(recreate_framebuffer(context)); + } else if(result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) { + return result; + } + + // Reset and begin both command buffers before executing the render graph, + // which records compute nodes into compute_commands and RENDER/DISPLAY nodes + // into the graphics command buffer simultaneously. + VkCommandBuffer compute_commands = frame->compute_commands; + VK_RESULT(vkResetCommandBuffer(compute_commands, 0)); + VK_RESULT(vkBeginCommandBuffer(compute_commands, &begin_info)); + VkCommandBuffer command_buffer = context->frame_command_buffers[context->current_frame]; + VK_RESULT(vkResetCommandBuffer(command_buffer, 0)); + VK_RESULT(vkBeginCommandBuffer(command_buffer, &begin_info)); + + VK_RESULT(render_graph_execute(graph, command_buffer, compute_commands, context, ui, hex, image_index, time)); + + VK_RESULT(vkEndCommandBuffer(compute_commands)); + VK_RESULT(vkEndCommandBuffer(command_buffer)); + + if(has_transfers) { frame->compute_index += 1; VkPipelineStageFlags compute_wait_stages[] = {VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT}; VkTimelineSemaphoreSubmitInfo compute_timeline = { @@ -175,191 +188,6 @@ VkResult draw_frame( VK_RESULT(vkQueueSubmit(context->transfer_queue.handle, 1, &compute_submit, VK_NULL_HANDLE)); } - uint32_t image_index; - result = vkAcquireNextImageKHR(context->device, context->swapchain, UINT64_MAX, frame->image, VK_NULL_HANDLE, &image_index); - if(result == VK_ERROR_OUT_OF_DATE_KHR) { - VK_RESULT(recreate_framebuffer(context)); - } else if(result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) { - return result; - } - - VkCommandBuffer command_buffer = context->frame_command_buffers[context->current_frame]; - VK_RESULT(vkResetCommandBuffer(command_buffer, 0)); - VK_RESULT(vkBeginCommandBuffer(command_buffer, &begin_info)); - - VkImageSubresourceRange color_range = { - .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, - .levelCount = 1, - .layerCount = 1, - }; - - // Render each camera's hex scene to its offscreen texture before the - // main scene pass. Iterate container_order to discover cameras; skip - // duplicates (same camera referenced by multiple containers). - // Cameras are independent so this order carries no semantic dependency. - for(uint32_t o = 0; o < ui->container_order_count; o++) { - Container* c = &ui->containers[ui->container_order[o]]; - Camera* cam = c->camera; - if(cam == NULL || !cam->has_texture_target) continue; - - bool already_rendered = false; - for(uint32_t p = 0; p < o; p++) { - if(ui->containers[ui->container_order[p]].camera == cam) { - already_rendered = true; - break; - } - } - if(already_rendered) continue; - - uint32_t f = context->current_frame; - Texture* target = &ui->texture_slots[cam->texture.texture_slot[f]]; - - VkExtent2D cam_extent = {cam->texture.width, cam->texture.height}; - VkViewport cam_viewport = { - .width = (float)cam_extent.width, - .height = (float)cam_extent.height, - .maxDepth = 1.0f, - .minDepth = 0.0f, - }; - vkCmdSetViewport(command_buffer, 0, 1, &cam_viewport); - VkRect2D cam_scissor = {.extent = cam_extent}; - vkCmdSetScissor(command_buffer, 0, 1, &cam_scissor); - - VkImageMemoryBarrier cam_acquire_barrier = { - .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, - .srcAccessMask = 0, - .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, - .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, - .newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = target->image, - .subresourceRange = color_range, - }; - vkCmdPipelineBarrier(command_buffer, - VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, - VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - 0, 0, NULL, 0, NULL, 1, &cam_acquire_barrier); - - VkRenderingAttachmentInfo cam_color = { - .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, - .imageView = target->view, - .imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, - .storeOp = VK_ATTACHMENT_STORE_OP_STORE, - .clearValue = {.color = {{0.0f, 0.0f, 0.0f, 0.0f}}}, - }; - VkRenderingAttachmentInfo cam_depth = { - .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, - .imageView = cam->texture.depth_image_view[f], - .imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, - .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, - .storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE, - .clearValue = {.depthStencil = {1.0f, 0}}, - }; - VkRenderingInfo cam_rendering = { - .sType = VK_STRUCTURE_TYPE_RENDERING_INFO, - .renderArea = {{0, 0}, cam_extent}, - .layerCount = 1, - .colorAttachmentCount = 1, - .pColorAttachments = &cam_color, - .pDepthAttachment = &cam_depth, - }; - vkCmdBeginRendering(command_buffer, &cam_rendering); - record_hex_draw(command_buffer, hex, cam->gpu_address[f], time, f); - vkCmdEndRendering(command_buffer); - - VkImageMemoryBarrier cam_read_barrier = { - .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, - .srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, - .dstAccessMask = VK_ACCESS_SHADER_READ_BIT, - .oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - .newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = target->image, - .subresourceRange = color_range, - }; - vkCmdPipelineBarrier(command_buffer, - VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, - 0, 0, NULL, 0, NULL, 1, &cam_read_barrier); - } - - VkViewport viewport = { - .width = context->swapchain_extent.width, - .height = context->swapchain_extent.height, - .maxDepth = 1.0f, - .minDepth = 0.0f, - }; - vkCmdSetViewport(command_buffer, 0, 1, &viewport); - - VkRect2D scissor = { - .extent = context->swapchain_extent, - }; - vkCmdSetScissor(command_buffer, 0, 1, &scissor); - - VkImageMemoryBarrier acquire_barrier = { - .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, - .srcAccessMask = 0, - .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, - .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, - .newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = context->swapchain_images[image_index], - .subresourceRange = color_range, - }; - vkCmdPipelineBarrier(command_buffer, - VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, - VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - 0, 0, NULL, 0, NULL, 1, &acquire_barrier); - - VkRenderingAttachmentInfo color_attachment = { - .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, - .imageView = context->swapchain_image_views[image_index], - .imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, - .storeOp = VK_ATTACHMENT_STORE_OP_STORE, - .clearValue = {.color = {{0.0f, 0.0f, 0.0f, 0.0f}}}, - }; - VkRenderingAttachmentInfo depth_attachment = { - .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, - .imageView = context->depth_image_view, - .imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, - .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, - .storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE, - .clearValue = {.depthStencil = {1.0f, 0}}, - }; - VkRenderingInfo scene_rendering = { - .sType = VK_STRUCTURE_TYPE_RENDERING_INFO, - .renderArea = {{0, 0}, context->swapchain_extent}, - .layerCount = 1, - .colorAttachmentCount = 1, - .pColorAttachments = &color_attachment, - .pDepthAttachment = &depth_attachment, - }; - vkCmdBeginRendering(command_buffer, &scene_rendering); - record_container_draw(command_buffer, context, ui, time, context->current_frame); - vkCmdEndRendering(command_buffer); - - VkImageMemoryBarrier present_barrier = { - .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, - .srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, - .dstAccessMask = 0, - .oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - .newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = context->swapchain_images[image_index], - .subresourceRange = color_range, - }; - vkCmdPipelineBarrier(command_buffer, - VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, - 0, 0, NULL, 0, NULL, 1, &present_barrier); - VK_RESULT(vkEndCommandBuffer(command_buffer)); - VkPipelineStageFlags wait_stages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT}; VkSemaphore wait_semaphores[] = {frame->image, frame->compute}; VkSemaphore signal_semaphores[] = {frame->render}; diff --git a/client/src/editor.c b/client/src/editor.c index 4dc14e4..6cbcd83 100644 --- a/client/src/editor.c +++ b/client/src/editor.c @@ -134,7 +134,7 @@ uint32_t add_hex_region(ClientContext* context) { set_hex_region(region, &context->hex, &context->render); uint32_t i = 0; - for(; i < MAX_LOADED_REGIONS; i++) { + for(; i < context->hex.cap_regions; i++) { if(region == context->hex.regions[i]) { break; } @@ -364,7 +364,7 @@ static uint32_t resolve_vertex_group( for(uint32_t i = 0; i < 2; i++) { uint32_t actual_region; first_matching_region(n_region_coord[i], r->data.y, &actual_region, &context->hex); - if(actual_region == MAX_LOADED_REGIONS) continue; + if(actual_region == UINT32_MAX) continue; out_region[count] = actual_region; out_hex[count] = n_hex[i]; out_vertex[count] = n_vertex[i]; diff --git a/client/src/engine.c b/client/src/engine.c index c505e71..93eb087 100644 --- a/client/src/engine.c +++ b/client/src/engine.c @@ -3,6 +3,7 @@ #include "gpu.h" #include "draw.h" #include "camera.h" +#include "render_graph.h" #include #include @@ -257,7 +258,12 @@ int app_main(ClientContext* context) { } } - VkResult result = draw_frame(&context->render, &context->ui, &context->hex, frame_time); + if(context->ui.render_graph_dirty) { + render_graph_compile(&context->render_graph, &context->ui); + context->ui.render_graph_dirty = false; + } + + VkResult result = draw_frame(&context->render_graph, &context->render, &context->ui, &context->hex, frame_time); if(result != VK_SUCCESS) { fprintf(stderr, "draw_frame error: %s\n", string_VkResult(result)); glfwDestroyWindow(context->window); @@ -349,6 +355,15 @@ int run_app( if(event_bus_init(&context->events, context->ui.lua) != VK_SUCCESS) return -5; context->ui.events = &context->events; + // Link render graph into UIContext so camera_display/undisplay can register nodes. + context->ui.render_graph = &context->render_graph; + + // Register the three permanent nodes (camera nodes are added by camera_display). + render_graph_add_ui_compute_node(&context->render_graph); + render_graph_add_ui_composite_node(&context->render_graph); + render_graph_add_display_node(&context->render_graph); + context->ui.render_graph_dirty = true; + if(app_startup != NULL) app_startup(context); int result = app_main(context); diff --git a/client/src/hex.c b/client/src/hex.c index ea48853..985a840 100644 --- a/client/src/hex.c +++ b/client/src/hex.c @@ -745,10 +745,16 @@ VkResult create_hex_context( VK_RESULT(create_hex_highlight_pipeline(gpu, &context->highlight_pipeline)); VK_RESULT(create_point_pipeline(gpu, &context->point_pipeline)); + context->regions = calloc(MIN_REGIONS, sizeof(HexRegion*)); + if(context->regions == NULL) return VK_ERROR_OUT_OF_HOST_MEMORY; + context->cap_regions = MIN_REGIONS; + context->active_region_count = 0; + context->max_active_slot = 0; + memset(&context->data, 0, sizeof(GPUHexContext)); for(uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { VK_RESULT(create_storage_buffer( - gpu->allocator, + gpu->allocator, 0, sizeof(GPUHexContext), &context->context[i], @@ -774,6 +780,13 @@ VkResult create_hex_context( sizeof(GPUHighlight)*MIN_HIGHLIGHTS, &context->highlights[i], &context->highlights_memory[i])); + + VK_RESULT(create_storage_buffer( + gpu->allocator, + 0, + sizeof(VkDeviceAddress)*MIN_REGIONS, + &context->regions_buf[i], + &context->regions_mem[i])); } context->cap_points = MIN_POINTS; context->cap_highlights = MIN_HIGHLIGHTS; @@ -834,6 +847,15 @@ VkResult create_hex_context( sizeof(VkDeviceAddress), i, gpu)); + + VkDeviceAddress regions_addr = buffer_address(gpu->device, context->regions_buf[i]); + VK_RESULT(add_transfer( + ®ions_addr, + context->context[i], + offsetof(GPUHexContext, regions), + sizeof(VkDeviceAddress), + i, + gpu)); } return VK_SUCCESS; @@ -875,6 +897,34 @@ VkResult ensure_point_capacity(HexContext* context, RenderContext* gpu, uint32_t return VK_SUCCESS; } +VkResult ensure_region_capacity(HexContext* context, RenderContext* gpu, uint32_t needed) { + VkResult result; + if(needed <= context->cap_regions) return VK_SUCCESS; + + uint32_t new_cap = context->cap_regions; + while(new_cap < needed) new_cap *= 2; + + for(uint32_t f = 0; f < MAX_FRAMES_IN_FLIGHT; f++) { + retire_buffer(context->regions_buf[f], context->regions_mem[f], gpu); + VK_RESULT(create_storage_buffer(gpu->allocator, 0, sizeof(VkDeviceAddress)*new_cap, &context->regions_buf[f], &context->regions_mem[f])); + VkDeviceAddress list_addr = buffer_address(gpu->device, context->regions_buf[f]); + VK_RESULT(add_transfer(&list_addr, context->context[f], offsetof(GPUHexContext, regions), sizeof(VkDeviceAddress), f, gpu)); + + for(uint32_t i = 0; i <= context->max_active_slot; i++) { + VkDeviceAddress addr = (context->regions[i] != NULL) ? context->regions[i]->address : 0; + VK_RESULT(add_transfer(&addr, context->regions_buf[f], sizeof(VkDeviceAddress)*i, sizeof(VkDeviceAddress), f, gpu)); + } + } + + HexRegion** new_regions = calloc(new_cap, sizeof(HexRegion*)); + if(new_regions == NULL) return VK_ERROR_OUT_OF_HOST_MEMORY; + memcpy(new_regions, context->regions, sizeof(HexRegion*) * context->cap_regions); + free(context->regions); + context->regions = new_regions; + context->cap_regions = new_cap; + return VK_SUCCESS; +} + void destroy_hex_context(RenderContext* gpu, HexContext* context) { vkDestroyPipeline(gpu->device, context->graphics.pipeline, NULL); vkDestroyPipelineLayout(gpu->device, context->graphics.layout, NULL); @@ -888,24 +938,26 @@ void destroy_hex_context(RenderContext* gpu, HexContext* context) { // Direct teardown rather than free_hex_region: its GPU-address-nulling // transfer is pointless work when the context's own buffers are about to // be destroyed right below, unread. - for(uint32_t i = 0; i < MAX_LOADED_REGIONS; i++) { + for(uint32_t i = 0; i < context->cap_regions; i++) { if(context->regions[i] == NULL) continue; vmaDestroyBuffer(gpu->allocator, context->regions[i]->region, context->regions[i]->region_memory); free(context->regions[i]); context->regions[i] = NULL; } + free(context->regions); for(uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { vmaDestroyBuffer(gpu->allocator, context->context[i], context->context_memory[i]); vmaDestroyBuffer(gpu->allocator, context->rays[i], context->rays_memory[i]); vmaDestroyBuffer(gpu->allocator, context->points[i], context->points_memory[i]); vmaDestroyBuffer(gpu->allocator, context->highlights[i], context->highlights_memory[i]); + vmaDestroyBuffer(gpu->allocator, context->regions_buf[i], context->regions_mem[i]); } } VkResult set_hex_region(HexRegion* region, HexContext* hex, RenderContext* gpu) { uint32_t index = UINT32_MAX; - for(uint32_t i = 0; i < MAX_LOADED_REGIONS; i++) { + for(uint32_t i = 0; i < hex->cap_regions; i++) { if(hex->regions[i] == region) { index = i; break; @@ -928,16 +980,23 @@ VkResult free_hex_region( uint32_t region_index, HexContext* hex, RenderContext* gpu) { - if(hex->regions[region_index] == NULL) return VK_ERROR_VALIDATION_FAILED_EXT; + if(region_index >= hex->cap_regions || hex->regions[region_index] == NULL) + return VK_ERROR_VALIDATION_FAILED_EXT; free(hex->regions[region_index]); hex->regions[region_index] = NULL; + hex->active_region_count--; + + if(region_index == hex->max_active_slot) { + while(hex->max_active_slot > 0 && hex->regions[hex->max_active_slot] == NULL) + hex->max_active_slot--; + } VkDeviceAddress null_address = 0x00; return add_transfers( &null_address, - hex->context, - offsetof(GPUHexContext, regions) + sizeof(VkDeviceAddress)*region_index, + hex->regions_buf, + sizeof(VkDeviceAddress)*region_index, sizeof(VkDeviceAddress), gpu); } @@ -953,18 +1012,17 @@ VkResult allocate_hex_region( VkResult result; uint32_t i = 0; - for(; i < MAX_LOADED_REGIONS; i++) { - if(hex->regions[i] == NULL) { - hex->regions[i] = malloc(sizeof(HexRegion)); - *region = hex->regions[i]; - break; - } + for(; i < hex->cap_regions; i++) { + if(hex->regions[i] == NULL) break; } - - if(*region == NULL) { - return VK_ERROR_OUT_OF_HOST_MEMORY; + if(i == hex->cap_regions) { + VK_RESULT(ensure_region_capacity(hex, gpu, hex->cap_regions + 1)); } + hex->regions[i] = malloc(sizeof(HexRegion)); + if(hex->regions[i] == NULL) return VK_ERROR_OUT_OF_HOST_MEMORY; + *region = hex->regions[i]; + (*region)->data.position.q = q; (*region)->data.position.r = r; (*region)->data.y = y; @@ -985,10 +1043,14 @@ VkResult allocate_hex_region( (*region)->address = buffer_address(gpu->device, (*region)->region); + if(i > hex->max_active_slot || hex->active_region_count == 0) + hex->max_active_slot = i; + hex->active_region_count++; + return add_transfers( &(*region)->address, - hex->context, - offsetof(GPUHexContext, regions) + sizeof(VkDeviceAddress)*i, + hex->regions_buf, + sizeof(VkDeviceAddress)*i, sizeof(VkDeviceAddress), gpu); } @@ -1175,7 +1237,8 @@ bool ray_world_intersect( *distance = INFINITY; - for(uint32_t intersect_rid = 0; intersect_rid < MAX_LOADED_REGIONS; intersect_rid++) { + uint32_t limit = (context->active_region_count > 0) ? context->max_active_slot + 1 : 0; + for(uint32_t intersect_rid = 0; intersect_rid < limit; intersect_rid++) { HexRegion* region = context->regions[intersect_rid]; if(region == NULL) { continue; @@ -1391,7 +1454,8 @@ void hex_vertex_neighbors( } void first_matching_region(HexCoord coord, int y, uint32_t* region, HexContext* context) { - for(*region = 0; *region < MAX_LOADED_REGIONS; (*region)++) { + uint32_t limit = (context->active_region_count > 0) ? context->max_active_slot + 1 : 0; + for(*region = 0; *region < limit; (*region)++) { if(context->regions[*region] != NULL && context->regions[*region]->data.position.q == coord.q && context->regions[*region]->data.position.r == coord.r @@ -1399,4 +1463,5 @@ void first_matching_region(HexCoord coord, int y, uint32_t* region, HexContext* return; } } + *region = UINT32_MAX; } diff --git a/client/src/render_graph.c b/client/src/render_graph.c new file mode 100644 index 0000000..28ae5bf --- /dev/null +++ b/client/src/render_graph.c @@ -0,0 +1,468 @@ +#include "render_graph.h" +#include "draw.h" +#include "camera.h" +#include "ui.h" +#include +#include +#include + +// --------------------------------------------------------------------------- +// Record callbacks (shims that drive the actual render/compute functions) +// --------------------------------------------------------------------------- + +static void record_hex_draw_for_camera( + VkCommandBuffer cmd, + void* userdata, + RenderContext* gpu, + UIContext* ui, + HexContext* hex, + uint32_t image_index, + uint32_t frame, + double time) { + (void)gpu; (void)image_index; + Camera* cam = (Camera*)userdata; + + VkExtent2D extent = {cam->texture.width, cam->texture.height}; + VkViewport viewport = { + .width = (float)extent.width, + .height = (float)extent.height, + .maxDepth = 1.0f, + .minDepth = 0.0f, + }; + vkCmdSetViewport(cmd, 0, 1, &viewport); + VkRect2D scissor = {.extent = extent}; + vkCmdSetScissor(cmd, 0, 1, &scissor); + + Texture* target = &ui->texture_slots[cam->texture.texture_slot[frame]]; + + VkRenderingAttachmentInfo cam_color = { + .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, + .imageView = target->view, + .imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, + .storeOp = VK_ATTACHMENT_STORE_OP_STORE, + .clearValue = {.color = {{0.0f, 0.0f, 0.0f, 0.0f}}}, + }; + VkRenderingAttachmentInfo cam_depth = { + .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, + .imageView = cam->texture.depth_image_view[frame], + .imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, + .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, + .storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE, + .clearValue = {.depthStencil = {1.0f, 0}}, + }; + VkRenderingInfo cam_rendering = { + .sType = VK_STRUCTURE_TYPE_RENDERING_INFO, + .renderArea = {{0, 0}, extent}, + .layerCount = 1, + .colorAttachmentCount = 1, + .pColorAttachments = &cam_color, + .pDepthAttachment = &cam_depth, + }; + vkCmdBeginRendering(cmd, &cam_rendering); + record_hex_draw(cmd, hex, cam->gpu_address[frame], time, frame); + vkCmdEndRendering(cmd); +} + +static void record_container_draw_wrapper( + VkCommandBuffer cmd, + void* userdata, + RenderContext* gpu, + UIContext* ui, + HexContext* hex, + uint32_t image_index, + uint32_t frame, + double time) { + (void)userdata; (void)hex; + + VkViewport viewport = { + .width = (float)gpu->swapchain_extent.width, + .height = (float)gpu->swapchain_extent.height, + .maxDepth = 1.0f, + .minDepth = 0.0f, + }; + vkCmdSetViewport(cmd, 0, 1, &viewport); + VkRect2D scissor = {.extent = gpu->swapchain_extent}; + vkCmdSetScissor(cmd, 0, 1, &scissor); + + VkRenderingAttachmentInfo color_attachment = { + .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, + .imageView = gpu->swapchain_image_views[image_index], + .imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, + .storeOp = VK_ATTACHMENT_STORE_OP_STORE, + .clearValue = {.color = {{0.0f, 0.0f, 0.0f, 0.0f}}}, + }; + VkRenderingAttachmentInfo depth_attachment = { + .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, + .imageView = gpu->depth_image_view, + .imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, + .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, + .storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE, + .clearValue = {.depthStencil = {1.0f, 0}}, + }; + VkRenderingInfo scene_rendering = { + .sType = VK_STRUCTURE_TYPE_RENDERING_INFO, + .renderArea = {{0, 0}, gpu->swapchain_extent}, + .layerCount = 1, + .colorAttachmentCount = 1, + .pColorAttachments = &color_attachment, + .pDepthAttachment = &depth_attachment, + }; + vkCmdBeginRendering(cmd, &scene_rendering); + record_container_draw(cmd, gpu, ui, time, frame); + vkCmdEndRendering(cmd); +} + +static void record_ui_compute_wrapper( + VkCommandBuffer cmd, + void* userdata, + RenderContext* gpu, + UIContext* ui, + HexContext* hex, + uint32_t image_index, + uint32_t frame, + double time) { + (void)userdata; (void)gpu; (void)hex; (void)image_index; (void)time; + + VkMemoryBarrier compute_barrier = { + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_SHADER_READ_BIT + | VK_ACCESS_TRANSFER_READ_BIT + | VK_ACCESS_TRANSFER_WRITE_BIT, + }; + vkCmdPipelineBarrier(cmd, + VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + 0, 1, &compute_barrier, 0, NULL, 0, NULL); + + record_ui_compute(cmd, ui, frame); +} + +// --------------------------------------------------------------------------- +// API +// --------------------------------------------------------------------------- + +uint32_t render_graph_add_node( + RenderGraph* graph, + RGNodeType type, + RGResourceUsage* reads, uint32_t read_count, + RGResourceUsage* writes, uint32_t write_count, + RGRecordFn record, void* userdata) { + assert(read_count <= RG_MAX_RESOURCES_PER_NODE); + assert(write_count <= RG_MAX_RESOURCES_PER_NODE); + + for(uint32_t i = 0; i < RG_MAX_NODES; i++) { + if(!graph->nodes[i].active) { + RGNode* node = &graph->nodes[i]; + memset(node, 0, sizeof(RGNode)); + node->type = type; + if(reads && read_count > 0) { + memcpy(node->reads, reads, read_count * sizeof(RGResourceUsage)); + node->read_count = read_count; + } + if(writes && write_count > 0) { + memcpy(node->writes, writes, write_count * sizeof(RGResourceUsage)); + node->write_count = write_count; + } + node->record = record; + node->userdata = userdata; + node->active = true; + if(i + 1 > graph->node_count) graph->node_count = i + 1; + graph->compiled = false; + return i; + } + } + assert(false && "RG_MAX_NODES exceeded"); + return UINT32_MAX; +} + +uint32_t render_graph_add_ui_compute_node(RenderGraph* graph) { + return render_graph_add_node(graph, RG_NODE_COMPUTE, + NULL, 0, NULL, 0, + record_ui_compute_wrapper, NULL); +} + +uint32_t render_graph_add_ui_composite_node(RenderGraph* graph) { + RGResourceUsage ui_writes[] = {{ + .resource = {RG_RESOURCE_SWAPCHAIN, RG_RESOURCE_SWAPCHAIN}, + .stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + .access = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + .layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + }}; + // reads are rebuilt at compile time from camera RENDER nodes' writes + return render_graph_add_node(graph, RG_NODE_RENDER, + NULL, 0, + ui_writes, 1, + record_container_draw_wrapper, NULL); +} + +uint32_t render_graph_add_display_node(RenderGraph* graph) { + // Reading PRESENT_SRC_KHR creates a dependency edge from the UI composite + // (which writes the swapchain) → this node, ensuring correct topo order + // and auto-deriving the present barrier. + RGResourceUsage display_reads[] = {{ + .resource = {RG_RESOURCE_SWAPCHAIN, RG_RESOURCE_SWAPCHAIN}, + .stage = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, + .access = 0, + .layout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + }}; + return render_graph_add_node(graph, RG_NODE_DISPLAY, + display_reads, 1, + NULL, 0, + NULL, NULL); +} + +uint32_t render_graph_add_camera_node(RenderGraph* graph, struct CameraStruct* camera) { + RGResourceUsage cam_writes[] = {{ + .resource = {camera->texture.texture_slot[0], camera->texture.texture_slot[1]}, + .stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + .access = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + .layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + }}; + return render_graph_add_node(graph, RG_NODE_RENDER, + NULL, 0, + cam_writes, 1, + record_hex_draw_for_camera, camera); +} + +void render_graph_remove_node(RenderGraph* graph, uint32_t handle) { + if(handle >= RG_MAX_NODES) return; + graph->nodes[handle].active = false; + graph->compiled = false; +} + +void render_graph_compile(RenderGraph* graph, UIContext* ui) { + (void)ui; + + // Step 1: Rebuild the UI composite node's reads from all other RENDER nodes' + // writes, so that topo sort derives the camera → UI composite dependency. + // The UI composite is identified as the RENDER node that writes RG_RESOURCE_SWAPCHAIN. + uint32_t ui_composite = UINT32_MAX; + for(uint32_t i = 0; i < graph->node_count; i++) { + if(!graph->nodes[i].active || graph->nodes[i].type != RG_NODE_RENDER) continue; + for(uint32_t j = 0; j < graph->nodes[i].write_count; j++) { + if(graph->nodes[i].writes[j].resource[0] == RG_RESOURCE_SWAPCHAIN) { + ui_composite = i; + break; + } + } + if(ui_composite != UINT32_MAX) break; + } + + if(ui_composite != UINT32_MAX) { + RGNode* ui_node = &graph->nodes[ui_composite]; + ui_node->read_count = 0; + for(uint32_t i = 0; i < graph->node_count; i++) { + if(!graph->nodes[i].active || i == ui_composite) continue; + if(graph->nodes[i].type != RG_NODE_RENDER) continue; + for(uint32_t j = 0; j < graph->nodes[i].write_count; j++) { + if(ui_node->read_count >= RG_MAX_RESOURCES_PER_NODE) break; + ui_node->reads[ui_node->read_count++] = (RGResourceUsage){ + .resource = { + graph->nodes[i].writes[j].resource[0], + graph->nodes[i].writes[j].resource[1], + }, + .stage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + .access = VK_ACCESS_SHADER_READ_BIT, + .layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + }; + } + } + } + + // Step 2: Topological sort (Kahn's algorithm). + graph->topo_count = 0; + + uint32_t active[RG_MAX_NODES]; + uint32_t active_count = 0; + for(uint32_t i = 0; i < graph->node_count; i++) { + if(graph->nodes[i].active) active[active_count++] = i; + } + + // Edge matrix: edges[w][r] = true means node w must execute before node r. + bool edges[RG_MAX_NODES][RG_MAX_NODES]; + uint32_t in_degree[RG_MAX_NODES]; + memset(edges, 0, sizeof(edges)); + memset(in_degree, 0, sizeof(in_degree)); + + for(uint32_t ai = 0; ai < active_count; ai++) { + uint32_t r = active[ai]; + for(uint32_t ri = 0; ri < graph->nodes[r].read_count; ri++) { + RGResource res0 = graph->nodes[r].reads[ri].resource[0]; + for(uint32_t bi = 0; bi < active_count; bi++) { + uint32_t w = active[bi]; + if(w == r) continue; + for(uint32_t wi = 0; wi < graph->nodes[w].write_count; wi++) { + if(graph->nodes[w].writes[wi].resource[0] == res0) { + if(!edges[w][r]) { + edges[w][r] = true; + in_degree[r]++; + } + } + } + } + } + } + + uint32_t queue[RG_MAX_NODES]; + uint32_t qhead = 0, qtail = 0; + for(uint32_t ai = 0; ai < active_count; ai++) { + if(in_degree[active[ai]] == 0) queue[qtail++] = active[ai]; + } + while(qhead < qtail) { + uint32_t n = queue[qhead++]; + graph->topo_order[graph->topo_count++] = n; + for(uint32_t ai = 0; ai < active_count; ai++) { + uint32_t m = active[ai]; + if(edges[n][m] && --in_degree[m] == 0) queue[qtail++] = m; + } + } + assert(graph->topo_count == active_count && "render graph has a cycle"); + + // Step 3: Derive image barriers. + // For each resource a node WRITES: find the prior writer's layout, or + // UNDEFINED if none → emit a pre-write (acquire) barrier. + // For each resource a node READS: find the most recent writer's layout + // → emit a pre-read (transition) barrier. + for(uint32_t ti = 0; ti < graph->topo_count; ti++) { + uint32_t n = graph->topo_order[ti]; + RGNode* node = &graph->nodes[n]; + node->pre_barrier_count = 0; + + VkImageSubresourceRange color_range = { + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .levelCount = 1, + .layerCount = 1, + }; + + // Pre-write barriers + for(uint32_t wi = 0; wi < node->write_count; wi++) { + RGResource res0 = node->writes[wi].resource[0]; + VkPipelineStageFlags src_stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkAccessFlags src_access = 0; + VkImageLayout old_layout = VK_IMAGE_LAYOUT_UNDEFINED; + + for(int32_t tj = (int32_t)ti - 1; tj >= 0; tj--) { + uint32_t m = graph->topo_order[tj]; + bool found = false; + for(uint32_t pwi = 0; pwi < graph->nodes[m].write_count; pwi++) { + if(graph->nodes[m].writes[pwi].resource[0] == res0) { + src_stage = graph->nodes[m].writes[pwi].stage; + src_access = graph->nodes[m].writes[pwi].access; + old_layout = graph->nodes[m].writes[pwi].layout; + found = true; + break; + } + } + if(found) break; + } + + if(node->pre_barrier_count < RG_MAX_RESOURCES_PER_NODE) { + RGBarrier* bar = &node->pre_barriers[node->pre_barrier_count++]; + bar->resource[0] = node->writes[wi].resource[0]; + bar->resource[1] = node->writes[wi].resource[1]; + bar->src_stage = src_stage; + bar->dst_stage = node->writes[wi].stage; + bar->barrier = (VkImageMemoryBarrier){ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcAccessMask = src_access, + .dstAccessMask = node->writes[wi].access, + .oldLayout = old_layout, + .newLayout = node->writes[wi].layout, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = VK_NULL_HANDLE, + .subresourceRange = color_range, + }; + } + } + + // Pre-read barriers + for(uint32_t ri = 0; ri < node->read_count; ri++) { + RGResource res0 = node->reads[ri].resource[0]; + + for(int32_t tj = (int32_t)ti - 1; tj >= 0; tj--) { + uint32_t m = graph->topo_order[tj]; + bool found = false; + for(uint32_t pwi = 0; pwi < graph->nodes[m].write_count; pwi++) { + if(graph->nodes[m].writes[pwi].resource[0] == res0) { + if(node->pre_barrier_count < RG_MAX_RESOURCES_PER_NODE) { + RGBarrier* bar = &node->pre_barriers[node->pre_barrier_count++]; + bar->resource[0] = node->reads[ri].resource[0]; + bar->resource[1] = node->reads[ri].resource[1]; + bar->src_stage = graph->nodes[m].writes[pwi].stage; + bar->dst_stage = node->reads[ri].stage; + bar->barrier = (VkImageMemoryBarrier){ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcAccessMask = graph->nodes[m].writes[pwi].access, + .dstAccessMask = node->reads[ri].access, + .oldLayout = graph->nodes[m].writes[pwi].layout, + .newLayout = node->reads[ri].layout, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = VK_NULL_HANDLE, + .subresourceRange = color_range, + }; + } + found = true; + break; + } + } + if(found) break; + } + } + } + + graph->compiled = true; +} + +VkResult render_graph_execute( + RenderGraph* graph, + VkCommandBuffer graphics_cmd, + VkCommandBuffer compute_cmd, + RenderContext* gpu, + UIContext* ui, + HexContext* hex, + uint32_t image_index, + double time) { + assert(graph->compiled && "render_graph_compile must be called before execute"); + + uint32_t frame = gpu->current_frame; + + for(uint32_t ti = 0; ti < graph->topo_count; ti++) { + uint32_t n = graph->topo_order[ti]; + RGNode* node = &graph->nodes[n]; + + VkCommandBuffer cmd = (node->type == RG_NODE_COMPUTE) ? compute_cmd : graphics_cmd; + + // Emit pre-barriers, resolving the per-frame image for each one. + for(uint32_t bi = 0; bi < node->pre_barrier_count; bi++) { + RGBarrier* bar = &node->pre_barriers[bi]; + VkImageMemoryBarrier img_barrier = bar->barrier; + + uint32_t slot = bar->resource[frame]; + if(slot == RG_RESOURCE_SWAPCHAIN) { + img_barrier.image = gpu->swapchain_images[image_index]; + } else { + img_barrier.image = ui->texture_slots[slot].image; + } + + vkCmdPipelineBarrier(cmd, + bar->src_stage, bar->dst_stage, + 0, 0, NULL, 0, NULL, + 1, &img_barrier); + } + + if(node->record != NULL) { + node->record(cmd, node->userdata, gpu, ui, hex, image_index, frame, time); + } + } + + return VK_SUCCESS; +} + +void render_graph_invalidate(RenderGraph* graph) { + graph->compiled = false; +}