Mercurial
diff infinite_canvas/scene_store.c @ 281:c57149ad216e default tip
Copilot-Session: f68442b1-fa8f-46a0-9689-81710613bbd4
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Tue, 18 Aug 2026 22:18:15 -0700 |
| parents | |
| children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/infinite_canvas/scene_store.c Tue Aug 18 22:18:15 2026 -0700 @@ -0,0 +1,569 @@ +#include "infinite_canvas/scene_store.h" + +#include <ctype.h> +#include <math.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#if defined(_WIN32) +#include <windows.h> +#elif !defined(PLATFORM_WEB) +#include <unistd.h> +#endif + +#define CANVAS_SCENE_STORE_VERSION 1 +#define CANVAS_SCENE_STORE_HEADER_SIZE 48 +#define CANVAS_SCENE_STORE_ENTITY_SIZE 2240 +#define CANVAS_SCENE_STORE_MAX_BYTES \ + (CANVAS_SCENE_STORE_HEADER_SIZE + \ + CANVAS_MAX_ENTITIES * CANVAS_SCENE_STORE_ENTITY_SIZE) + +static const uint8 CANVAS_SCENE_STORE_MAGIC[8] = { + 'Z', 'E', 'N', 'B', 'U', 'M', 'A', 'P', +}; + +_Static_assert(sizeof(Color) == 4, "scene format requires RGBA8 Color"); +_Static_assert( + CANVAS_ENTITY_TEXT_CAPACITY == 2048, + "scene format version must change when text capacity changes"); +_Static_assert( + CANVAS_SCENE_STORE_ENTITY_SIZE == + 2 * 4 + 4 * 4 + 4 + 128 + 2048 + 3 * 4 + 6 * 4, + "scene entity record size is inconsistent"); + +typedef struct { + uint8 *p_data; + size_t size; + size_t offset; + boolean valid; +} Canvas_Scene_Writer; + +typedef struct { + const uint8 *p_data; + size_t size; + size_t offset; + boolean valid; +} Canvas_Scene_Reader; + +static uint64 Canvas_Scene_Store_FNV( + uint64 hash, + const uint8 *p_data, + size_t size) +{ + for (size_t index = 0; index < size; index++) { + hash ^= p_data[index]; + hash *= 1099511628211ULL; + } + return hash; +} + +static void Canvas_Scene_Store_Write( + Canvas_Scene_Writer *p_writer, + const void *p_data, + size_t size) +{ + if (!p_writer->valid || + p_writer->offset > p_writer->size || + size > p_writer->size - p_writer->offset) { + p_writer->valid = FALSE; + return; + } + memcpy(p_writer->p_data + p_writer->offset, p_data, size); + p_writer->offset += size; +} + +static void Canvas_Scene_Store_Write_U32( + Canvas_Scene_Writer *p_writer, + uint32 value) +{ + uint8 bytes[4] = { + (uint8)(value & 0xff), + (uint8)((value >> 8) & 0xff), + (uint8)((value >> 16) & 0xff), + (uint8)((value >> 24) & 0xff), + }; + Canvas_Scene_Store_Write(p_writer, bytes, sizeof(bytes)); +} + +static void Canvas_Scene_Store_Write_F32( + Canvas_Scene_Writer *p_writer, + float value) +{ + uint32 bits = 0; + memcpy(&bits, &value, sizeof(bits)); + Canvas_Scene_Store_Write_U32(p_writer, bits); +} + +static void Canvas_Scene_Store_Read( + Canvas_Scene_Reader *p_reader, + void *p_output, + size_t size) +{ + if (!p_reader->valid || + p_reader->offset > p_reader->size || + size > p_reader->size - p_reader->offset) { + p_reader->valid = FALSE; + return; + } + memcpy(p_output, p_reader->p_data + p_reader->offset, size); + p_reader->offset += size; +} + +static uint32 Canvas_Scene_Store_Read_U32( + Canvas_Scene_Reader *p_reader) +{ + uint8 bytes[4] = {0}; + Canvas_Scene_Store_Read(p_reader, bytes, sizeof(bytes)); + return (uint32)bytes[0] | + ((uint32)bytes[1] << 8) | + ((uint32)bytes[2] << 16) | + ((uint32)bytes[3] << 24); +} + +static float Canvas_Scene_Store_Read_F32( + Canvas_Scene_Reader *p_reader) +{ + uint32 bits = Canvas_Scene_Store_Read_U32(p_reader); + float value = 0.0f; + memcpy(&value, &bits, sizeof(value)); + return value; +} + +static uint32 Canvas_Scene_Store_Entity_Flags( + const Canvas_Entity *p_entity) +{ + uint32 flags = 0; + if (p_entity->active) flags |= 1u << 0; + if (p_entity->pinned) flags |= 1u << 1; + if (p_entity->text_muted) flags |= 1u << 2; + if (p_entity->parked) flags |= 1u << 3; + return flags; +} + +static void Canvas_Scene_Store_Write_Entity( + Canvas_Scene_Writer *p_writer, + const Canvas_Entity *p_entity) +{ + Canvas_Scene_Store_Write_U32(p_writer, p_entity->id); + Canvas_Scene_Store_Write_U32(p_writer, (uint32)p_entity->type); + Canvas_Scene_Store_Write_F32(p_writer, p_entity->position.x); + Canvas_Scene_Store_Write_F32(p_writer, p_entity->position.y); + Canvas_Scene_Store_Write_F32(p_writer, p_entity->size.x); + Canvas_Scene_Store_Write_F32(p_writer, p_entity->size.y); + Canvas_Scene_Store_Write(p_writer, &p_entity->color, 4); + Canvas_Scene_Store_Write( + p_writer, p_entity->label, sizeof(p_entity->label)); + Canvas_Scene_Store_Write( + p_writer, p_entity->text, sizeof(p_entity->text)); + Canvas_Scene_Store_Write_U32(p_writer, (uint32)p_entity->value); + Canvas_Scene_Store_Write_F32(p_writer, p_entity->text_scroll_y); + Canvas_Scene_Store_Write_U32( + p_writer, Canvas_Scene_Store_Entity_Flags(p_entity)); + Canvas_Scene_Store_Write_F32( + p_writer, p_entity->pinned_screen_position.x); + Canvas_Scene_Store_Write_F32( + p_writer, p_entity->pinned_screen_position.y); + Canvas_Scene_Store_Write_F32( + p_writer, p_entity->pinned_screen_size.x); + Canvas_Scene_Store_Write_F32( + p_writer, p_entity->pinned_screen_size.y); + Canvas_Scene_Store_Write_F32( + p_writer, p_entity->parked_restore_position.x); + Canvas_Scene_Store_Write_F32( + p_writer, p_entity->parked_restore_position.y); +} + +static Canvas_Entity Canvas_Scene_Store_Read_Entity( + Canvas_Scene_Reader *p_reader) +{ + Canvas_Entity entity = {0}; + entity.id = Canvas_Scene_Store_Read_U32(p_reader); + entity.type = + (Canvas_Entity_Type)Canvas_Scene_Store_Read_U32(p_reader); + entity.position.x = Canvas_Scene_Store_Read_F32(p_reader); + entity.position.y = Canvas_Scene_Store_Read_F32(p_reader); + entity.size.x = Canvas_Scene_Store_Read_F32(p_reader); + entity.size.y = Canvas_Scene_Store_Read_F32(p_reader); + Canvas_Scene_Store_Read(p_reader, &entity.color, 4); + Canvas_Scene_Store_Read( + p_reader, entity.label, sizeof(entity.label)); + Canvas_Scene_Store_Read( + p_reader, entity.text, sizeof(entity.text)); + entity.label[sizeof(entity.label) - 1] = '\0'; + entity.text[sizeof(entity.text) - 1] = '\0'; + entity.value = (int32)Canvas_Scene_Store_Read_U32(p_reader); + entity.text_scroll_y = Canvas_Scene_Store_Read_F32(p_reader); + uint32 flags = Canvas_Scene_Store_Read_U32(p_reader); + entity.active = flags & (1u << 0) ? TRUE : FALSE; + entity.pinned = flags & (1u << 1) ? TRUE : FALSE; + entity.text_muted = flags & (1u << 2) ? TRUE : FALSE; + entity.parked = flags & (1u << 3) ? TRUE : FALSE; + entity.pinned_screen_position.x = + Canvas_Scene_Store_Read_F32(p_reader); + entity.pinned_screen_position.y = + Canvas_Scene_Store_Read_F32(p_reader); + entity.pinned_screen_size.x = + Canvas_Scene_Store_Read_F32(p_reader); + entity.pinned_screen_size.y = + Canvas_Scene_Store_Read_F32(p_reader); + entity.parked_restore_position.x = + Canvas_Scene_Store_Read_F32(p_reader); + entity.parked_restore_position.y = + Canvas_Scene_Store_Read_F32(p_reader); + entity.text_cursor = (int32)strlen(entity.text); + entity.text_selection_anchor = entity.text_cursor; + entity.visibility_amount = 1.0f; + entity.screen_space_initialized = entity.pinned; + entity.animation_amount = + entity.type == CANVAS_ENTITY_CONVERSATION + ? (entity.active ? 0.0f : 1.0f) + : (entity.active ? 1.0f : 0.0f); + return entity; +} + +static boolean Canvas_Scene_Store_Entity_Valid( + const Canvas_Entity *p_entity) +{ + boolean base_valid = p_entity->id != 0 && + p_entity->type >= 0 && + p_entity->type < CANVAS_ENTITY_TYPE_COUNT && + isfinite(p_entity->position.x) && + isfinite(p_entity->position.y) && + isfinite(p_entity->size.x) && + isfinite(p_entity->size.y) && + p_entity->size.x > 0.0f && + p_entity->size.y > 0.0f && + fabsf(p_entity->position.x) <= 1000000000.0f && + fabsf(p_entity->position.y) <= 1000000000.0f && + p_entity->size.x <= 10000000.0f && + p_entity->size.y <= 10000000.0f; + if (!base_valid) return FALSE; + if (p_entity->pinned && + (!isfinite(p_entity->pinned_screen_position.x) || + !isfinite(p_entity->pinned_screen_position.y) || + !isfinite(p_entity->pinned_screen_size.x) || + !isfinite(p_entity->pinned_screen_size.y) || + p_entity->pinned_screen_size.x <= 0.0f || + p_entity->pinned_screen_size.y <= 0.0f)) { + return FALSE; + } + return !p_entity->parked || + (isfinite(p_entity->parked_restore_position.x) && + isfinite(p_entity->parked_restore_position.y)); +} + +boolean Canvas_Scene_Store_Path( + const char *p_name, + char *p_path, + size_t path_capacity) +{ +#if defined(PLATFORM_WEB) + (void)p_name; + (void)p_path; + (void)path_capacity; + return FALSE; +#else + if (!p_name || !p_name[0] || !p_path || path_capacity == 0) { + return FALSE; + } + size_t name_length = strlen(p_name); + if (name_length >= CANVAS_SCENE_STORE_NAME_CAPACITY) return FALSE; + for (size_t index = 0; index < name_length; index++) { + uint8 character = (uint8)p_name[index]; + if (!isalnum(character) && + character != '-' && + character != '_') { + return FALSE; + } + } + const char *p_root = getenv("XDG_STATE_HOME"); + const char *p_suffix = "/zenbu/infinite-canvas/snapshots"; + char fallback[CANVAS_SCENE_STORE_PATH_CAPACITY]; + if (!p_root || !p_root[0]) { + const char *p_home = getenv("HOME"); + if (!p_home || !p_home[0]) return FALSE; + int fallback_written = snprintf( + fallback, + sizeof(fallback), + "%s/.local/state", + p_home); + if (fallback_written <= 0 || + (size_t)fallback_written >= sizeof(fallback)) { + return FALSE; + } + p_root = fallback; + } + int written = snprintf( + p_path, + path_capacity, + "%s%s/%s.zmap", + p_root, + p_suffix, + p_name); + return written > 0 && (size_t)written < path_capacity; +#endif +} + +boolean Canvas_Scene_Store_Save( + const char *p_path, + const Canvas_Scene *p_scene, + const Canvas_Camera *p_camera) +{ +#if defined(PLATFORM_WEB) + (void)p_path; + (void)p_scene; + (void)p_camera; + return FALSE; +#else + if (!p_path || !p_scene || !p_camera) return FALSE; + size_t entity_count = 0; + for (size_t index = 0; + index < Dowa_Array_Length(p_scene->p_entities); + index++) { + if (!p_scene->p_entities[index].removing) entity_count++; + } + if (entity_count > CANVAS_MAX_ENTITIES) return FALSE; + size_t size = CANVAS_SCENE_STORE_HEADER_SIZE + + entity_count * CANVAS_SCENE_STORE_ENTITY_SIZE; + uint8 *p_data = (uint8 *)calloc(size, 1); + if (!p_data) return FALSE; + Canvas_Scene_Writer writer = { + .p_data = p_data, + .size = size, + .valid = TRUE, + }; + Canvas_Scene_Store_Write( + &writer, CANVAS_SCENE_STORE_MAGIC, sizeof(CANVAS_SCENE_STORE_MAGIC)); + Canvas_Scene_Store_Write_U32(&writer, CANVAS_SCENE_STORE_VERSION); + Canvas_Scene_Store_Write_U32(&writer, (uint32)entity_count); + Canvas_Scene_Store_Write_U32(&writer, p_scene->next_entity); + Canvas_Scene_Store_Write_U32(&writer, p_scene->next_entity_id); + Canvas_Scene_Store_Write_U32( + &writer, p_scene->show_grid ? 1u : 0u); + Canvas_Scene_Store_Write_F32(&writer, p_camera->target.x); + Canvas_Scene_Store_Write_F32(&writer, p_camera->target.y); + Canvas_Scene_Store_Write_F32(&writer, p_camera->zoom); + Canvas_Scene_Store_Write_U32( + &writer, (uint32)(size - CANVAS_SCENE_STORE_HEADER_SIZE)); + Canvas_Scene_Store_Write_U32(&writer, 0); + for (size_t index = 0; + index < Dowa_Array_Length(p_scene->p_entities); + index++) { + if (!p_scene->p_entities[index].removing) { + Canvas_Scene_Store_Write_Entity( + &writer, &p_scene->p_entities[index]); + } + } + if (!writer.valid || writer.offset != size) { + Dowa_Free(p_data); + return FALSE; + } + uint64 checksum_hash = Canvas_Scene_Store_FNV( + 1469598103934665603ULL, + p_data, + 44); + checksum_hash = Canvas_Scene_Store_FNV( + checksum_hash, + p_data + CANVAS_SCENE_STORE_HEADER_SIZE, + size - CANVAS_SCENE_STORE_HEADER_SIZE); + uint32 checksum = (uint32)checksum_hash; + p_data[44] = (uint8)(checksum & 0xff); + p_data[45] = (uint8)((checksum >> 8) & 0xff); + p_data[46] = (uint8)((checksum >> 16) & 0xff); + p_data[47] = (uint8)((checksum >> 24) & 0xff); + + char directory[CANVAS_SCENE_STORE_PATH_CAPACITY]; + char temporary[CANVAS_SCENE_STORE_PATH_CAPACITY]; + snprintf(directory, sizeof(directory), "%s", p_path); + char *p_separator = strrchr(directory, '/'); +#if defined(_WIN32) + char *p_windows_separator = strrchr(directory, '\\'); + if (!p_separator || p_windows_separator > p_separator) { + p_separator = p_windows_separator; + } +#endif + if (!p_separator) { + Dowa_Free(p_data); + return FALSE; + } + *p_separator = '\0'; + if (MakeDirectory(directory) != 0 && !DirectoryExists(directory)) { + Dowa_Free(p_data); + return FALSE; + } + int temporary_written = snprintf( + temporary, sizeof(temporary), "%s.tmp", p_path); + if (temporary_written <= 0 || + (size_t)temporary_written >= sizeof(temporary)) { + Dowa_Free(p_data); + return FALSE; + } + FILE *p_file = fopen(temporary, "wb"); + if (!p_file) { + Dowa_Free(p_data); + return FALSE; + } + boolean success = + fwrite(p_data, 1, size, p_file) == size && + fflush(p_file) == 0; +#if !defined(_WIN32) + if (success) success = fsync(fileno(p_file)) == 0; +#endif + if (fclose(p_file) != 0) success = FALSE; + Dowa_Free(p_data); + if (!success) { + remove(temporary); + return FALSE; + } +#if defined(_WIN32) + success = MoveFileExA( + temporary, + p_path, + MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) + ? TRUE + : FALSE; +#else + success = rename(temporary, p_path) == 0 ? TRUE : FALSE; +#endif + if (!success) remove(temporary); + return success; +#endif +} + +boolean Canvas_Scene_Store_Load( + const char *p_path, + Canvas_Scene *p_scene, + Canvas_Camera *p_camera) +{ +#if defined(PLATFORM_WEB) + (void)p_path; + (void)p_scene; + (void)p_camera; + return FALSE; +#else + if (!p_path || !p_scene || !p_camera) return FALSE; + FILE *p_file = fopen(p_path, "rb"); + if (!p_file) return FALSE; + if (fseek(p_file, 0, SEEK_END) != 0) { + fclose(p_file); + return FALSE; + } + long file_size = ftell(p_file); + if (file_size < CANVAS_SCENE_STORE_HEADER_SIZE || + file_size > CANVAS_SCENE_STORE_MAX_BYTES || + fseek(p_file, 0, SEEK_SET) != 0) { + fclose(p_file); + return FALSE; + } + uint8 *p_data = (uint8 *)malloc((size_t)file_size); + if (!p_data) { + fclose(p_file); + return FALSE; + } + boolean success = + fread(p_data, 1, (size_t)file_size, p_file) == (size_t)file_size; + fclose(p_file); + if (!success) { + Dowa_Free(p_data); + return FALSE; + } + + Canvas_Scene_Reader reader = { + .p_data = p_data, + .size = (size_t)file_size, + .valid = TRUE, + }; + uint8 magic[8] = {0}; + Canvas_Scene_Store_Read(&reader, magic, sizeof(magic)); + uint32 version = Canvas_Scene_Store_Read_U32(&reader); + uint32 entity_count = Canvas_Scene_Store_Read_U32(&reader); + uint32 next_entity = Canvas_Scene_Store_Read_U32(&reader); + uint32 next_entity_id = Canvas_Scene_Store_Read_U32(&reader); + uint32 scene_flags = Canvas_Scene_Store_Read_U32(&reader); + float camera_x = Canvas_Scene_Store_Read_F32(&reader); + float camera_y = Canvas_Scene_Store_Read_F32(&reader); + float camera_zoom = Canvas_Scene_Store_Read_F32(&reader); + uint32 payload_size = Canvas_Scene_Store_Read_U32(&reader); + uint32 stored_checksum = Canvas_Scene_Store_Read_U32(&reader); + size_t expected_size = CANVAS_SCENE_STORE_HEADER_SIZE + + (size_t)entity_count * CANVAS_SCENE_STORE_ENTITY_SIZE; + uint64 checksum_hash = Canvas_Scene_Store_FNV( + 1469598103934665603ULL, + p_data, + 44); + checksum_hash = Canvas_Scene_Store_FNV( + checksum_hash, + p_data + CANVAS_SCENE_STORE_HEADER_SIZE, + (size_t)file_size - CANVAS_SCENE_STORE_HEADER_SIZE); + uint32 actual_checksum = (uint32)checksum_hash; + if (!reader.valid || + memcmp(magic, CANVAS_SCENE_STORE_MAGIC, sizeof(magic)) != 0 || + version != CANVAS_SCENE_STORE_VERSION || + entity_count > CANVAS_MAX_ENTITIES || + expected_size != (size_t)file_size || + payload_size != (uint32)( + (size_t)file_size - CANVAS_SCENE_STORE_HEADER_SIZE) || + stored_checksum != actual_checksum || + next_entity == 0xffffffffu || + next_entity_id == 0xffffffffu || + !isfinite(camera_x) || + !isfinite(camera_y) || + !isfinite(camera_zoom) || + fabsf(camera_x) > 1000000000.0f || + fabsf(camera_y) > 1000000000.0f || + camera_zoom < p_camera->min_zoom || + camera_zoom > p_camera->max_zoom) { + Dowa_Free(p_data); + return FALSE; + } + + Canvas_Entity *p_entities = (Canvas_Entity *)calloc( + entity_count ? entity_count : 1, + sizeof(*p_entities)); + if (!p_entities) { + Dowa_Free(p_data); + return FALSE; + } + uint32 max_id = 0; + for (uint32 index = 0; index < entity_count; index++) { + p_entities[index] = Canvas_Scene_Store_Read_Entity(&reader); + if (!reader.valid || + !Canvas_Scene_Store_Entity_Valid(&p_entities[index])) { + Dowa_Free(p_entities); + Dowa_Free(p_data); + return FALSE; + } + for (uint32 previous = 0; previous < index; previous++) { + if (p_entities[previous].id == p_entities[index].id) { + Dowa_Free(p_entities); + Dowa_Free(p_data); + return FALSE; + } + } + if (p_entities[index].id > max_id) { + max_id = p_entities[index].id; + } + } + Dowa_Free(p_data); + if (!reader.valid || reader.offset != (size_t)file_size) { + Dowa_Free(p_entities); + return FALSE; + } + + Canvas_Scene_Clear(p_scene); + for (uint32 index = 0; index < entity_count; index++) { + Dowa_Array_Push_Arena( + p_scene->p_entities, p_entities[index], p_scene->p_arena); + } + Dowa_Free(p_entities); + p_scene->next_entity = + next_entity < entity_count ? entity_count : next_entity; + p_scene->next_entity_id = + next_entity_id < max_id ? max_id : next_entity_id; + p_scene->show_grid = scene_flags & 1u ? TRUE : FALSE; + p_camera->target = (Vector2){camera_x, camera_y}; + p_camera->zoom = camera_zoom; + return TRUE; +#endif +}