Added topdown view and teardown functionality

main
noah metz 2026-07-28 16:54:44 -06:00
parent 1f2118f210
commit 42332a7ffc
19 changed files with 522 additions and 41 deletions

@ -57,6 +57,13 @@ typedef struct CameraStruct {
VkResult create_camera(RenderContext* gpu, Camera* camera); 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_view(Camera* camera);
void camera_update_proj(Camera* camera, float aspect); void camera_update_proj(Camera* camera, float aspect);

@ -61,6 +61,7 @@ struct EditorDataStruct {
}; };
EditorData* create_editor_data(void); EditorData* create_editor_data(void);
void destroy_editor_data(EditorData* data);
void editor_startup(ClientContext* context); void editor_startup(ClientContext* context);
void editor_frame_callback(ClientContext* context, double delta_time); void editor_frame_callback(ClientContext* context, double delta_time);

@ -91,6 +91,11 @@ struct EventBusStruct {
// Registers the `app` global (emit/subscribe/get/set) in the Lua state // Registers the `app` global (emit/subscribe/get/set) in the Lua state
VkResult event_bus_init(EventBus* bus, lua_State* L); 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 // 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 // args and appends to the queue. Never dispatches; the queue drains once
// per frame from the top level. // per frame from the top level.

@ -198,6 +198,11 @@ void retire_buffer(
void destroy_retired(RenderContext* gpu); 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( VkDeviceAddress buffer_address(
VkDevice device, VkDevice device,
VkBuffer buffer); VkBuffer buffer);
@ -257,6 +262,10 @@ VkShaderModule load_shader_file(
VkResult recreate_framebuffer( VkResult recreate_framebuffer(
RenderContext* gpu); 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( VkResult create_depth_image(
VkDevice device, VkDevice device,
VkFormat depth_format, VkFormat depth_format,

@ -123,6 +123,13 @@ VkResult create_hex_context(
RenderContext* gpu, RenderContext* gpu,
HexContext* context); 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( VkResult set_hex_region(
HexRegion* region, HexRegion* region,
HexContext* hex, HexContext* hex,

@ -276,6 +276,16 @@ VkResult create_ui_context(
RenderContext* gpu, RenderContext* gpu,
UIContext* context); 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( VkResult load_font(
uint32_t index, uint32_t index,
const char* ttf_file, const char* ttf_file,

@ -23,6 +23,13 @@ VkResult ui_lua_run_script(
// to have been created first. // to have been created first.
Container* ui_lua_current_container(lua_State* L); 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 // Points the binding's registry context at a container (or NULL) before
// calling into a script from outside the normal dispatch path // calling into a script from outside the normal dispatch path
void ui_lua_set_current( void ui_lua_set_current(

@ -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)

@ -20,6 +20,15 @@ VkResult create_camera(RenderContext* gpu, Camera* camera) {
return VK_SUCCESS; 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) { void camera_update_view(Camera* camera) {
vec3 eye = {}; vec3 eye = {};
eye[0] = camera->position[0] + camera->distance*cos(camera->rotation[1])*cos(camera->rotation[0]); eye[0] = camera->position[0] + camera->distance*cos(camera->rotation[1])*cos(camera->rotation[0]);

@ -555,6 +555,10 @@ void editor_startup(ClientContext* context) {
camera_sync_gpu(&context->camera, &context->render); camera_sync_gpu(&context->camera, &context->render);
update_hex_picking_inverse(&context->camera, &context->hex); 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 // TODO: Remove when region mode is implemented
add_hex_region(context); add_hex_region(context);
} }
@ -591,3 +595,13 @@ EditorData* create_editor_data(void) {
return data; 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);
}

@ -1,6 +1,11 @@
#include "editor_lua.h" #include "editor_lua.h"
#include "ui_lua.h"
#include <stdlib.h>
#include <string.h>
#define EDITOR_LUA_CONTEXT "editor_context" #define EDITOR_LUA_CONTEXT "editor_context"
#define CAMERA_LUA_META "camera_handle"
// ClientContext lives once, heap-allocated for the process, never moves — // ClientContext lives once, heap-allocated for the process, never moves —
// unlike ui_lua.c's per-dispatch container/gpu pointers, one registry slot // 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; return context;
} }
// Recomputes the camera's view and pushes it to the currently-rendered hex // camera.main() wraps &context->camera (engine-owned, script can't destroy
// scene. Not gated behind any per-frame check - a script-driven camera move // it); camera.create() wraps a script-owned Camera* malloc'd on create and
// with no held input must be visible immediately. // freed on :destroy(). `camera == NULL` marks an already-destroyed handle,
static void refresh_camera(ClientContext* context) { // so later method calls raise a clear error instead of touching freed
camera_update_view(&context->camera); // memory - same stale-handle shape as ui_lua.c's element/overlay handles.
camera_sync_gpu(&context->camera, &context->render); typedef struct CameraLuaHandleStruct {
update_hex_picking_inverse(&context->camera, &context->hex); 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 CameraLuaHandle* push_camera_handle(lua_State* L, Camera* camera, bool owned) {
static int lua_camera_set_position(lua_State* L) { 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, &current_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); ClientContext* context = current_context(L);
context->camera.position[0] = luaL_checknumber(L, 1);
context->camera.position[1] = luaL_checknumber(L, 2); Camera* camera = malloc(sizeof(Camera));
context->camera.position[2] = luaL_checknumber(L, 3); memset(camera, 0, sizeof(Camera));
refresh_camera(context); 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;
}
// __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(&current_context(L)->render, h->camera);
free(h->camera);
h->camera = NULL;
return 0; return 0;
} }
// camera.get_position() -> x, y, z // 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) { static int lua_camera_get_position(lua_State* L) {
Camera* camera = &current_context(L)->camera; CameraLuaHandle* h = check_camera(L, 1);
lua_pushnumber(L, camera->position[0]); lua_pushnumber(L, h->camera->position[0]);
lua_pushnumber(L, camera->position[1]); lua_pushnumber(L, h->camera->position[1]);
lua_pushnumber(L, camera->position[2]); lua_pushnumber(L, h->camera->position[2]);
return 3; return 3;
} }
// camera.set_rotation(yaw, pitch) // h:set_rotation(yaw, pitch)
static int lua_camera_set_rotation(lua_State* L) { static int lua_camera_set_rotation(lua_State* L) {
ClientContext* context = current_context(L); CameraLuaHandle* h = check_camera(L, 1);
context->camera.rotation[0] = luaL_checknumber(L, 1); h->camera->rotation[0] = luaL_checknumber(L, 2);
context->camera.rotation[1] = luaL_checknumber(L, 2); h->camera->rotation[1] = luaL_checknumber(L, 3);
refresh_camera(context); refresh_camera(current_context(L), h->camera);
return 0; return 0;
} }
// camera.get_rotation() -> yaw, pitch // h:get_rotation() -> yaw, pitch
static int lua_camera_get_rotation(lua_State* L) { static int lua_camera_get_rotation(lua_State* L) {
Camera* camera = &current_context(L)->camera; CameraLuaHandle* h = check_camera(L, 1);
lua_pushnumber(L, camera->rotation[0]); lua_pushnumber(L, h->camera->rotation[0]);
lua_pushnumber(L, camera->rotation[1]); lua_pushnumber(L, h->camera->rotation[1]);
return 2; return 2;
} }
// camera.set_distance(d) // h:set_distance(d)
static int lua_camera_set_distance(lua_State* L) { static int lua_camera_set_distance(lua_State* L) {
ClientContext* context = current_context(L); CameraLuaHandle* h = check_camera(L, 1);
context->camera.distance = luaL_checknumber(L, 1); h->camera->distance = luaL_checknumber(L, 2);
refresh_camera(context); refresh_camera(current_context(L), h->camera);
return 0; return 0;
} }
// camera.get_distance() -> d // h:get_distance() -> d
static int lua_camera_get_distance(lua_State* L) { 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; 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, &current_context(L)->render);
return 0;
}
void editor_lua_register(lua_State* L, ClientContext* context) { void editor_lua_register(lua_State* L, ClientContext* context) {
lua_pushlightuserdata(L, context); lua_pushlightuserdata(L, context);
lua_setfield(L, LUA_REGISTRYINDEX, EDITOR_LUA_CONTEXT); lua_setfield(L, LUA_REGISTRYINDEX, EDITOR_LUA_CONTEXT);
static const luaL_Reg camera_funcs[] = { 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}, {"set_position", lua_camera_set_position},
{"get_position", lua_camera_get_position}, {"get_position", lua_camera_get_position},
{"set_rotation", lua_camera_set_rotation}, {"set_rotation", lua_camera_set_rotation},
{"get_rotation", lua_camera_get_rotation}, {"get_rotation", lua_camera_get_rotation},
{"set_distance", lua_camera_set_distance}, {"set_distance", lua_camera_set_distance},
{"get_distance", lua_camera_get_distance}, {"get_distance", lua_camera_get_distance},
{"set_aspect", lua_camera_set_aspect},
{NULL, NULL}, {NULL, NULL},
}; };
luaL_newlib(L, camera_funcs); luaL_newmetatable(L, CAMERA_LUA_META);
lua_setglobal(L, "camera"); 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);
} }

@ -2,5 +2,7 @@
int main() { int main() {
EditorData* data = create_editor_data(); 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;
} }

@ -219,6 +219,38 @@ int app_main(ClientContext* context) {
return 0; 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( int run_app(
void* app_data, void* app_data,
app_startup_function app_startup, app_startup_function app_startup,
@ -268,5 +300,8 @@ int run_app(
if(app_startup != NULL) app_startup(context); if(app_startup != NULL) app_startup(context);
return app_main(context); int result = app_main(context);
shutdown_app(context);
free(context);
return result;
} }

@ -403,6 +403,33 @@ static int lua_app_set(lua_State* L) {
return 0; 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) { VkResult event_bus_init(EventBus* bus, lua_State* L) {
memset(bus, 0, sizeof(EventBus)); memset(bus, 0, sizeof(EventBus));
bus->L = L; bus->L = L;

@ -74,6 +74,7 @@ VkShaderModule load_shader_file(
int result = fseek(file, 0, SEEK_END); int result = fseek(file, 0, SEEK_END);
if(result != 0) { if(result != 0) {
fclose(file);
return VK_NULL_HANDLE; return VK_NULL_HANDLE;
} }
@ -81,15 +82,18 @@ VkShaderModule load_shader_file(
result = fseek(file, 0, SEEK_SET); result = fseek(file, 0, SEEK_SET);
if(result != 0) { if(result != 0) {
fclose(file);
return VK_NULL_HANDLE; return VK_NULL_HANDLE;
} }
char * buffer = malloc(buffer_size); char * buffer = malloc(buffer_size);
if(buffer == 0) { if(buffer == 0) {
fclose(file);
return VK_NULL_HANDLE; return VK_NULL_HANDLE;
} }
size_t read = fread(buffer, 1, buffer_size, file); size_t read = fread(buffer, 1, buffer_size, file);
fclose(file);
VkShaderModuleCreateInfo shader_info = { VkShaderModuleCreateInfo shader_info = {
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
@ -1031,11 +1035,11 @@ void retire_buffer(
gpu->retired_count += 1; 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 // Entries are appended in frame order, so eligible entries are a prefix
uint32_t i = 0; uint32_t i = 0;
while(i < gpu->retired_count 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); vmaDestroyBuffer(gpu->allocator, gpu->retired[i].buffer, gpu->retired[i].memory);
i += 1; 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( void destroy_transfer_buffer(
VmaAllocator allocator, VmaAllocator allocator,
VkBuffer buffer, 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); result = vkQueueSubmit(transfer_queue.handle, 1, &submit_info, submit_fence);
if(result != VK_SUCCESS) { if(result != VK_SUCCESS) {
vkDestroyFence(device, submit_fence, NULL);
vkFreeCommandBuffers(device, transfer_pool, 1, &command_buffer); vkFreeCommandBuffers(device, transfer_pool, 1, &command_buffer);
return result; return result;
} }
result = vkWaitForFences(device, 1, &submit_fence, VK_TRUE, UINT64_MAX); result = vkWaitForFences(device, 1, &submit_fence, VK_TRUE, UINT64_MAX);
vkDestroyFence(device, submit_fence, NULL);
vkFreeCommandBuffers(device, transfer_pool, 1, &command_buffer); vkFreeCommandBuffers(device, transfer_pool, 1, &command_buffer);
return result; return result;
} }
@ -1303,3 +1317,65 @@ VkResult recreate_framebuffer(RenderContext* gpu) {
return VK_SUCCESS; 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);
}

@ -223,6 +223,8 @@ VkResult create_point_pipeline(
1, &graphics_pipeline_info, 1, &graphics_pipeline_info,
NULL, NULL,
&pipeline->pipeline)); &pipeline->pipeline));
vkDestroyShaderModule(gpu->device, vert_shader, NULL);
vkDestroyShaderModule(gpu->device, frag_shader, NULL);
return VK_SUCCESS; return VK_SUCCESS;
} }
@ -390,6 +392,8 @@ VkResult create_ray_pipeline(
1, &graphics_pipeline_info, 1, &graphics_pipeline_info,
NULL, NULL,
&pipeline->pipeline)); &pipeline->pipeline));
vkDestroyShaderModule(gpu->device, vert_shader, NULL);
vkDestroyShaderModule(gpu->device, frag_shader, NULL);
return VK_SUCCESS; return VK_SUCCESS;
} }
@ -557,6 +561,8 @@ VkResult create_hex_highlight_pipeline(
1, &graphics_pipeline_info, 1, &graphics_pipeline_info,
NULL, NULL,
&pipeline->pipeline)); &pipeline->pipeline));
vkDestroyShaderModule(gpu->device, vert_shader, NULL);
vkDestroyShaderModule(gpu->device, frag_shader, NULL);
return VK_SUCCESS; return VK_SUCCESS;
} }
@ -724,6 +730,8 @@ VkResult create_hex_pipeline(
1, &graphics_pipeline_info, 1, &graphics_pipeline_info,
NULL, NULL,
&pipeline->pipeline)); &pipeline->pipeline));
vkDestroyShaderModule(gpu->device, vert_shader, NULL);
vkDestroyShaderModule(gpu->device, frag_shader, NULL);
return VK_SUCCESS; return VK_SUCCESS;
} }
@ -829,6 +837,34 @@ VkResult create_hex_context(
return VK_SUCCESS; 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) { VkResult set_hex_region(HexRegion* region, HexContext* hex, RenderContext* gpu) {
uint32_t index = UINT32_MAX; uint32_t index = UINT32_MAX;
for(uint32_t i = 0; i < MAX_LOADED_REGIONS; i++) { for(uint32_t i = 0; i < MAX_LOADED_REGIONS; i++) {

@ -59,6 +59,8 @@ VkResult create_ui_pipeline(
if(result != VK_SUCCESS) { if(result != VK_SUCCESS) {
return result; 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); VkShaderModule vert_shader = load_shader_file("shader/ui.vert.spv", device);
if(vert_shader == VK_NULL_HANDLE) { if(vert_shader == VK_NULL_HANDLE) {
@ -244,6 +246,8 @@ VkResult create_ui_pipeline(
if(result != VK_SUCCESS) { if(result != VK_SUCCESS) {
return result; return result;
} }
vkDestroyShaderModule(device, vert_shader, NULL);
vkDestroyShaderModule(device, frag_shader, NULL);
return VK_SUCCESS; return VK_SUCCESS;
} }
@ -1140,10 +1144,10 @@ VkResult load_font(
} }
context->fonts[index].family = malloc(strlen(face->family_name)+1); 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); 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); error = FT_Set_Pixel_Sizes(face, 0, size);
@ -1676,6 +1680,56 @@ VkResult create_ui_context(
return VK_SUCCESS; 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( VkResult map_string(
const char* text, const char* text,
uint32_t* buffer, uint32_t* buffer,

@ -448,6 +448,11 @@ static int lua_element_remove(lua_State* L) {
return 0; 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 handle methods ----------
// overlay:to_front() // overlay:to_front()

@ -201,6 +201,7 @@ int main() {
EditorData* data = create_editor_data(); 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); 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) { if(failures > 0) {
fprintf(stderr, "%d test(s) failed\n", failures); fprintf(stderr, "%d test(s) failed\n", failures);