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
+}