From 520e4e67cd9ff53f3c3512c80d07193625e07e3e Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Fri, 16 Jun 2023 09:38:15 -0700 Subject: New plugin architecture. --- gltfview/src/game.c | 279 ++++++++++++------------------------ gltfview/src/game.h | 16 ++- gltfview/src/plugins/CMakeLists.txt | 17 +++ gltfview/src/plugins/gltf_view.c | 167 +++++++++++++++++++++ gltfview/src/plugins/gltf_view.h | 9 ++ gltfview/src/plugins/plugin.h | 49 +++++++ gltfview/src/plugins/texture_view.c | 94 ++++++++++++ gltfview/src/plugins/texture_view.h | 9 ++ 8 files changed, 444 insertions(+), 196 deletions(-) create mode 100644 gltfview/src/plugins/CMakeLists.txt create mode 100644 gltfview/src/plugins/gltf_view.c create mode 100644 gltfview/src/plugins/gltf_view.h create mode 100644 gltfview/src/plugins/plugin.h create mode 100644 gltfview/src/plugins/texture_view.c create mode 100644 gltfview/src/plugins/texture_view.h (limited to 'gltfview/src') diff --git a/gltfview/src/game.c b/gltfview/src/game.c index c80a1b5..6d8430b 100644 --- a/gltfview/src/game.c +++ b/gltfview/src/game.c @@ -1,258 +1,157 @@ +/* + * Main game module with entry point and game loop. + * + * The game module sets up the window and GL context and defers the core game + * logic to a plugin. + */ +#define _GNU_SOURCE 200112L // For readlink() + #include "game.h" +#include "plugins/plugin.h" #include #include #include -#include -#include -#include #include -#include -#include -#include -#include -#include #include #include +#include #include #include #include -// Paths to various scene files. -static const char* BOX = "/assets/models/box.gltf"; -static const char* SUZANNE = "/assets/models/suzanne.gltf"; -static const char* SPONZA = - "/assets/glTF-Sample-Models/2.0/Sponza/glTF/Sponza.gltf"; -static const char* FLIGHT_HELMET = - "/assets/glTF-Sample-Models/2.0/FlightHelmet/glTF/FlightHelmet.gltf"; -static const char* DAMAGED_HELMET = - "/assets/glTF-Sample-Models/2.0/DamagedHelmet/glTF/DamagedHelmet.gltf"; -static const char* GIRL = - "/home/jeanne/Nextcloud/assets/models/girl/girl-with-ground.gltf"; - -#define DEFAULT_SCENE_FILE GIRL +#include -static const char* CLOUDS1_TEXTURE = "/assets/skybox/clouds1/clouds1_west.bmp"; +#include -/// Load the skyquad texture. -static Texture* load_environment_map(RenderBackend* render_backend) { - return gfx_load_texture( - render_backend, - &(LoadTextureCmd){ - .origin = TextureFromFile, - .type = LoadCubemap, - .colour_space = sRGB, - .filtering = NearestFiltering, - .mipmaps = false, - .data.cubemap.filepaths = { - mstring_make("/assets/skybox/clouds1/clouds1_east.bmp"), - mstring_make("/assets/skybox/clouds1/clouds1_west.bmp"), - mstring_make("/assets/skybox/clouds1/clouds1_up.bmp"), - mstring_make("/assets/skybox/clouds1/clouds1_down.bmp"), - mstring_make("/assets/skybox/clouds1/clouds1_south.bmp"), - mstring_make("/assets/skybox/clouds1/clouds1_north.bmp")} - }); -} +#undef _GNU_SOURCE -/// Load the skyquad and return the environment light node. -static SceneNode* load_skyquad(Game* game) { - assert(game); +// Plugin to load if no plugin is provided. +static const char* DEFAULT_PLUGIN = "texture_view"; - Texture* environment_map = load_environment_map(game->render_backend); - if (!environment_map) { - return 0; +static bool validate_plugin(const Plugin* plugin) { +#define CHECK_FUNCTION(name, signature) \ + if (!plugin_resolve(plugin, signature, name)) { \ + LOGE("Plugin is missing function: " #name); \ + return false; \ } - - return gfx_setup_skyquad( - game->gfx, gfx_get_scene_root(game->scene), environment_map); -} - -/// Load the 3D scene. -static SceneNode* load_scene( - Game* game, const char* scene_filepath, const char* view_mode) { - assert(game); - - game->camera = gfx_make_camera(); - if (!game->camera) { - return 0; - } - Camera* camera = gfx_get_camera_camera(game->camera); - spatial3_set_position(&camera->spatial, vec3_make(0, 0, 2)); - - SceneNode* sky_light_node = load_skyquad(game); - if (!sky_light_node) { - return 0; - } - - SceneNode* scene_node = gfx_load_scene( - game->gfx, sky_light_node, - &(LoadSceneCmd){.origin = SceneFromFile, .filepath = scene_filepath}); - if (!scene_node) { - return 0; - } - - gfx_log_node_hierarchy(gfx_get_scene_root(game->scene)); - - return scene_node; + CHECK_FUNCTION("init", plugin_init); + CHECK_FUNCTION("boot", plugin_boot); + CHECK_FUNCTION("update", plugin_update); + CHECK_FUNCTION("render", plugin_render); + return true; } -/// Load a scene for debugging textures. -static bool load_texture_debugger_scene(Game* game) { +bool game_new(Game* game, int argc, const char** argv) { assert(game); - Texture* texture = gfx_load_texture( - game->render_backend, - &(LoadTextureCmd){ - .origin = TextureFromFile, - .type = LoadTexture, - .filtering = LinearFiltering, - .mipmaps = false, - .data.texture.filepath = mstring_make(CLOUDS1_TEXTURE)}); + // Syntax: game [plugin] + // + // Here we consume the [plugin] arg so that plugins receive the remainder + // args starting from 0. + game->argc = argc - 1; + game->argv = argv + 1; - game->camera = gfx_make_camera(); - if (!game->camera) { - return false; + char exe_path_buf[NAME_MAX] = {0}; + if (readlink("/proc/self/exe", exe_path_buf, sizeof(exe_path_buf)) == -1) { + LOGE("readlink(/proc/self/exe) failed"); + goto cleanup; } - Camera* camera = gfx_get_camera_camera(game->camera); - spatial3_set_position(&camera->spatial, vec3_make(0, 0, 1)); - ShaderProgram* shader = gfx_make_view_texture_shader(game->render_backend); - if (!shader) { - return false; - } + // Replace the last / with a null terminator to remove the exe file from the + // path. This gets the file's parent directory. + *strrchr(exe_path_buf, '/') = 0; - Geometry* geometry = gfx_make_quad_11(game->render_backend); - if (!geometry) { - return false; - } + const mstring exe_dir = mstring_make(exe_path_buf); + const mstring plugins_path = mstring_concat_cstr(exe_dir, "/src/plugins"); - MaterialDesc material_desc = (MaterialDesc){0}; - material_desc.uniforms[0] = (ShaderUniform){ - .type = UniformTexture, - .value.texture = texture, - .name = sstring_make("Texture")}; - material_desc.num_uniforms = 1; - Material* material = gfx_make_material(&material_desc); - if (!material) { - return false; + if (!(game->plugin_engine = new_plugin_engine( + &(PluginEngineDesc){.plugins_dir = mstring_cstr(&plugins_path)}))) { + goto cleanup; } - MeshDesc mesh_desc = (MeshDesc){0}; - mesh_desc.geometry = geometry; - mesh_desc.material = material; - mesh_desc.shader = shader; - Mesh* mesh = gfx_make_mesh(&mesh_desc); - if (!mesh) { - return false; + const char* plugin = argc > 1 ? argv[1] : DEFAULT_PLUGIN; + if (!(game->plugin = load_plugin(game->plugin_engine, plugin))) { + goto cleanup; } - - SceneObject* object = gfx_make_object(); - if (!object) { - return false; + if (!validate_plugin(game->plugin)) { + goto cleanup; } - gfx_add_object_mesh(object, mesh); - SceneNode* node = gfx_make_object_node(object); - SceneNode* root = gfx_get_scene_root(game->scene); - gfx_set_node_parent(node, root); - - return true; -} - -/// Render the bounding boxes of all scene objects. -static void render_bounding_boxes(ImmRenderer* imm, const SceneNode* node) { - if (gfx_get_node_type(node) == ObjectNode) { - // TODO: Look at the scene log. The JointNodes are detached from the - // ObjectNodes. This is why the boxes are not being transformed as expected - // here. Anima needs to animate boxes? Use OOBB in addition to AABB? - const mat4 model = gfx_get_node_global_transform(node); - const SceneObject* obj = gfx_get_node_object(node); - const aabb3 box = gfx_calc_object_aabb(obj); - gfx_imm_set_model_matrix(imm, &model); - gfx_imm_draw_aabb(imm, box); + if (!(game->gfx = gfx_init())) { + goto cleanup; } - - // Render children's boxes. - for (NodeIter it = gfx_get_node_child(node); it; - it = gfx_get_next_child(it)) { - render_bounding_boxes(imm, gfx_get_iter_node(it)); + if (!(game->scene = gfx_make_scene())) { + goto cleanup; } -} - -bool game_new(Game* game, int argc, const char** argv) { - // TODO: getopt() to implement proper argument parsing. - const char* scene_filepath = argc > 1 ? argv[1] : DEFAULT_SCENE_FILE; - const char* view_mode = argc > 2 ? argv[2] : ""; - - game->gfx = gfx_init(); - if (!game->gfx) { + if (!(game->camera = gfx_make_camera())) { goto cleanup; } - game->render_backend = gfx_get_render_backend(game->gfx); - game->renderer = gfx_get_renderer(game->gfx); - - game->scene = gfx_make_scene(); - if (!game->scene) { + void* plugin_state = plugin_call(game->plugin, plugin_init, "init", game); + if (!plugin_state) { goto cleanup; } + set_plugin_state(game->plugin, plugin_state); - game->root_node = load_scene(game, scene_filepath, view_mode); - if (!game->root_node) { + bool boot_success = + plugin_call(game->plugin, plugin_boot, "boot", plugin_state, game); + if (!boot_success) { goto cleanup; } - /*if (!load_texture_debugger_scene(game)) { - goto cleanup; - }*/ - - Anima* anima = gfx_get_node_anima(game->root_node); - - gfx_play_animation( - anima, &(AnimationPlaySettings){.name = "Walk", .loop = true}); return true; cleanup: LOGE("Gfx error: %s", gfx_get_error()); + game_end(game); + return false; +} + +void game_end(Game* game) { + assert(game); if (game->gfx) { gfx_destroy(&game->gfx); } - return false; + if (game->plugin) { + delete_plugin(&game->plugin); + } + if (game->plugin_engine) { + delete_plugin_engine(&game->plugin_engine); + } } -void game_end(Game* game) { gfx_destroy(&game->gfx); } - void game_update(Game* game, double t, double dt) { - gfx_animate_scene(game->scene, (R)t); + plugin_engine_update(game->plugin_engine); + if (plugin_reloaded(game->plugin)) { + 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); + } - const vec3 orbit_point = vec3_make(0, 2, 0); - Camera* camera = gfx_get_camera_camera(game->camera); - spatial3_orbit( - &camera->spatial, orbit_point, - /*radius=*/2.5, - /*azimuth=*/t * 0.5, /*zenith=*/0); - spatial3_lookat(&camera->spatial, orbit_point); + void* plugin_state = get_plugin_state(game->plugin); + assert(plugin_state); + plugin_call(game->plugin, plugin_update, "update", plugin_state, game, t, dt); } void game_render(const Game* game) { + Renderer* renderer = gfx_get_renderer(game->gfx); + gfx_render_scene( - game->renderer, + renderer, &(RenderSceneParams){ .mode = RenderDefault, .scene = game->scene, .camera = game->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_colour(imm, vec4_make(0.2, 0.2, 1.0, 0.3)); - render_bounding_boxes(imm, gfx_get_scene_root(game->scene)); - gfx_imm_end(imm); + void* plugin_state = get_plugin_state(game->plugin); + assert(plugin_state); + plugin_call(game->plugin, plugin_render, "render", plugin_state, game); } void game_set_viewport(Game* game, int width, int height) { - gfx_set_viewport(game->render_backend, width, 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; diff --git a/gltfview/src/game.h b/gltfview/src/game.h index 4aeb5ea..2a7b7ef 100644 --- a/gltfview/src/game.h +++ b/gltfview/src/game.h @@ -9,17 +9,21 @@ #include +typedef struct Plugin Plugin; +typedef struct PluginEngine PluginEngine; + /// The delta time the game should be updated with. static const double game_dt = 1.0 / 60.0; /// Game state. typedef struct { - Gfx* gfx; - RenderBackend* render_backend; - Renderer* renderer; - Scene* scene; - SceneCamera* camera; - SceneNode* root_node; + int argc; + const char** argv; + PluginEngine* plugin_engine; + Plugin* plugin; + Gfx* gfx; + Scene* scene; + SceneCamera* camera; } Game; bool game_new(Game*, int argc, const char** argv); diff --git a/gltfview/src/plugins/CMakeLists.txt b/gltfview/src/plugins/CMakeLists.txt new file mode 100644 index 0000000..ecb2a45 --- /dev/null +++ b/gltfview/src/plugins/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.0) + +project(plugins) + +set(LINK_LIBRARIES cstring math gfx) + +add_library(gltf_view SHARED + gltf_view.c) + +add_library(texture_view SHARED + texture_view.c) + +target_link_libraries(gltf_view PUBLIC + ${LINK_LIBRARIES}) + +target_link_libraries(texture_view PUBLIC + ${LINK_LIBRARIES}) diff --git a/gltfview/src/plugins/gltf_view.c b/gltfview/src/plugins/gltf_view.c new file mode 100644 index 0000000..511c2e8 --- /dev/null +++ b/gltfview/src/plugins/gltf_view.c @@ -0,0 +1,167 @@ +#include "gltf_view.h" + +#include +#include +#include +#include +#include +#include + +#include + +// Paths to various scene files. +/*static const char* BOX = "/assets/models/box.gltf"; +static const char* SUZANNE = "/assets/models/suzanne.gltf"; +static const char* SPONZA = + "/assets/glTF-Sample-Models/2.0/Sponza/glTF/Sponza.gltf"; +static const char* FLIGHT_HELMET = + "/assets/glTF-Sample-Models/2.0/FlightHelmet/glTF/FlightHelmet.gltf"; +static const char* DAMAGED_HELMET = + "/assets/glTF-Sample-Models/2.0/DamagedHelmet/glTF/DamagedHelmet.gltf";*/ +static const char* GIRL = + "/home/jeanne/Nextcloud/assets/models/girl/girl-with-ground.gltf"; + +#define DEFAULT_SCENE_FILE GIRL + +/// Load the skyquad texture. +static Texture* load_environment_map(RenderBackend* render_backend) { + return gfx_load_texture( + render_backend, + &(LoadTextureCmd){ + .origin = TextureFromFile, + .type = LoadCubemap, + .colour_space = sRGB, + .filtering = NearestFiltering, + .mipmaps = false, + .data.cubemap.filepaths = { + mstring_make("/assets/skybox/clouds1/clouds1_east.bmp"), + mstring_make("/assets/skybox/clouds1/clouds1_west.bmp"), + mstring_make("/assets/skybox/clouds1/clouds1_up.bmp"), + mstring_make("/assets/skybox/clouds1/clouds1_down.bmp"), + mstring_make("/assets/skybox/clouds1/clouds1_south.bmp"), + mstring_make("/assets/skybox/clouds1/clouds1_north.bmp")} + }); +} + +/// Load the skyquad and return the environment light node. +static SceneNode* load_skyquad(RenderBackend* render_backend, SceneNode* root) { + assert(render_backend); + assert(root); + + Texture* environment_map = load_environment_map(render_backend); + if (!environment_map) { + return 0; + } + + return gfx_setup_skyquad(render_backend, root, environment_map); +} + +/// Load the 3D scene. +static SceneNode* load_scene(Game* game, const char* scene_filepath) { + assert(game); + assert(game->gfx); + assert(game->scene); + + SceneNode* root = gfx_get_scene_root(game->scene); + RenderBackend* render_backend = gfx_get_render_backend(game->gfx); + + Camera* camera = gfx_get_camera_camera(game->camera); + spatial3_set_position(&camera->spatial, vec3_make(0, 0, 2)); + + SceneNode* sky_light_node = load_skyquad(render_backend, root); + if (!sky_light_node) { + return 0; + } + + SceneNode* scene_node = gfx_load_scene( + game->gfx, sky_light_node, + &(LoadSceneCmd){.origin = SceneFromFile, .filepath = scene_filepath}); + if (!scene_node) { + return 0; + } + + gfx_log_node_hierarchy(root); + + return scene_node; +} + +State* init(Game* game) { + assert(game); + + State* state = calloc(1, sizeof(State)); + return state; +} + +bool boot(State* state, Game* game) { + assert(state); + assert(game); + + const int argc = game->argc; + const char** argv = game->argv; + + // Usage: + const char* scene_filepath = argc > 1 ? argv[1] : DEFAULT_SCENE_FILE; + + SceneNode* node = load_scene(game, scene_filepath); + if (!node) { + return false; + } + Anima* anima = gfx_get_node_anima(node); + gfx_play_animation( + anima, &(AnimationPlaySettings){.name = "Walk", .loop = true}); + + return true; +} + +void update(State* state, Game* game, double t, double dt) { + assert(state); + assert(game); + assert(game->scene); + assert(game->camera); + + gfx_animate_scene(game->scene, (R)t); + + const vec3 orbit_point = vec3_make(0, 2, 0); + Camera* camera = gfx_get_camera_camera(game->camera); + spatial3_orbit( + &camera->spatial, orbit_point, + /*radius=*/2.5, + /*azimuth=*/t * 0.5, /*zenith=*/0); + spatial3_lookat(&camera->spatial, orbit_point); +} + +/// Render the bounding boxes of all scene objects. +static void render_bounding_boxes(ImmRenderer* imm, const SceneNode* node) { + if (gfx_get_node_type(node) == ObjectNode) { + // TODO: Look at the scene log. The JointNodes are detached from the + // ObjectNodes. This is why the boxes are not being transformed as expected + // here. Anima needs to animate boxes? Use OOBB in addition to AABB? + const mat4 model = gfx_get_node_global_transform(node); + const SceneObject* obj = gfx_get_node_object(node); + const aabb3 box = gfx_calc_object_aabb(obj); + gfx_imm_set_model_matrix(imm, &model); + gfx_imm_draw_aabb(imm, box); + } + + // Render children's boxes. + for (NodeIter it = gfx_get_node_child(node); it; + it = gfx_get_next_child(it)) { + render_bounding_boxes(imm, gfx_get_iter_node(it)); + } +} + +void render(State* state, const Game* game) { + assert(state); + assert(game); + assert(game->gfx); + assert(game->scene); + assert(game->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_colour(imm, vec4_make(0.2, 0.2, 1.0, 0.3)); + render_bounding_boxes(imm, gfx_get_scene_root(game->scene)); + gfx_imm_end(imm); +} diff --git a/gltfview/src/plugins/gltf_view.h b/gltfview/src/plugins/gltf_view.h new file mode 100644 index 0000000..670d88d --- /dev/null +++ b/gltfview/src/plugins/gltf_view.h @@ -0,0 +1,9 @@ +#pragma once + +#include "plugin.h" + +#include + +typedef struct State { + int unused; +} State; diff --git a/gltfview/src/plugins/plugin.h b/gltfview/src/plugins/plugin.h new file mode 100644 index 0000000..0e0e12c --- /dev/null +++ b/gltfview/src/plugins/plugin.h @@ -0,0 +1,49 @@ +/* + * 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 + +#include "../game.h" + +#include +#include + +#include + +typedef struct State State; + +/// Initialize the plugin's state. +State* init(Game*); + +/// 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*); + +/// Update the plugin's and the game's state. +void update(State*, Game*, double t, double dt); + +/// Optional plugin rendering hook. +void render(State*, const Game*); + +// 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*); diff --git a/gltfview/src/plugins/texture_view.c b/gltfview/src/plugins/texture_view.c new file mode 100644 index 0000000..f2c650f --- /dev/null +++ b/gltfview/src/plugins/texture_view.c @@ -0,0 +1,94 @@ +#include "texture_view.h" + +#include +#include +#include +#include + +#include + +#include +#include + +// Default texture to load if no texture is provided. +static const char* CLOUDS1_TEXTURE = "/assets/skybox/clouds1/clouds1_west.bmp"; + +State* init(Game* game) { + assert(game); + + State* state = calloc(1, sizeof(State)); + return state; +} + +bool boot(State* state, Game* game) { + assert(state); + assert(game); + + // Usage: [texture file] + const char* texture_file = game->argc > 1 ? game->argv[1] : CLOUDS1_TEXTURE; + + RenderBackend* render_backend = gfx_get_render_backend(game->gfx); + + Texture* texture = gfx_load_texture( + render_backend, &(LoadTextureCmd){ + .origin = TextureFromFile, + .type = LoadTexture, + .filtering = LinearFiltering, + .mipmaps = false, + .data.texture.filepath = mstring_make(texture_file)}); + + Camera* camera = gfx_get_camera_camera(game->camera); + spatial3_set_position(&camera->spatial, vec3_make(0, 0, 1)); + + ShaderProgram* shader = gfx_make_view_texture_shader(render_backend); + if (!shader) { + return false; + } + + Geometry* geometry = gfx_make_quad_11(render_backend); + if (!geometry) { + return false; + } + + MaterialDesc material_desc = (MaterialDesc){0}; + material_desc.uniforms[0] = (ShaderUniform){ + .type = UniformTexture, + .value.texture = texture, + .name = sstring_make("Texture")}; + material_desc.num_uniforms = 1; + Material* material = gfx_make_material(&material_desc); + if (!material) { + return false; + } + + MeshDesc mesh_desc = (MeshDesc){0}; + mesh_desc.geometry = geometry; + mesh_desc.material = material; + mesh_desc.shader = shader; + Mesh* mesh = gfx_make_mesh(&mesh_desc); + if (!mesh) { + return false; + } + + SceneObject* object = gfx_make_object(); + if (!object) { + return false; + } + gfx_add_object_mesh(object, mesh); + + SceneNode* node = gfx_make_object_node(object); + SceneNode* root = gfx_get_scene_root(game->scene); + gfx_set_node_parent(node, root); + + return true; +} + +void update(State* state, Game* game, double t, double dt) { + assert(state); + assert(game); +} + +void render(State* state, const Game* game) { + assert(state); + assert(game); +} diff --git a/gltfview/src/plugins/texture_view.h b/gltfview/src/plugins/texture_view.h new file mode 100644 index 0000000..670d88d --- /dev/null +++ b/gltfview/src/plugins/texture_view.h @@ -0,0 +1,9 @@ +#pragma once + +#include "plugin.h" + +#include + +typedef struct State { + int unused; +} State; -- cgit v1.2.3