From 4212e57d06afac8a19b09fdebc24bad10b78f1ac Mon Sep 17 00:00:00 2001
From: 3gg <3gg@shellblade.net>
Date: Fri, 19 Jan 2024 07:21:45 -0800
Subject: Plugin refactor, moving scene from game to plugin.

---
 gltfview/src/game.c                 | 138 ++++++++++++++++++++++--------------
 gltfview/src/game.h                 |   2 -
 gltfview/src/plugins/gltf_view.c    |  77 +++++++++++++-------
 gltfview/src/plugins/gltf_view.h    |   9 ---
 gltfview/src/plugins/plugin.h       |  59 +++++++--------
 gltfview/src/plugins/texture_view.c |  91 +++++++++++++++++++++---
 gltfview/src/plugins/texture_view.h |   3 -
 7 files changed, 249 insertions(+), 130 deletions(-)
 delete mode 100644 gltfview/src/plugins/gltf_view.h
 delete mode 100644 gltfview/src/plugins/texture_view.h

(limited to 'gltfview')

diff --git a/gltfview/src/game.c b/gltfview/src/game.c
index c331190..64be4f3 100644
--- a/gltfview/src/game.c
+++ b/gltfview/src/game.c
@@ -39,6 +39,78 @@ static const int WIDTH   = 1350;
 static const int HEIGHT  = 900;
 static const int MAX_FPS = 60;
 
