From 42332a7ffc413ff432469c025cca1433a8b93c84 Mon Sep 17 00:00:00 2001 From: Noah Metz Date: Tue, 28 Jul 2026 16:54:44 -0600 Subject: [PATCH] Added topdown view and teardown functionality --- client/include/camera.h | 7 + client/include/editor.h | 1 + client/include/events.h | 5 + client/include/gpu.h | 9 ++ client/include/hex.h | 7 + client/include/ui.h | 10 ++ client/include/ui_lua.h | 7 + client/script/editor_topdown.lua | 16 +++ client/src/camera.c | 9 ++ client/src/editor.c | 14 ++ client/src/editor_lua.c | 230 ++++++++++++++++++++++++++----- client/src/editor_main.c | 4 +- client/src/engine.c | 37 ++++- client/src/events.c | 27 ++++ client/src/gpu.c | 80 ++++++++++- client/src/hex.c | 36 +++++ client/src/ui.c | 58 +++++++- client/src/ui_lua.c | 5 + client/test/editor.c | 1 + 19 files changed, 522 insertions(+), 41 deletions(-) create mode 100644 client/script/editor_topdown.lua diff --git a/client/include/camera.h b/client/include/camera.h index c2f9ad0..30c3d7b 100644 --- a/client/include/camera.h +++ b/client/include/camera.h @@ -57,6 +57,13 @@ typedef struct CameraStruct { VkResult create_camera(RenderContext* gpu, Camera* camera); +// Frees the GPU buffers create_camera allocated, cascading to +// camera_destroy_texture_target first if a texture target was ever set up. +// Does not free the Camera struct itself (caller-owned - e.g. a stack/ +// struct member for the engine's own cameras, malloc'd for script-created +// ones) or touch any Container still pointing at it. +void destroy_camera(RenderContext* gpu, Camera* camera); + void camera_update_view(Camera* camera); void camera_update_proj(Camera* camera, float aspect); diff --git a/client/include/editor.h b/client/include/editor.h index 9ab2e83..f11800a 100644 --- a/client/include/editor.h +++ b/client/include/editor.h @@ -61,6 +61,7 @@ struct EditorDataStruct { }; EditorData* create_editor_data(void); +void destroy_editor_data(EditorData* data); void editor_startup(ClientContext* context); void editor_frame_callback(ClientContext* context, double delta_time); diff --git a/client/include/events.h b/client/include/events.h index 5fcd119..c0dded2 100644 --- a/client/include/events.h +++ b/client/include/events.h @@ -91,6 +91,11 @@ struct EventBusStruct { // Registers the `app` global (emit/subscribe/get/set) in the Lua state VkResult event_bus_init(EventBus* bus, lua_State* L); +// Frees subs/queue/properties. Must run while bus->L is still open - it +// luaL_unrefs each subscription's handler and each queued event's args +// table out of the registry. +void event_bus_destroy(EventBus* bus); + // Pops nargs values off the top of the Lua stack into the event's packed // args and appends to the queue. Never dispatches; the queue drains once // per frame from the top level. diff --git a/client/include/gpu.h b/client/include/gpu.h index 210e9ce..90274a9 100644 --- a/client/include/gpu.h +++ b/client/include/gpu.h @@ -198,6 +198,11 @@ void retire_buffer( void destroy_retired(RenderContext* gpu); +// Same as destroy_retired but ignores the MAX_FRAMES_IN_FLIGHT age check - +// only safe once the GPU is confirmed idle (vkDeviceWaitIdle), e.g. during +// shutdown when there's no "next frame" to age the queue out naturally. +void destroy_retired_all(RenderContext* gpu); + VkDeviceAddress buffer_address( VkDevice device, VkBuffer buffer); @@ -257,6 +262,10 @@ VkShaderModule load_shader_file( VkResult recreate_framebuffer( RenderContext* gpu); +// Reverse of init_vulkan. Caller must vkDeviceWaitIdle first - nothing here +// is safe while the GPU may still reference these objects. +void terminate_vulkan(RenderContext* gpu); + VkResult create_depth_image( VkDevice device, VkFormat depth_format, diff --git a/client/include/hex.h b/client/include/hex.h index 1788f29..5beeee1 100644 --- a/client/include/hex.h +++ b/client/include/hex.h @@ -123,6 +123,13 @@ VkResult create_hex_context( RenderContext* gpu, HexContext* context); +// Reverse of create_hex_context: destroys the pipelines, every allocated +// HexRegion (regardless of who allocated it), and the per-frame storage +// buffers. +void destroy_hex_context( + RenderContext* gpu, + HexContext* context); + VkResult set_hex_region( HexRegion* region, HexContext* hex, diff --git a/client/include/ui.h b/client/include/ui.h index fa72ba3..b6f4a0b 100644 --- a/client/include/ui.h +++ b/client/include/ui.h @@ -276,6 +276,16 @@ VkResult create_ui_context( RenderContext* gpu, UIContext* context); +// Reverse of create_ui_context. Precondition: every container has already +// been unloaded (unload_container, looped by the caller) - this only tears +// down what create_ui_context itself allocated (fonts, textures, the +// container/order arrays, the descriptor/pipeline objects, its own GPU +// buffers) plus closes the Lua state, which cascades into any script-owned +// resource with a __gc finalizer (e.g. editor_lua.c's camera.create()). +void destroy_ui_context( + RenderContext* gpu, + UIContext* context); + VkResult load_font( uint32_t index, const char* ttf_file, diff --git a/client/include/ui_lua.h b/client/include/ui_lua.h index 8bb4e7d..7ee77c4 100644 --- a/client/include/ui_lua.h +++ b/client/include/ui_lua.h @@ -23,6 +23,13 @@ VkResult ui_lua_run_script( // to have been created first. Container* ui_lua_current_container(lua_State* L); +// Resolves an overlay handle argument (as returned by ui.create_overlay) to +// its Container, raising a Lua error if the argument isn't an overlay +// handle or its container was already destroyed. For bindings outside +// ui_lua.c (e.g. editor_lua.c's camera:attach) that need to accept an +// overlay handle without reaching into its opaque internals. +Container* ui_lua_check_overlay(lua_State* L, int idx); + // Points the binding's registry context at a container (or NULL) before // calling into a script from outside the normal dispatch path void ui_lua_set_current( diff --git a/client/script/editor_topdown.lua b/client/script/editor_topdown.lua new file mode 100644 index 0000000..8d6dffd --- /dev/null +++ b/client/script/editor_topdown.lua @@ -0,0 +1,16 @@ +-- Top-down minimap: a fixed-size, top-right, non-interactive second camera +-- - proves screen-region targeting isn't limited to one camera. Not wired +-- into picking (see cursor_to_world_ray's full-window assumption in +-- hex.c), so it's display-only. + +local overlay = ui.create_overlay{anchor = ANCHOR_TOP_RIGHT, offset = {-10, 10}, size = {250, 250}} + +local top_down = camera.create() +top_down:attach(overlay) +top_down:set_position(0, 0, 0) +-- Just shy of straight down (pi/2) - exactly vertical is singular for +-- glm_lookat against the {0,1,0} up vector (same bound editor.c's own pitch +-- clamp uses for the main camera). +top_down:set_rotation(0, math.pi/2 - 0.1) +top_down:set_distance(30) +top_down:set_aspect(250, 250) diff --git a/client/src/camera.c b/client/src/camera.c index f7bc7fb..9e8fb94 100644 --- a/client/src/camera.c +++ b/client/src/camera.c @@ -20,6 +20,15 @@ VkResult create_camera(RenderContext* gpu, Camera* camera) { return VK_SUCCESS; } +void destroy_camera(RenderContext* gpu, Camera* camera) { + if(camera->has_texture_target) { + camera_destroy_texture_target(gpu, NULL, camera); + } + for(uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + vmaDestroyBuffer(gpu->allocator, camera->gpu_buffer[i], camera->gpu_buffer_memory[i]); + } +} + void camera_update_view(Camera* camera) { vec3 eye = {}; eye[0] = camera->position[0] + camera->distance*cos(camera->rotation[1])*cos(camera->rotation[0]); diff --git a/client/src/editor.c b/client/src/editor.c index 06558d8..4184963 100644 --- a/client/src/editor.c +++ b/client/src/editor.c @@ -555,6 +555,10 @@ void editor_startup(ClientContext* context) { camera_sync_gpu(&context->camera, &context->render); update_hex_picking_inverse(&context->camera, &context->hex); + // Top-down minimap: a second, script-owned camera - proves camera + // creation/attachment isn't C-only. See script/editor_topdown.lua. + ui_lua_run_script(context->ui.lua, &context->ui, &context->render, "script/editor_topdown.lua"); + // TODO: Remove when region mode is implemented add_hex_region(context); } @@ -591,3 +595,13 @@ EditorData* create_editor_data(void) { return data; } + +void destroy_editor_data(EditorData* data) { + for(int i = 0; i < MODE_MAX_ENUM; i++) { + free(data->mode_keys[i]); + } + free(data->selected_regions); + free(data->selected_hexes); + free(data->selected_vertices); + free(data); +} diff --git a/client/src/editor_lua.c b/client/src/editor_lua.c index 9f79535..231cca2 100644 --- a/client/src/editor_lua.c +++ b/client/src/editor_lua.c @@ -1,6 +1,11 @@ #include "editor_lua.h" +#include "ui_lua.h" + +#include +#include #define EDITOR_LUA_CONTEXT "editor_context" +#define CAMERA_LUA_META "camera_handle" // ClientContext lives once, heap-allocated for the process, never moves — // unlike ui_lua.c's per-dispatch container/gpu pointers, one registry slot @@ -12,78 +17,233 @@ static ClientContext* current_context(lua_State* L) { return context; } -// Recomputes the camera's view and pushes it to the currently-rendered hex -// scene. Not gated behind any per-frame check - a script-driven camera move -// with no held input must be visible immediately. -static void refresh_camera(ClientContext* context) { - camera_update_view(&context->camera); - camera_sync_gpu(&context->camera, &context->render); - update_hex_picking_inverse(&context->camera, &context->hex); +// camera.main() wraps &context->camera (engine-owned, script can't destroy +// it); camera.create() wraps a script-owned Camera* malloc'd on create and +// freed on :destroy(). `camera == NULL` marks an already-destroyed handle, +// so later method calls raise a clear error instead of touching freed +// memory - same stale-handle shape as ui_lua.c's element/overlay handles. +typedef struct CameraLuaHandleStruct { + Camera* camera; + bool owned; + // Registry ref this handle holds on itself, LUA_NOREF for camera.main()'s + // handle. container_set_camera (ui.h) stores the raw Camera* with no + // Lua-visible reference back to this userdata ("caller keeps camera alive + // as long as it's attached" - the caller here is the script). Without + // this self-pin, a script that does `local c = camera.create(); c:attach(o)` + // and lets `c` fall out of scope hands the handle to Lua's *incremental* + // GC - which runs mid-frame, not just at shutdown - while the container is + // still actively rendering it every frame. Set on create, released on + // :destroy(); lua_close's shutdown sweep collects it (self-pin or not) + // regardless, which is what lets __gc below double as the cascade for a + // script that never explicitly destroys it. + int self_ref; +} CameraLuaHandle; + +static CameraLuaHandle* check_camera(lua_State* L, int idx) { + CameraLuaHandle* h = luaL_checkudata(L, idx, CAMERA_LUA_META); + if(h->camera == NULL) { + luaL_error(L, "camera handle was destroyed"); + } + return h; } -// camera.set_position(x, y, z) -static int lua_camera_set_position(lua_State* L) { +static CameraLuaHandle* push_camera_handle(lua_State* L, Camera* camera, bool owned) { + CameraLuaHandle* h = lua_newuserdata(L, sizeof(CameraLuaHandle)); + h->camera = camera; + h->owned = owned; + h->self_ref = LUA_NOREF; + luaL_setmetatable(L, CAMERA_LUA_META); + return h; +} + +// Recomputes the camera's view and pushes it to the GPU. Not gated behind +// any per-frame check - a script-driven camera move with no held input must +// be visible immediately. The picking-ray inverse only tracks the one +// interactive camera (see cursor_to_world_ray's full-window assumption in +// hex.c), so it's only refreshed when this is that specific camera. +static void refresh_camera(ClientContext* context, Camera* camera) { + camera_update_view(camera); + camera_sync_gpu(camera, &context->render); + if(camera == &context->camera) { + update_hex_picking_inverse(camera, &context->hex); + } +} + +// camera.main() -> handle for the engine's one interactive camera +static int lua_camera_main(lua_State* L) { + push_camera_handle(L, ¤t_context(L)->camera, false); + return 1; +} + +// camera.create() -> handle for a new, script-owned camera (no render +// target yet - attach it to an overlay with h:attach, or give it one with +// nothing at all to leave it inert) +static int lua_camera_create(lua_State* L) { ClientContext* context = current_context(L); - context->camera.position[0] = luaL_checknumber(L, 1); - context->camera.position[1] = luaL_checknumber(L, 2); - context->camera.position[2] = luaL_checknumber(L, 3); - refresh_camera(context); + + Camera* camera = malloc(sizeof(Camera)); + memset(camera, 0, sizeof(Camera)); + if(create_camera(&context->render, camera) != VK_SUCCESS) { + free(camera); + return luaL_error(L, "camera.create: failed to allocate camera buffers"); + } + + CameraLuaHandle* h = push_camera_handle(L, camera, true); + lua_pushvalue(L, -1); + h->self_ref = luaL_ref(L, LUA_REGISTRYINDEX); + return 1; +} + +// h:destroy() - only valid for camera.create()'d handles +static int lua_camera_destroy(lua_State* L) { + CameraLuaHandle* h = check_camera(L, 1); + if(!h->owned) { + return luaL_error(L, "camera:destroy: the main camera is engine-owned, not destroyable"); + } + ClientContext* context = current_context(L); + + // Don't leave any container pointing at memory this is about to free. + for(uint32_t i = 0; i < context->ui.max_containers; i++) { + if(context->ui.containers[i].camera == h->camera) { + context->ui.containers[i].camera = NULL; + } + } + + luaL_unref(L, LUA_REGISTRYINDEX, h->self_ref); + h->self_ref = LUA_NOREF; + + destroy_camera(&context->render, h->camera); + free(h->camera); + h->camera = NULL; return 0; } -// camera.get_position() -> x, y, z +// __gc - catches camera.create()'d handles a script never explicitly +// :destroy()'d. Runs for every camera userdata when lua_close collects the +// state (including camera.main()'s, hence the owned check), by which point +// destroy_ui_context has already unloaded every container, so there's no +// container->camera pointer left to scrub the way lua_camera_destroy does. +static int lua_camera_gc(lua_State* L) { + CameraLuaHandle* h = luaL_checkudata(L, 1, CAMERA_LUA_META); + if(!h->owned || h->camera == NULL) { + return 0; + } + destroy_camera(¤t_context(L)->render, h->camera); + free(h->camera); + h->camera = NULL; + return 0; +} + +// h:attach(overlay) - the overlay's region/z-order becomes this camera's +// viewport and draw order (see container_set_camera, ui.h) +static int lua_camera_attach(lua_State* L) { + CameraLuaHandle* h = check_camera(L, 1); + Container* c = ui_lua_check_overlay(L, 2); + container_set_camera(c, h->camera); + return 0; +} + +// h:detach(overlay) +static int lua_camera_detach(lua_State* L) { + check_camera(L, 1); + Container* c = ui_lua_check_overlay(L, 2); + container_set_camera(c, NULL); + return 0; +} + +// h:set_position(x, y, z) +static int lua_camera_set_position(lua_State* L) { + CameraLuaHandle* h = check_camera(L, 1); + h->camera->position[0] = luaL_checknumber(L, 2); + h->camera->position[1] = luaL_checknumber(L, 3); + h->camera->position[2] = luaL_checknumber(L, 4); + refresh_camera(current_context(L), h->camera); + return 0; +} + +// h:get_position() -> x, y, z static int lua_camera_get_position(lua_State* L) { - Camera* camera = ¤t_context(L)->camera; - lua_pushnumber(L, camera->position[0]); - lua_pushnumber(L, camera->position[1]); - lua_pushnumber(L, camera->position[2]); + CameraLuaHandle* h = check_camera(L, 1); + lua_pushnumber(L, h->camera->position[0]); + lua_pushnumber(L, h->camera->position[1]); + lua_pushnumber(L, h->camera->position[2]); return 3; } -// camera.set_rotation(yaw, pitch) +// h:set_rotation(yaw, pitch) static int lua_camera_set_rotation(lua_State* L) { - ClientContext* context = current_context(L); - context->camera.rotation[0] = luaL_checknumber(L, 1); - context->camera.rotation[1] = luaL_checknumber(L, 2); - refresh_camera(context); + CameraLuaHandle* h = check_camera(L, 1); + h->camera->rotation[0] = luaL_checknumber(L, 2); + h->camera->rotation[1] = luaL_checknumber(L, 3); + refresh_camera(current_context(L), h->camera); return 0; } -// camera.get_rotation() -> yaw, pitch +// h:get_rotation() -> yaw, pitch static int lua_camera_get_rotation(lua_State* L) { - Camera* camera = ¤t_context(L)->camera; - lua_pushnumber(L, camera->rotation[0]); - lua_pushnumber(L, camera->rotation[1]); + CameraLuaHandle* h = check_camera(L, 1); + lua_pushnumber(L, h->camera->rotation[0]); + lua_pushnumber(L, h->camera->rotation[1]); return 2; } -// camera.set_distance(d) +// h:set_distance(d) static int lua_camera_set_distance(lua_State* L) { - ClientContext* context = current_context(L); - context->camera.distance = luaL_checknumber(L, 1); - refresh_camera(context); + CameraLuaHandle* h = check_camera(L, 1); + h->camera->distance = luaL_checknumber(L, 2); + refresh_camera(current_context(L), h->camera); return 0; } -// camera.get_distance() -> d +// h:get_distance() -> d static int lua_camera_get_distance(lua_State* L) { - lua_pushnumber(L, current_context(L)->camera.distance); + CameraLuaHandle* h = check_camera(L, 1); + lua_pushnumber(L, h->camera->distance); return 1; } +// h:set_aspect(width, height) - projection is derived from an explicit +// aspect rather than read off whatever the camera is attached to (camera.c +// stays decoupled from ui.h), so scripts set it once after attaching (and +// again on resize, for anything that tracks the window) +static int lua_camera_set_aspect(lua_State* L) { + CameraLuaHandle* h = check_camera(L, 1); + float width = luaL_checknumber(L, 2); + float height = luaL_checknumber(L, 3); + camera_update_proj(h->camera, width/height); + camera_sync_gpu(h->camera, ¤t_context(L)->render); + return 0; +} + void editor_lua_register(lua_State* L, ClientContext* context) { lua_pushlightuserdata(L, context); lua_setfield(L, LUA_REGISTRYINDEX, EDITOR_LUA_CONTEXT); static const luaL_Reg camera_funcs[] = { + {"main", lua_camera_main}, + {"create", lua_camera_create}, + {NULL, NULL}, + }; + luaL_newlib(L, camera_funcs); + lua_setglobal(L, "camera"); + + static const luaL_Reg camera_methods[] = { + {"destroy", lua_camera_destroy}, + {"attach", lua_camera_attach}, + {"detach", lua_camera_detach}, {"set_position", lua_camera_set_position}, {"get_position", lua_camera_get_position}, {"set_rotation", lua_camera_set_rotation}, {"get_rotation", lua_camera_get_rotation}, {"set_distance", lua_camera_set_distance}, {"get_distance", lua_camera_get_distance}, + {"set_aspect", lua_camera_set_aspect}, {NULL, NULL}, }; - luaL_newlib(L, camera_funcs); - lua_setglobal(L, "camera"); + luaL_newmetatable(L, CAMERA_LUA_META); + luaL_newlib(L, camera_methods); + lua_setfield(L, -2, "__index"); + lua_pushcfunction(L, lua_camera_gc); + lua_setfield(L, -2, "__gc"); + lua_pop(L, 1); } diff --git a/client/src/editor_main.c b/client/src/editor_main.c index 15d4e81..2b07768 100644 --- a/client/src/editor_main.c +++ b/client/src/editor_main.c @@ -2,5 +2,7 @@ int main() { EditorData* data = create_editor_data(); - return run_app(data, editor_startup, editor_frame_callback, NULL, editor_key_callback, editor_button_callback, editor_scroll_callback, editor_cursor_callback); + int result = run_app(data, editor_startup, editor_frame_callback, NULL, editor_key_callback, editor_button_callback, editor_scroll_callback, editor_cursor_callback); + destroy_editor_data(data); + return result; } diff --git a/client/src/engine.c b/client/src/engine.c index 7dd927e..5098218 100644 --- a/client/src/engine.c +++ b/client/src/engine.c @@ -219,6 +219,38 @@ int app_main(ClientContext* context) { return 0; } +// Reverse of run_app's own setup sequence below - see shutdown_app for the +// full ordering rationale. Caller (app_main) has already exited the frame +// loop, so nothing here races in-flight GPU work once vkDeviceWaitIdle +// returns. +void shutdown_app(ClientContext* context) { + vkDeviceWaitIdle(context->render.device); + + // app_startup ran last during setup (after ui/hex/camera/events were all + // up), and its scripts are what created every container - unwind those + // first. unload_container needs context->ui.events (== &context->events) + // still alive to drop the container's subscriptions, so this must come + // before event_bus_destroy. + for(uint32_t i = 0; i < context->ui.max_containers; i++) { + if(context->ui.containers[i].id != 0) { + unload_container(context->ui.containers[i].id, &context->render, &context->ui); + } + } + destroy_retired_all(&context->render); + + event_bus_destroy(&context->events); + destroy_camera(&context->render, &context->camera); + destroy_hex_context(&context->render, &context->hex); + // Closes context->ui.lua last among these - after event_bus_destroy is + // done unref'ing into its registry, and after every container is gone. + destroy_ui_context(&context->render, &context->ui); + + terminate_vulkan(&context->render); + + glfwDestroyWindow(context->window); + glfwTerminate(); +} + int run_app( void* app_data, app_startup_function app_startup, @@ -268,5 +300,8 @@ int run_app( if(app_startup != NULL) app_startup(context); - return app_main(context); + int result = app_main(context); + shutdown_app(context); + free(context); + return result; } diff --git a/client/src/events.c b/client/src/events.c index 1d5f98b..d5b033e 100644 --- a/client/src/events.c +++ b/client/src/events.c @@ -403,6 +403,33 @@ static int lua_app_set(lua_State* L) { return 0; } +void event_bus_destroy(EventBus* bus) { + for(uint32_t i = 0; i < bus->sub_count; i++) { + if(bus->subs[i].lua_ref != LUA_NOREF) { + luaL_unref(bus->L, LUA_REGISTRYINDEX, bus->subs[i].lua_ref); + } + free(bus->subs[i].event); + } + free(bus->subs); + + // Normally empty (the queue drains every frame), but nothing guarantees + // that at shutdown - e.g. a script could emit during app_startup before + // the first drain. + for(uint32_t i = 0; i < bus->queue_count; i++) { + luaL_unref(bus->L, LUA_REGISTRYINDEX, bus->queue[i].args_ref); + free(bus->queue[i].name); + } + free(bus->queue); + + for(uint32_t i = 0; i < bus->property_count; i++) { + if(bus->properties[i].type == PROPERTY_STRING) { + free(bus->properties[i].value.string); + } + free(bus->properties[i].name); + } + free(bus->properties); +} + VkResult event_bus_init(EventBus* bus, lua_State* L) { memset(bus, 0, sizeof(EventBus)); bus->L = L; diff --git a/client/src/gpu.c b/client/src/gpu.c index ccdbcc3..8ebae6d 100644 --- a/client/src/gpu.c +++ b/client/src/gpu.c @@ -74,6 +74,7 @@ VkShaderModule load_shader_file( int result = fseek(file, 0, SEEK_END); if(result != 0) { + fclose(file); return VK_NULL_HANDLE; } @@ -81,15 +82,18 @@ VkShaderModule load_shader_file( result = fseek(file, 0, SEEK_SET); if(result != 0) { + fclose(file); return VK_NULL_HANDLE; } char * buffer = malloc(buffer_size); if(buffer == 0) { + fclose(file); return VK_NULL_HANDLE; } size_t read = fread(buffer, 1, buffer_size, file); + fclose(file); VkShaderModuleCreateInfo shader_info = { .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, @@ -1031,11 +1035,11 @@ void retire_buffer( gpu->retired_count += 1; } -void destroy_retired(RenderContext* gpu) { +static void destroy_retired_impl(RenderContext* gpu, bool force) { // Entries are appended in frame order, so eligible entries are a prefix uint32_t i = 0; while(i < gpu->retired_count - && gpu->frame_number - gpu->retired[i].frame >= MAX_FRAMES_IN_FLIGHT) { + && (force || gpu->frame_number - gpu->retired[i].frame >= MAX_FRAMES_IN_FLIGHT)) { vmaDestroyBuffer(gpu->allocator, gpu->retired[i].buffer, gpu->retired[i].memory); i += 1; } @@ -1046,6 +1050,14 @@ void destroy_retired(RenderContext* gpu) { } } +void destroy_retired(RenderContext* gpu) { + destroy_retired_impl(gpu, false); +} + +void destroy_retired_all(RenderContext* gpu) { + destroy_retired_impl(gpu, true); +} + void destroy_transfer_buffer( VmaAllocator allocator, VkBuffer buffer, @@ -1115,11 +1127,13 @@ VkResult command_end_single(VkDevice device, VkCommandBuffer command_buffer, VkC result = vkQueueSubmit(transfer_queue.handle, 1, &submit_info, submit_fence); if(result != VK_SUCCESS) { + vkDestroyFence(device, submit_fence, NULL); vkFreeCommandBuffers(device, transfer_pool, 1, &command_buffer); return result; } result = vkWaitForFences(device, 1, &submit_fence, VK_TRUE, UINT64_MAX); + vkDestroyFence(device, submit_fence, NULL); vkFreeCommandBuffers(device, transfer_pool, 1, &command_buffer); return result; } @@ -1303,3 +1317,65 @@ VkResult recreate_framebuffer(RenderContext* gpu) { return VK_SUCCESS; } + +// Reverse of create_frame_context. +static void destroy_frame_context(VkDevice device, VmaAllocator allocator, VkCommandPool transfer_pool, FrameContext* frame) { + destroy_transfer_buffer(allocator, frame->transfers[1].buffer, frame->transfers[1].memory); + free(frame->transfers[1].infos); + destroy_transfer_buffer(allocator, frame->transfers[0].buffer, frame->transfers[0].memory); + free(frame->transfers[0].infos); + + vkFreeCommandBuffers(device, transfer_pool, 1, &frame->compute_commands); + vkFreeCommandBuffers(device, transfer_pool, 1, &frame->transfer_commands); + + vkDestroySemaphore(device, frame->compute, NULL); + vkDestroySemaphore(device, frame->transfer, NULL); + vkDestroySemaphore(device, frame->render, NULL); + vkDestroySemaphore(device, frame->image, NULL); + vkDestroyFence(device, frame->ready, NULL); +} + +void terminate_vulkan(RenderContext* gpu) { + for(uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + destroy_frame_context(gpu->device, gpu->allocator, gpu->transfer_pool, &gpu->frame[i]); + } + + vkDestroyImageView(gpu->device, gpu->depth_image_view, NULL); + vmaDestroyImage(gpu->allocator, gpu->depth_image, gpu->depth_image_memory); + + for(uint32_t i = 0; i < gpu->swapchain_image_count; i++) { + vkDestroyImageView(gpu->device, gpu->swapchain_image_views[i], NULL); + } + free(gpu->swapchain_image_views); + free(gpu->swapchain_images); + + vkDestroySwapchainKHR(gpu->device, gpu->swapchain, NULL); + + free(gpu->swapchain_details.formats); + free(gpu->swapchain_details.present_modes); + + free(gpu->retired); + + vkFreeCommandBuffers(gpu->device, gpu->graphics_pool, MAX_FRAMES_IN_FLIGHT, gpu->frame_command_buffers); + free(gpu->frame_command_buffers); + + vkDestroyCommandPool(gpu->device, gpu->extra_graphics_pool, NULL); + vkDestroyCommandPool(gpu->device, gpu->graphics_pool, NULL); + vkDestroyCommandPool(gpu->device, gpu->transfer_pool, NULL); + + vmaDestroyAllocator(gpu->allocator); + + vkDestroyDevice(gpu->device, NULL); + + vkDestroySurfaceKHR(gpu->instance, gpu->surface, NULL); + + // Mirrors create_debug_messenger's own vkGetInstanceProcAddr lookup - this + // entry point isn't part of the core loader dispatch table. + PFN_vkDestroyDebugUtilsMessengerEXT destroy_messenger = + (PFN_vkDestroyDebugUtilsMessengerEXT)vkGetInstanceProcAddr(gpu->instance, "vkDestroyDebugUtilsMessengerEXT"); + if(destroy_messenger != NULL) { + destroy_messenger(gpu->instance, gpu->debug_messenger, NULL); + } + + vkDestroyInstance(gpu->instance, NULL); +} diff --git a/client/src/hex.c b/client/src/hex.c index 34fd52c..47b15b1 100644 --- a/client/src/hex.c +++ b/client/src/hex.c @@ -223,6 +223,8 @@ VkResult create_point_pipeline( 1, &graphics_pipeline_info, NULL, &pipeline->pipeline)); + vkDestroyShaderModule(gpu->device, vert_shader, NULL); + vkDestroyShaderModule(gpu->device, frag_shader, NULL); return VK_SUCCESS; } @@ -390,6 +392,8 @@ VkResult create_ray_pipeline( 1, &graphics_pipeline_info, NULL, &pipeline->pipeline)); + vkDestroyShaderModule(gpu->device, vert_shader, NULL); + vkDestroyShaderModule(gpu->device, frag_shader, NULL); return VK_SUCCESS; } @@ -557,6 +561,8 @@ VkResult create_hex_highlight_pipeline( 1, &graphics_pipeline_info, NULL, &pipeline->pipeline)); + vkDestroyShaderModule(gpu->device, vert_shader, NULL); + vkDestroyShaderModule(gpu->device, frag_shader, NULL); return VK_SUCCESS; } @@ -724,6 +730,8 @@ VkResult create_hex_pipeline( 1, &graphics_pipeline_info, NULL, &pipeline->pipeline)); + vkDestroyShaderModule(gpu->device, vert_shader, NULL); + vkDestroyShaderModule(gpu->device, frag_shader, NULL); return VK_SUCCESS; } @@ -829,6 +837,34 @@ VkResult create_hex_context( 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); + vkDestroyPipeline(gpu->device, context->highlight_pipeline.pipeline, NULL); + vkDestroyPipelineLayout(gpu->device, context->highlight_pipeline.layout, NULL); + vkDestroyPipeline(gpu->device, context->point_pipeline.pipeline, NULL); + vkDestroyPipelineLayout(gpu->device, context->point_pipeline.layout, NULL); + vkDestroyPipeline(gpu->device, context->ray_pipeline.pipeline, NULL); + vkDestroyPipelineLayout(gpu->device, context->ray_pipeline.layout, NULL); + + // 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++) { + 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; + } + + 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]); + } +} + 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++) { diff --git a/client/src/ui.c b/client/src/ui.c index 2edc45c..efb5e25 100644 --- a/client/src/ui.c +++ b/client/src/ui.c @@ -59,6 +59,8 @@ VkResult create_ui_pipeline( if(result != VK_SUCCESS) { return result; } + // Only needed to build the pipeline above, not to run it. + vkDestroyShaderModule(device, compute_shader, NULL); VkShaderModule vert_shader = load_shader_file("shader/ui.vert.spv", device); if(vert_shader == VK_NULL_HANDLE) { @@ -244,6 +246,8 @@ VkResult create_ui_pipeline( if(result != VK_SUCCESS) { return result; } + vkDestroyShaderModule(device, vert_shader, NULL); + vkDestroyShaderModule(device, frag_shader, NULL); return VK_SUCCESS; } @@ -1140,10 +1144,10 @@ VkResult load_font( } context->fonts[index].family = malloc(strlen(face->family_name)+1); - memcpy(&context->fonts[index].family, face->family_name, strlen(face->family_name)+1); + memcpy(context->fonts[index].family, face->family_name, strlen(face->family_name)+1); context->fonts[index].style = malloc(strlen(face->style_name)+1); - memcpy(&context->fonts[index].style, face->style_name, strlen(face->style_name)+1); + memcpy(context->fonts[index].style, face->style_name, strlen(face->style_name)+1); error = FT_Set_Pixel_Sizes(face, 0, size); @@ -1676,6 +1680,56 @@ VkResult create_ui_context( return VK_SUCCESS; } +void destroy_ui_context(RenderContext* gpu, UIContext* context) { + for(uint32_t i = 0; i < context->max_fonts; i++) { + Font* font = &context->fonts[i]; + if(font->symbols == VK_NULL_HANDLE) continue; + + vkDestroyImageView(gpu->device, font->view, NULL); + vmaDestroyImage(gpu->allocator, font->image, font->image_memory); + vkDestroySampler(gpu->device, font->sampler, NULL); + vmaDestroyBuffer(gpu->allocator, font->symbols, font->symbol_memory); + free(font->charmap); + free(font->family); + free(font->style); + } + + for(uint32_t i = 0; i < context->max_textures; i++) { + Texture* texture = &context->texture_slots[i]; + if(texture->path == NULL) continue; + + vkDestroyImageView(gpu->device, texture->view, NULL); + vmaDestroyImage(gpu->allocator, texture->image, texture->image_memory); + vkDestroySampler(gpu->device, texture->sampler, NULL); + free(texture->path); + } + + free(context->texture_slots); + free(context->fonts); + free(context->containers); + free(context->container_order); + + FT_Done_FreeType(context->freetype); + + // Last thing that touches Lua: this collects any userdata with a __gc + // finalizer still alive (e.g. a script-owned camera.create() camera that + // was never explicitly :destroy()'d - see editor_lua.c's CAMERA_LUA_META). + lua_close(context->lua); + + vkDestroyPipeline(gpu->device, context->pipeline.pipeline, NULL); + vkDestroyPipelineLayout(gpu->device, context->pipeline.layout, NULL); + vkDestroyPipeline(gpu->device, context->string_pipeline.pipeline, NULL); + vkDestroyPipelineLayout(gpu->device, context->string_pipeline.layout, NULL); + + vkDestroyDescriptorPool(gpu->device, context->fonts_pool, NULL); + vkDestroyDescriptorPool(gpu->device, context->textures_pool, NULL); + vkDestroyDescriptorSetLayout(gpu->device, context->samplers_layout, NULL); + vkDestroyDescriptorSetLayout(gpu->device, context->textures_layout, NULL); + + vmaDestroyBuffer(gpu->allocator, context->context, context->context_memory); + vmaDestroyBuffer(gpu->allocator, context->font_infos, context->font_infos_memory); +} + VkResult map_string( const char* text, uint32_t* buffer, diff --git a/client/src/ui_lua.c b/client/src/ui_lua.c index 241f12c..f77a6ac 100644 --- a/client/src/ui_lua.c +++ b/client/src/ui_lua.c @@ -448,6 +448,11 @@ static int lua_element_remove(lua_State* L) { return 0; } +Container* ui_lua_check_overlay(lua_State* L, int idx) { + UIOverlayHandle* h = check_overlay(L, idx); + return resolve_overlay(L, h); +} + // ---------- overlay handle methods ---------- // overlay:to_front() diff --git a/client/test/editor.c b/client/test/editor.c index 0815f73..bb98f13 100644 --- a/client/test/editor.c +++ b/client/test/editor.c @@ -201,6 +201,7 @@ int main() { EditorData* data = create_editor_data(); int result = run_app(data, editor_startup, test_frame_callback, NULL, editor_key_callback, editor_button_callback, editor_scroll_callback, NULL); + destroy_editor_data(data); if(failures > 0) { fprintf(stderr, "%d test(s) failed\n", failures);