+/// Initialize the game's plugin.
+static bool init_plugin(Game* game) {
+  assert(game);
+  assert(game->plugin);
+  // Plugin state is allowed to be null, either when the plugin does not
+  // expose an init() or when init() does not initialize a state.
+  if (plugin_resolve(game->plugin, plugin_init, "init")) {
+    State* plugin_state = 0;
+    if (!plugin_call(game->plugin, plugin_init, "init", game, &plugin_state)) {
+      return false;
+    }
+    set_plugin_state(game->plugin, plugin_state);
+  }
+  return true; // Plugin does not need to expose an init().
+}
+
+/// Shutdown the game's plugin.
+/// The game's plugin is allowed to be null in the call to this function.
+static void shutdown_plugin(Game* game) {
+  assert(game);
+  if (game->plugin &&
+      (plugin_resolve(game->plugin, plugin_shutdown, "shutdown"))) {
+    void* plugin_state = get_plugin_state(game->plugin);
+    plugin_call(game->plugin, plugin_shutdown, "shutdown", game, plugin_state);
+    set_plugin_state(game->plugin, 0);
+  }
+}
+
+/// Boot the game's plugin.
+static bool boot_plugin(Game* game) {
+  assert(game);
+  assert(game->plugin);
+  if (plugin_resolve(game->plugin, plugin_boot, "boot")) {
+    void* plugin_state = get_plugin_state(game->plugin);
+    return plugin_call(game->plugin, plugin_boot, "boot", game, plugin_state);
+  }
+  return true; // Plugin does not need to expose a boot().
+}
+
+/// Update the plugin's state.
+static void update_plugin(Game* game, double t, double dt) {
+  assert(game);
+  assert(game->plugin);
+  if (plugin_resolve(game->plugin, plugin_update, "update")) {
+    void* plugin_state = get_plugin_state(game->plugin);
+    plugin_call(
+        game->plugin, plugin_update, "update", game, plugin_state, t, dt);
+  }
+}
+
+/// Plugin render.
+static void render_plugin(const Game* game) {
+  assert(game);
+  assert(game->plugin);
+  if (plugin_resolve(game->plugin, plugin_render, "render")) {
+    void* plugin_state = get_plugin_state(game->plugin);
+    plugin_call(game->plugin, plugin_render, "render", game, plugin_state);
+  }
+}
+
+/// Plugin resize.
+static void resize_plugin(Game* game, int width, int height) {
+  assert(game);
+  assert(game->plugin);
+  if (plugin_resolve(game->plugin, plugin_resize, "resize")) {
+    void* plugin_state = get_plugin_state(game->plugin);
+    plugin_call(
+        game->plugin, plugin_resize, "resize", game, plugin_state, width,
+        height);
+  }
+}
+
 void app_end(Game* game);
 
 bool app_init(const GfxAppDesc* desc, void** app_state) {
@@ -88,30 +160,14 @@ bool app_init(const GfxAppDesc* desc, void** app_state) {
   if (!(game->gfx = gfx_init())) {
     goto cleanup;
   }
-  if (!(game->scene = gfx_make_scene())) {
+
+  if (!init_plugin(game)) {
     goto cleanup;
   }
-  if (!(game->camera = gfx_make_camera())) {
+  if (!boot_plugin(game)) {
     goto cleanup;
   }
 
-  if (plugin_resolve(game->plugin, plugin_init, "init")) {
-    void* plugin_state = plugin_call(game->plugin, plugin_init, "init", game);
-    if (!plugin_state) {
-      goto cleanup;
-    }
-    set_plugin_state(game->plugin, plugin_state);
-  }
-
-  if (plugin_resolve(game->plugin, plugin_boot, "boot")) {
-    void* plugin_state = get_plugin_state(game->plugin);
-    bool  boot_success =
-        plugin_call(game->plugin, plugin_boot, "boot", plugin_state, game);
-    if (!boot_success) {
-      goto cleanup;
-    }
-  }
-
   *app_state = game;
   return true;
 
@@ -123,6 +179,7 @@ cleanup:
 
 void app_end(Game* game) {
   assert(game);
+  shutdown_plugin(game);
   if (game->gfx) {
     gfx_destroy(&game->gfx);
   }
@@ -136,53 +193,24 @@ void app_end(Game* game) {
 
 void app_update(Game* game, double t, double dt) {
   plugin_engine_update(game->plugin_engine);
-  if (plugin_reloaded(game->plugin) &&
-      plugin_resolve(game->plugin, plugin_init, "init")) {
-    void* plugin_state = plugin_call(game->plugin, plugin_init, "init", game);
-    assert(plugin_state); // TODO: handle error better.
-    set_plugin_state(game->plugin, plugin_state);
+  if (plugin_reloaded(game->plugin)) {
+    shutdown_plugin(game);
+    const bool result = init_plugin(game);
+    assert(result); // TODO: handle error better.
   }
 
-  if (plugin_resolve(game->plugin, plugin_update, "update")) {
-    // Plugin state may be null if plugin does not expose init().
-    void* plugin_state = get_plugin_state(game->plugin);
-    plugin_call(
-        game->plugin, plugin_update, "update", plugin_state, game, t, dt);
-  }
+  update_plugin(game, t, dt);
 }
 
 void app_render(const Game* game) {
   RenderBackend* render_backend = gfx_get_render_backend(game->gfx);
-  Renderer*      renderer       = gfx_get_renderer(game->gfx);
-
   gfx_start_frame(render_backend);
-
-  gfx_render_scene(
-      renderer,
-      &(RenderSceneParams){
-          .mode = RenderDefault, .scene = game->scene, .camera = game->camera});
-
-  if (plugin_resolve(game->plugin, plugin_render, "render")) {
-    // Plugin state may be null if plugin does not expose init().
-    void* plugin_state = get_plugin_state(game->plugin);
-    plugin_call(game->plugin, plugin_render, "render", plugin_state, game);
-  }
-
+  render_plugin(game);
   gfx_end_frame(render_backend);
 }
 
 void app_resize(Game* game, int width, int height) {
-  RenderBackend* render_backend = gfx_get_render_backend(game->gfx);
-  gfx_set_viewport(render_backend, width, height);
-
-  const R    fovy       = 90 * TO_RAD;
-  const R    aspect     = (R)width / (R)height;
-  const R    near       = 0.1;
-  const R    far        = 1000;
-  const mat4 projection = mat4_perspective(fovy, aspect, near, far);
-
-  Camera* camera     = gfx_get_camera_camera(game->camera);
-  camera->projection = projection;
+  resize_plugin(game, width, height);
 }
 
 GFX_APP_MAIN(WIDTH, HEIGHT, MAX_FPS);
diff --git a/gltfview/src/game.h b/gltfview/src/game.h
index 53725c7..93a5e39 100644
--- a/gltfview/src/game.h
+++ b/gltfview/src/game.h
@@ -16,6 +16,4 @@ typedef struct {
   PluginEngine* plugin_engine;
   Plugin*       plugin;
   Gfx*          gfx;
-  Scene*        scene;  // TODO: Move scene graph to plugin?
-  SceneCamera*  camera; // TODO: Move too.
 } Game;
diff --git a/gltfview/src/plugins/gltf_view.c b/gltfview/src/plugins/gltf_view.c
index c3457a8..c19d1b8 100644
--- a/gltfview/src/plugins/gltf_view.c
+++ b/gltfview/src/plugins/gltf_view.c
@@ -1,6 +1,7 @@
-#include "gltf_view.h"
+#include "plugin.h"
 
 #include <gfx/renderer.h>
+#include <gfx/scene.h>
 #include <gfx/util/scene.h>
 #include <gfx/util/skyquad.h>
 #include <gfx/util/texture.h>
@@ -23,6 +24,11 @@ static const char* GIRL =
 
 #define DEFAULT_SCENE_FILE GIRL
 
+struct State {
+  Scene*       scene;
+  SceneCamera* camera;
+};
+
 /// Load the skyquad texture.
 static Texture* load_environment_map(RenderBackend* render_backend) {
   return gfx_load_texture(
@@ -57,15 +63,17 @@ static SceneNode* load_skyquad(RenderBackend* render_backend, SceneNode* root) {
 }
 
 /// Load the 3D scene.
-static SceneNode* load_scene(Game* game, const char* scene_filepath) {
+static SceneNode* load_scene(
+    Game* game, State* state, const char* scene_filepath) {
   assert(game);
   assert(game->gfx);
-  assert(game->scene);
+  assert(state);
+  assert(state->scene);
 
-  SceneNode*     root           = gfx_get_scene_root(game->scene);
+  SceneNode*     root           = gfx_get_scene_root(state->scene);
   RenderBackend* render_backend = gfx_get_render_backend(game->gfx);
 
-  Camera* camera = gfx_get_camera_camera(game->camera);
+  Camera* camera = gfx_get_camera_camera(state->camera);
   spatial3_set_position(&camera->spatial, vec3_make(0, 0, 2));
 
   SceneNode* sky_light_node = load_skyquad(render_backend, root);
@@ -85,16 +93,20 @@ static SceneNode* load_scene(Game* game, const char* scene_filepath) {
   return scene_node;
 }
 
-State* init(Game* game) {
+bool init(Game* game, State** pp_state) {
   assert(game);
 
   State* state = calloc(1, sizeof(State));
-  return state;
-}
+  if (!state) {
+    goto cleanup;
+  }
 
-bool boot(State* state, Game* game) {
-  assert(state);
-  assert(game);
+  if (!(state->scene = gfx_make_scene())) {
+    goto cleanup;
+  }
+  if (!(state->camera = gfx_make_camera())) {
+    goto cleanup;
+  }
 
   const int    argc = game->argc;
   const char** argv = game->argv;
@@ -102,27 +114,44 @@ bool boot(State* state, Game* game) {
   // Usage: <scene file>
   const char* scene_filepath = argc > 1 ? argv[1] : DEFAULT_SCENE_FILE;
 
-  SceneNode* node = load_scene(game, scene_filepath);
+  SceneNode* node = load_scene(game, state, scene_filepath);
   if (!node) {
-    return false;
+    goto cleanup;
   }
   Anima* anima = gfx_get_node_anima(node);
   gfx_play_animation(
       anima, &(AnimationPlaySettings){.name = "Walk", .loop = true});
 
+  *pp_state = state;
   return true;
+
+cleanup:
+  shutdown(game, state);
+  if (state) {
+    free(state);
+  }
+  return false;
 }
 
-void update(State* state, Game* game, double t, double dt) {
-  assert(state);
+void shutdown(Game* game, State* state) {
+  assert(game);
+  if (state) {
+    gfx_destroy_camera(&state->camera);
+    gfx_destroy_scene(&state->scene);
+    // State freed by plugin engine.
+  }
+}
+
+void update(Game* game, State* state, double t, double dt) {
   assert(game);
-  assert(game->scene);
-  assert(game->camera);
+  assert(state);
+  assert(state->scene);
+  assert(state->camera);
 
-  gfx_animate_scene(game->scene, (R)t);
+  gfx_animate_scene(state->scene, (R)t);
 
   const vec3 orbit_point = vec3_make(0, 2, 0);
-  Camera*    camera      = gfx_get_camera_camera(game->camera);
+  Camera*    camera      = gfx_get_camera_camera(state->camera);
   spatial3_orbit(
       &camera->spatial, orbit_point,
       /*radius=*/2.5,
@@ -150,18 +179,18 @@ static void render_bounding_boxes(ImmRenderer* imm, const SceneNode* node) {
   }
 }
 
-void render(State* state, const Game* game) {
+void render(const Game* game, const State* state) {
   assert(state);
   assert(game);
   assert(game->gfx);
-  assert(game->scene);
-  assert(game->camera);
+  assert(state->scene);
+  assert(state->camera);
 
   ImmRenderer* imm = gfx_get_imm_renderer(game->gfx);
   assert(imm);
   gfx_imm_start(imm);
-  gfx_imm_set_camera(imm, gfx_get_camera_camera(game->camera));
+  gfx_imm_set_camera(imm, gfx_get_camera_camera(state->camera));
   gfx_imm_set_colour(imm, vec4_make(0.2, 0.2, 1.0, 0.3));
-  render_bounding_boxes(imm, gfx_get_scene_root(game->scene));
+  render_bounding_boxes(imm, gfx_get_scene_root(state->scene));
   gfx_imm_end(imm);
 }
diff --git a/gltfview/src/plugins/gltf_view.h b/gltfview/src/plugins/gltf_view.h
deleted file mode 100644
index 670d88d..0000000
--- a/gltfview/src/plugins/gltf_view.h
+++ /dev/null
@@ -1,9 +0,0 @@
-#pragma once
-
-#include "plugin.h"
-
-#include <gfx/scene.h>
-
-typedef struct State {
-  int unused;
-} State;
diff --git a/gltfview/src/plugins/plugin.h b/gltfview/src/plugins/plugin.h
index 0e0e12c..a2632cd 100644
--- a/gltfview/src/plugins/plugin.h
+++ b/gltfview/src/plugins/plugin.h
@@ -1,21 +1,5 @@
 /*
  * Game plugin.
- *
- * A game plugin exposes three functions:
- * - boot(): called once when the plugin is first loaded during the lifetime of
- * the game.
- * - init() -> state: creates and returns the plugin's state.
- * - update(state): takes and updates the state, possibly with side effects.
- * - render(): performs custom rendering.
- *
- * boot() is convenient for one-time initialization of the scene.
- *
- * init() is called every time the plugin is loaded. It is assumed that the
- * plugin's state is encapsulated in the object returned.
- *
- * update() updates the plugin state and has side effects on the scene. It is
- * assumed that update does not reference any global, mutable state outside of
- * the scene and the plugin state returned by init().
  */
 #pragma once
 
@@ -28,22 +12,41 @@
 
 typedef struct State State;
 
-/// Initialize the plugin's state.
-State* init(Game*);
+/// Initialize the plugin, which may optionally return a state object.
+///
+/// This function is called every time the plugin is (re)loaded.
+///
+/// It is assumed that the plugin's state is fully encapsulated in the returned
+/// state object. The plugin should not store any (mutable) state outside of the
+/// returned state object (e.g., no mutable global variables.)
+bool init(Game*, State**);
+
+/// Shut down the plugin.
+///
+/// This function is called before the plugin is unloaded.
+///
+/// The plugin should perform any destruction needed, but not free the state
+/// object; freeing the state object's memory is handled by the caller.
+void shutdown(Game*, State*);
 
 /// Function called the first time the plugin is loaded throughout the
-/// application's lifetime. Allows the plugin to do one-time initialization of
-/// the game state.
-bool boot(State*, Game*);
+/// application's lifetime. This allows the plugin to do one-time initialization
+/// of the game state.
+bool boot(Game*, State*);
 
 /// Update the plugin's and the game's state.
-void update(State*, Game*, double t, double dt);
+void update(Game*, State*, double t, double dt);
 
-/// Optional plugin rendering hook.
-void render(State*, const Game*);
+/// Render hook.
+void render(const Game*, const State*);
+
+/// Called when the game's window is resized.
+void resize(Game* game, State* state, int width, int height);
 
 // Signatures for the plugin's exposed functions.
-typedef void* (*plugin_init)(Game*);
-typedef bool (*plugin_boot)(State*, Game*);
-typedef void (*plugin_update)(State*, Game*, double t, double dt);
-typedef void (*plugin_render)(State*, const Game*);
+typedef bool (*plugin_init)(Game*, State**);
+typedef bool (*plugin_shutdown)(Game*, State*);
+typedef bool (*plugin_boot)(Game*, State*);
+typedef void (*plugin_update)(Game*, State*, double t, double dt);
+typedef void (*plugin_render)(const Game*, const State*);
+typedef void (*plugin_resize)(Game* game, State* state, int width, int height);
diff --git a/gltfview/src/plugins/texture_view.c b/gltfview/src/plugins/texture_view.c
index f16c8d1..b424158 100644
--- a/gltfview/src/plugins/texture_view.c
+++ b/gltfview/src/plugins/texture_view.c
@@ -1,6 +1,7 @@
-#include "texture_view.h"
+#include "plugin.h"
 
 #include <gfx/render_backend.h>
+#include <gfx/renderer.h>
 #include <gfx/scene.h>
 #include <gfx/util/geometry.h>
 #include <gfx/util/shader.h>
@@ -9,13 +10,25 @@
 #include <math/camera.h>
 
 #include <assert.h>
+#include <stdlib.h>
 
 // Default texture to load if no texture is provided.
 static const char* DEFAULT_TEXTURE = "/assets/skybox/clouds1/clouds1_west.bmp";
 // static const char* DEFAULT_TEXTURE = "/assets/checkerboard.jpg";
 
-bool boot(State* state, Game* game) {
+struct State {
+  Scene*       scene;
+  SceneCamera* camera;
+};
+
+bool init(Game* game, State** pp_state) {
   assert(game);
+  assert(pp_state);
+
+  State* state = calloc(1, sizeof(State));
+  if (!state) {
+    goto cleanup;
+  }
 
   // Usage: [texture file]
   const char* texture_file = game->argc > 1 ? game->argv[1] : DEFAULT_TEXTURE;
@@ -30,17 +43,17 @@ bool boot(State* state, Game* game) {
                           .mipmaps               = false,
                           .data.texture.filepath = mstring_make(texture_file)});
   if (!texture) {
-    return false;
+    goto cleanup;
   }
 
   ShaderProgram* shader = gfx_make_view_texture_shader(render_backend);
   if (!shader) {
-    return false;
+    goto cleanup;
   }
 
   Geometry* geometry = gfx_make_quad_11(render_backend);
   if (!geometry) {
-    return false;
+    goto cleanup;
   }
 
   MaterialDesc material_desc = (MaterialDesc){.num_uniforms = 1};
@@ -50,25 +63,85 @@ bool boot(State* state, Game* game) {
        .name          = sstring_make("Texture")};
   Material* material = gfx_make_material(&material_desc);
   if (!material) {
-    return false;
+    goto cleanup;
   }
 
   const MeshDesc mesh_desc =
       (MeshDesc){.geometry = geometry, .material = material, .shader = shader};
   Mesh* mesh = gfx_make_mesh(&mesh_desc);
   if (!mesh) {
-    return false;
+    goto cleanup;
   }
 
   SceneObject* object = gfx_make_object();
   if (!object) {
-    return false;
+    goto cleanup;
   }
   gfx_add_object_mesh(object, mesh);
 
+  if (!(state->scene = gfx_make_scene())) {
+    goto cleanup;
+  }
+
   SceneNode* node = gfx_make_object_node(object);
-  SceneNode* root = gfx_get_scene_root(game->scene);
+  if (!node) {
+    goto cleanup;
+  }
+  SceneNode* root = gfx_get_scene_root(state->scene);
+  if (!root) {
+    goto cleanup;
+  }
   gfx_set_node_parent(node, root);
 
+  if (!(state->camera = gfx_make_camera())) {
+    goto cleanup;
+  }
+
+  *pp_state = state;
   return true;
+
+cleanup:
+  shutdown(game, state);
+  if (state) {
+    free(state);
+  }
+  return false;
+}
+
+void shutdown(Game* game, State* state) {
+  assert(game);
+  if (state) {
+    gfx_destroy_camera(&state->camera);
+    gfx_destroy_scene(&state->scene);
+    // State freed by plugin engine.
+  }
+}
+
+void render(const Game* game, const State* state) {
+  assert(game);
+  assert(state);
+
+  Renderer* renderer = gfx_get_renderer(game->gfx);
+  gfx_render_scene(
+      renderer, &(RenderSceneParams){
+                    .mode   = RenderDefault,
+                    .scene  = state->scene,
+                    .camera = state->camera});
+}
+
+void resize(Game* game, State* state, int width, int height) {
+  assert(game);
+  assert(state);
+
+  RenderBackend* render_backend = gfx_get_render_backend(game->gfx);
+  gfx_set_viewport(render_backend, width, height);
+
+  const R    fovy       = 90 * TO_RAD;
+  const R    aspect     = (R)width / (R)height;
+  const R    near       = 0.1;
+  const R    far        = 1000;
+  const mat4 projection = mat4_perspective(fovy, aspect, near, far);
+
+  Camera* camera     = gfx_get_camera_camera(state->camera);
+  camera->projection = projection;
 }
diff --git a/gltfview/src/plugins/texture_view.h b/gltfview/src/plugins/texture_view.h
deleted file mode 100644
index 956f34a..0000000
--- a/gltfview/src/plugins/texture_view.h
+++ /dev/null
@@ -1,3 +0,0 @@
-#pragma once
-
-#include "plugin.h"
-- 
cgit v1.2.3