diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f3ac262..745e4492 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.8) +cmake_minimum_required(VERSION 3.10) project(psemek) cmake_policy(SET CMP0077 NEW) @@ -45,8 +45,6 @@ option(PSEMEK_LEGACY_UI "Use legacy UI library" OFF) message(STATUS "Using backend ${PSEMEK_BACKEND}") -option(PSEMEK_BT_LOG "Add logging for behavior tree actions" OFF) - if(PSEMEK_BACKEND STREQUAL "ANDROID") set(PSEMEK_GL_API gles32) set(PSEMEK_GL_LIBRARIES GLESv3 EGL) @@ -78,7 +76,7 @@ if(NOT DEFINED PSEMEK_GRAPHICS_API) endif() if(PSEMEK_GRAPHICS_API STREQUAL WEBGPU) - find_package(wgpu-native REQUIRED) + find_package(wgpu-native 27.0.2.0 REQUIRED) endif() message(STATUS "Using graphics API ${PSEMEK_GRAPHICS_API}") diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..075d3886 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,29 @@ +MIT NON-AI NON-NFT License + +Copyright (c) 2025, Nikita Lisitsa + +Permission is hereby granted, free of charge, to any person obtaining a copy of the software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions. + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +In addition, the following restrictions apply: + +1. The Software, any modifications made to it, any artifacts or media generated by it, and any parts thereof, including but not limited to images, +video, and code snippets, may not be used for the purpose of training or improving machine learning algorithms, or be included in any dataset used +for training or improving machine learning algorithms, including but not limited to artificial intelligence, natural language processing, large +language models, or data mining. This condition applies to any derivatives, modifications, or updates based on the Software code. Any usage of the +Software in an AI-training dataset is considered a breach of this License. + +2. The Software, any modifications made to it, any artifacts or media generated by it, and any parts thereof, including but not limited to images, +video, and code snippets, may not be owned via a blockchain-backed ownership token, or as a means of generating blockchain-backed ownership tokens, +including but not limited to non-fungible tokens. Any usage of the Software to generate non-fungible tokens is considered a breach of this License. + +3. Any person or organization found to be in violation of these restrictions will be subject to legal action and may be held liable for any damages +resulting from such use. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/cmake/modules/Findwgpu-native.cmake b/cmake/modules/Findwgpu-native.cmake index 517d3c06..c146786a 100644 --- a/cmake/modules/Findwgpu-native.cmake +++ b/cmake/modules/Findwgpu-native.cmake @@ -3,16 +3,27 @@ if(wgpu-native_FOUND) endif() # Don't search for include files - these are bundled with psemek-wgpu lib -find_library(wgpu-native_LIBRARIES NAMES libwgpu_native.a wgpu_native.dll wgpu_native PATHS "${WGPU_NATIVE_ROOT}") +find_library(wgpu-native_LIBRARY NAMES libwgpu_native.a wgpu_native.dll wgpu_native PATHS "${WGPU_NATIVE_ROOT}" "${WGPU_NATIVE_ROOT}/lib") +find_file(wgpu-native_VERSION_FILE wgpu-native-git-tag PATHS "${WGPU_NATIVE_ROOT}/wgpu-native-meta") + +if(EXISTS "${wgpu-native_VERSION_FILE}") + file(READ "${wgpu-native_VERSION_FILE}" wgpu-native_VERSION_NOT_STRIPPED) + string(STRIP "${wgpu-native_VERSION_NOT_STRIPPED}" wgpu-native_VERSION_STRIPPED) + string(SUBSTRING "${wgpu-native_VERSION_STRIPPED}" 1 -1 wgpu-native_VERSION) +endif() include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(wgpu-native DEFAULT_MSG wgpu-native_LIBRARIES) +find_package_handle_standard_args(wgpu-native + REQUIRED_VARS wgpu-native_LIBRARY wgpu-native_VERSION_FILE + VERSION_VAR wgpu-native_VERSION +) if(wgpu-native_FOUND AND NOT TARGET wgpu-native) + set(wgpu-native_LIBRARIES ${wgpu-native_LIBRARY}) add_library(wgpu-native STATIC IMPORTED) set_target_properties(wgpu-native PROPERTIES IMPORTED_LOCATION "${wgpu-native_LIBRARIES}" ) endif() -mark_as_advanced(wgpu-native_LIBRARIES) +mark_as_advanced(wgpu-native_LIBRARY wgpu-native_VERSION_FILE) diff --git a/examples/biomes.png b/examples/biomes.png new file mode 100644 index 00000000..d6a16c38 Binary files /dev/null and b/examples/biomes.png differ diff --git a/examples/cloud.cpp b/examples/cloud.cpp index 274d5b3c..adec7a4f 100644 --- a/examples/cloud.cpp +++ b/examples/cloud.cpp @@ -223,7 +223,7 @@ struct cloud_app random::generator rng(random::device{}); random::uniform_sphere_vector_distribution d; - std::vector, 3>> grad(4); + std::vector, 3>> grad(4); std::vector weights(grad.size()); float weight_sum = 0.f; @@ -247,7 +247,7 @@ struct cloud_app // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=89062 math::gradient g(std::make_pair(0.2f, 0.f), math::easing_type::quadratic_out, std::pair{0.3f, max_density}); - util::array cloud_data({cloud_size[0], cloud_size[1], cloud_size[2]}); + util::ndarray cloud_data({cloud_size[0], cloud_size[1], cloud_size[2]}); for (std::size_t z = 0; z < cloud_data.depth(); ++z) { @@ -328,7 +328,7 @@ struct cloud_app return math::lerp(v0, v1, t[2]) / 255.f * max_density; }; - util::array, 3> cloud_shadow_f(cloud_data.dims()); + util::ndarray, 3> cloud_shadow_f(cloud_data.dims()); dirs.resize(32); @@ -448,7 +448,7 @@ struct cloud_app } } - util::array, 3> cloud_shadow(cloud_data.dims()); + util::ndarray, 3> cloud_shadow(cloud_data.dims()); for (auto const & idx : cloud_shadow.indices()) { diff --git a/examples/fibonacci_music_box.cpp b/examples/fibonacci_music_box.cpp index 176bff5a..bb9f638e 100644 --- a/examples/fibonacci_music_box.cpp +++ b/examples/fibonacci_music_box.cpp @@ -22,7 +22,7 @@ #include #include #include -#include +#include #include @@ -32,7 +32,7 @@ using namespace psemek; std::vector>> fibonacci_cycles(int n) { - util::array cycle({n, n}, -1); + util::ndarray cycle({n, n}, -1); std::vector>> result; diff --git a/examples/fire.cpp b/examples/fire.cpp index db1b350b..62a2da16 100644 --- a/examples/fire.cpp +++ b/examples/fire.cpp @@ -172,7 +172,7 @@ candle_renderer::candle_renderer() random::generator rng; random::uniform_sphere_vector_distribution d; - util::array, 2> grad({16, 16}); + util::ndarray, 2> grad({16, 16}); for (auto & v : grad) v = d(rng); pcg::perlin perlinx(grad, pcg::seamless); diff --git a/examples/heightmap_seed_0.png b/examples/heightmap_seed_0.png new file mode 100644 index 00000000..0b246c81 Binary files /dev/null and b/examples/heightmap_seed_0.png differ diff --git a/examples/heightmap_seed_1.png b/examples/heightmap_seed_1.png new file mode 100644 index 00000000..5275a52b Binary files /dev/null and b/examples/heightmap_seed_1.png differ diff --git a/examples/heightmap_seed_2.png b/examples/heightmap_seed_2.png new file mode 100644 index 00000000..34a0206d Binary files /dev/null and b/examples/heightmap_seed_2.png differ diff --git a/examples/heightmap_seed_3.png b/examples/heightmap_seed_3.png new file mode 100644 index 00000000..7f591c9e Binary files /dev/null and b/examples/heightmap_seed_3.png differ diff --git a/examples/particle_life_2d.cpp b/examples/particle_life_2d.cpp new file mode 100644 index 00000000..85f74e4a --- /dev/null +++ b/examples/particle_life_2d.cpp @@ -0,0 +1,441 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace psemek; + +struct particle +{ + math::point position; + math::vector velocity; + float mass; + float radius; + int type; +}; + +struct particle_life_2d_app + : app::application_base +{ + particle_life_2d_app(options const &, context const &) + { + float S = 5.f; + float W = 160.f * S; + float H = 90.f * S; + area_ = {{{-W, W}, {-H, H}}}; + + types_ = 10; + + auto seed = random::device{}(); + + // Explosions + // types_ = 6; + // seed = 3940378904; + + // Cells + // types_ = 8; + // seed = 3071915692; + + // Cells v2 + // types_ = 6; + // seed = 3337663839; + + // Cool cells! + types_ = 10; + seed = 388834085; + + // One cell, use central gravity + // types_ = 10; + // seed = 175098945; + + // Mitochondria cell + // types_ = 10; + // seed = 1763331406; + + // Just fun + // types_ = 10; + // seed = 1676103531; + + // Another cell + // types_ = 10; + // seed = 2396049785; + + log::info() << "Seed: " << seed; + + random::generator rng{seed, 0}; + + random::uniform_distribution dcolor(63, 255); + random::uniform_distribution dtype(0, types_ - 1); + random::uniform_box_point_distribution darea(area_); + + for (int i = 0; i < types_; ++i) + colors_.push_back({dcolor(rng), dcolor(rng), dcolor(rng), 255}); + + for (int i = 0; i < 4 * 1024; ++i) + { + particles_.push_back({ + .position = darea(rng), + .velocity = {0.f, 0.f}, + .mass = 1.f, + .radius = 1.f, + .type = dtype(rng), + // .type = random::uniform_from(rng, {4, 6}), + }); + } + + auto dforce = [](auto & rng) + { + return random::uniform(rng, 20.f, 100.f) * (random::uniform(rng) ? 1.f : -1.f); + }; + + random::uniform_distribution ddistance(3.f, 20.f); + + force_constants_.resize({types_, types_}); + force_distance_.resize({types_, types_}); + collision_distance_.resize({types_, types_}); + + for (int i = 0; i < types_; ++i) + for (int j = 0; j < types_; ++j) + force_constants_(i, j) = dforce(rng); + + for (int i = 0; i < types_; ++i) + for (int j = 0; j < types_; ++j) + force_distance_(i, j) = ddistance(rng); + + for (int i = 0; i < types_; ++i) + for (int j = 0; j < types_; ++j) + collision_distance_(i, j) = random::uniform(rng, 0.125f, 0.75f) * force_distance_(i, j); + + // for (int i = 0; i < types_; ++i) + // for (int j = 0; j < types_; ++j) + // force_constants_(i, j) = -100.f * ((i == j) ? 1.f : 2.f); + + max_force_distance_.resize(types_, 2.f); + for (int i = 0; i < types_; ++i) + for (int j = 0; j < types_; ++j) + math::make_max(max_force_distance_[i], force_distance_(i, j)); + } + + void on_event(app::mouse_wheel_event const & event) override + { + app::application_base::on_event(event); + + scale_target_ *= std::pow(1.25f, -event.delta); + } + + void on_event(app::key_event const & event) override + { + app::application_base::on_event(event); + + if (event.down && event.key == app::keycode::SPACE) + paused_ ^= true; + } + + void update() override + { + float const dt = 0.02f; + + scale_ += (scale_target_ - scale_) * (- std::expm1(- 20.f * dt)); + + auto visible_area = area_; + visible_area = math::expand(visible_area, 0.5f * (scale_ - 1.f) * visible_area.dimensions()); + + float window_aspect_ratio = state().size[0] * 1.f / state().size[1]; + float area_aspect_ratio = visible_area[0].length() / visible_area[1].length(); + + if (area_aspect_ratio < window_aspect_ratio) + { + view_area_[1] = visible_area[1]; + view_area_[0] = math::expand(math::interval::singleton(visible_area[0].center()), visible_area[1].length() * window_aspect_ratio * 0.5f); + } + else + { + view_area_[0] = visible_area[0]; + view_area_[1] = math::expand(math::interval::singleton(visible_area[1].center()), visible_area[0].length() / window_aspect_ratio * 0.5f); + } + + if (!paused_) + for (int step = 0; step < 5; ++step) + { + cg::kdtree kdtree; + + for (int i = 0; i < particles_.size(); ++i) + kdtree.insert({particles_[i].position, i}); + + float const collision_strength = 200.f; + + mouseover_particle_ = std::nullopt; + if (state().mouse_button_down.contains(app::mouse_button::left)) + { + math::point m; + m[0] = math::lerp(view_area_[0], state().mouse[0] * 1.f / state().size[0]); + m[1] = math::lerp(view_area_[1], 1.f - state().mouse[1] * 1.f / state().size[1]); + mouseover_particle_ = kdtree.closest(m).data; + } + + #pragma omp parallel for + for (int i = 0; i < particles_.size(); ++i) + { + // ======== kd-tree based algorithm ======== + + auto & pi = particles_[i]; + auto f = math::vector{0.f, 0.f}; + + kdtree.closer_than_map(pi.position, max_force_distance_[pi.type], [&](auto const & value){ + auto j = value.data; + + if (i == j) + return; + + auto const & pj = particles_[j]; + + auto r = pj.position - pi.position; + float l = math::length(r); + auto n = r / l; + + // ==== 1/R forces ==== + // float lg = std::max(0.5f, l); + // auto g = n / (lg * lg); + + // if (l < force_distance_(pi.type, pj.type)) + // f += force_constants_(pi.type, pj.type) * g; + + // float collision_distance = pi.radius + pj.radius; + + // if (l < collision_distance) + // { + // float d = std::max(0.25f, l / collision_distance); + + // f -= collision_strength * n * (1.f / (d * d) - 1.f); + // } + + // ==== Linear forces ==== + if (l < force_distance_(pi.type, pj.type)) + f += force_constants_(pi.type, pj.type) * n * (1.f - l / force_distance_(pi.type, pj.type)); + + if (l < collision_distance_(pi.type, pj.type)) + f -= 10.f * std::abs(force_constants_(pi.type, pj.type)) * n * (1.f - l / collision_distance_(pi.type, pj.type)); + + (void)collision_strength; + }); + + pi.velocity += f * dt / pi.mass; + + + + + + + // ======== Naive algorithm v2 ======== + + // auto & pi = particles_[i]; + // auto f = math::vector{0.f, 0.f}; + + // for (int j = 0; j < particles_.size(); ++j) + // { + // if (i == j) + // continue; + + // auto const & pj = particles_[j]; + + // auto r = pj.position - pi.position; + // float l = math::length(r); + // auto n = r / l; + // auto g = n / (l * l); + + // if (l < force_distance_(pi.type, pj.type)) + // f += force_constants_(pi.type, pj.type) * g; + + // if (l < pi.radius + pj.radius) + // { + // float d = l / (pi.radius + pj.radius); + + // f -= collision_strength * n * (1.f / (d * d) - 1.f); + // } + // } + + // pi.velocity += f * dt / pi.mass; + + + + + + // ======== Naive algorithm ======== + + // for (int j = i + 1; j < particles_.size(); ++j) + // { + // auto & pi = particles_[i]; + // auto & pj = particles_[j]; + + // auto fi = math::vector{0.f, 0.f}; + // auto fj = math::vector{0.f, 0.f}; + + // auto r = pj.position - pi.position; + // float l = math::length(r); + // auto n = r / l; + // auto g = n / (l * l); + + // if (l < force_distance_(pi.type, pj.type)) + // fi += force_constants_(pi.type, pj.type) * g; + + // if (l < force_distance_(pj.type, pi.type)) + // fj -= force_constants_(pj.type, pi.type) * g; + + // if (l < pi.radius + pj.radius) + // { + // float d = l / (pi.radius + pj.radius); + + // auto f = collision_strength * n * (1.f / (d * d) - 1.f); + // fi -= f; + // fj += f; + // } + + // pi.velocity += fi * dt / pi.mass; + // pj.velocity += fj * dt / pj.mass; + // } + } + + float const friction = 10.f; + float const friction_factor = std::exp(- friction * dt); + float const wall_distance = 0.f; + float const wall_force = 10.f; + float const gravity = 0.f; + float const center_gravity = 0.f; + float const line_gravity = 0.f; + float const circle_gravity = 0.f; + float const circle_radius = 100.f; + bool const circle_wall = false; + float const circle_wall_radius = 100.f; + + for (auto & p : particles_) + { + auto r = p.position - p.position.zero(); + + p.velocity[1] -= gravity * dt; + p.velocity[1] -= line_gravity * p.position[1] * dt; + p.velocity -= center_gravity * r * dt; + p.velocity += circle_gravity * r / math::length(r) * (circle_radius - math::length(r)) * dt; + p.velocity *= friction_factor; + p.position += p.velocity * dt; + + for (int d : {0, 1}) + { + // if (p.position[d] < area_[d].min + p.radius) + // { + // p.position[d] = area_[d].min + p.radius; + // p.velocity[d] *= -1.f; + // } + + // if (p.position[d] > area_[d].max - p.radius) + // { + // p.position[d] = area_[d].max - p.radius; + // p.velocity[d] *= -1.f; + // } + + if (circle_wall) + { + auto r = p.position - p.position.zero(); + auto l = math::length(r); + if (l > circle_wall_radius) + { + p.velocity -= wall_force * r / l * (l - circle_wall_radius) * dt; + } + } + else + { + if (p.position[d] < area_[d].min + wall_distance) + { + p.velocity[d] += wall_force * (area_[d].min + wall_distance - p.position[d]) * dt; + } + + if (p.position[d] > area_[d].max - wall_distance) + { + p.velocity[d] -= wall_force * (p.position[d] - (area_[d].max - wall_distance)) * dt; + } + } + } + } + } + } + + void present() override + { + gl::Viewport(0, 0, state().size[0], state().size[1]); + + gl::ClearColor(0.02f, 0.02f, 0.02f, 1.f); + gl::Clear(gl::COLOR_BUFFER_BIT); + + for (auto const & p : particles_) + { + auto c0 = colors_[p.type]; + c0[3] = 15; + + auto c1 = colors_[p.type]; + c1[3] = 0; + + painter_.circle(p.position, p.radius * 8.f, c0, c1); + } + + for (auto const & p : particles_) + painter_.circle(p.position, p.radius, colors_[p.type]); + + painter_.render(math::orthographic_camera{view_area_}.transform()); + + if (mouseover_particle_) + { + int type = particles_[*mouseover_particle_].type; + painter_.text({state().size[0] / 2.f, state().size[1] - 20.f}, std::format("Species #{}", type), { + .scale = {2.f, 2.f}, + .y = gfx::painter::y_align::bottom, + .c = colors_[type], + }); + } + + painter_.render(math::window_camera{state().size[0], state().size[1]}.transform()); + } + +private: + gfx::painter painter_; + + util::clock, std::chrono::high_resolution_clock> clock_; + + math::box area_; + math::box view_area_; + float scale_ = 1.f; + float scale_target_ = 1.f; + + int types_; + std::vector colors_; + util::ndarray force_constants_; + util::ndarray force_distance_; + util::ndarray collision_distance_; + std::vector max_force_distance_; + + std::vector particles_; + + std::optional mouseover_particle_; + + bool paused_ = true; +}; + +namespace psemek::app +{ + + std::unique_ptr make_application_factory() + { + return default_application_factory({.name = "Particle Life 2D"}); + } + +} diff --git a/examples/physics.cpp b/examples/physics.cpp index 6a80550d..918e90e0 100644 --- a/examples/physics.cpp +++ b/examples/physics.cpp @@ -507,7 +507,7 @@ void physics_demo_app::update() std::vector, 8>> ball_cells(model.points.size()); - util::array, 2> cells({std::ceil(view_region[0].length() / cell_size), std::ceil(view_region[1].length() / cell_size)}); + util::ndarray, 2> cells({std::ceil(view_region[0].length() / cell_size), std::ceil(view_region[1].length() / cell_size)}); float dx = view_region[0].length() / cells.width(); float dy = view_region[1].length() / cells.height(); diff --git a/examples/soft_creatures_2d.cpp b/examples/soft_creatures_2d.cpp index bc1664b1..fd4dffa8 100644 --- a/examples/soft_creatures_2d.cpp +++ b/examples/soft_creatures_2d.cpp @@ -751,7 +751,9 @@ private: std::vector evaluated_creatures(population_.size()); +#ifdef _OPENMP #pragma omp parallel for +#endif for (int i = 0; i < population_.size(); ++i) { auto & creature = population_[i]; diff --git a/examples/srtm.cpp b/examples/srtm.cpp index cec65db4..1adc1bc6 100644 --- a/examples/srtm.cpp +++ b/examples/srtm.cpp @@ -648,7 +648,7 @@ srtm_app::srtm_app(options const &, context const & context) selected_mesh.setup>(); { - util::array colors({16}); + util::ndarray colors({16}); auto * c = colors.data(); @@ -674,7 +674,7 @@ srtm_app::srtm_app(options const &, context const & context) } { - util::array colors({5}); + util::ndarray colors({5}); colors(0) = {0, 63, 127}; colors(1) = {0, 0, 127}; diff --git a/examples/water_2d.cpp b/examples/water_2d.cpp index ce22777b..f9081d00 100644 --- a/examples/water_2d.cpp +++ b/examples/water_2d.cpp @@ -2,8 +2,9 @@ #include #include #include +#include #include -#include +#include #include #include #include @@ -31,31 +32,40 @@ struct water_2d_app random::generator rng{random::device{}}; - int const N = 128; + int const N = 256; float time = 0.f; - util::array bed; - util::array water; - util::array flowx; - util::array flowy; - util::array, 2> velocity; - util::array sediment; - util::array new_sediment; + util::ndarray bed; + util::ndarray water; + util::ndarray flowx; + util::ndarray flowy; + util::ndarray, 2> velocity; + util::ndarray sediment; + util::ndarray new_sediment; gfx::painter painter; bool paused = true; bool show_water = true; bool show_velocity = false; + bool show_particles = false; bool erosion_on = false; bool rain_on = false; + + struct particle + { + math::point position; + int lifetime; + }; + + std::vector particles; }; water_2d_app::water_2d_app(options const &, context const & ctx) { (void)ctx; - ctx.vsync(true); + // ctx.vsync(true); simulation_area[0] = {-1.f, 1.f}; simulation_area[1] = {-1.f, 1.f}; @@ -73,7 +83,7 @@ water_2d_app::water_2d_app(options const &, context const & ctx) // water(N / 2, N / 2) = 100.f; random::uniform_sphere_vector_distribution d; - util::array, 2> grads({9, 9}); + util::ndarray, 2> grads({9, 9}); for (auto & v : grads) v = d(rng); @@ -86,8 +96,8 @@ water_2d_app::water_2d_app(options const &, context const & ctx) // if (x == N / 2 && (y != N / 4 && y != 3 * N / 4)) // bed(x, y) = 100.f; - float tx = (x + 0.5f) / N; - float ty = (y + 0.5f) / N; + [[maybe_unused]] float tx = (x + 0.5f) / N; + [[maybe_unused]] float ty = (y + 0.5f) / N; float n = 0.f; @@ -106,7 +116,9 @@ water_2d_app::water_2d_app(options const &, context const & ctx) // n = 4.f * std::abs(2.f * noise(tx, ty) - 1.f) * std::pow(4.f * tx * (1.f - tx), 2.f); // River - n = std::min(4.f, math::lerp(10.f, 20.f, noise(tx, ty)) * std::abs(ty - math::lerp({0.4f, 0.6f}, noise(tx, 0.f)))); + // n = std::min(4.f, math::lerp(10.f, 20.f, noise(tx, ty)) * std::abs(ty - math::lerp({0.4f, 0.6f}, noise(tx, 0.f)))); + + // n = 0.f; // Canyon // n = 10.f * math::clamp(10.f * math::lerp(-1.f, 1.f, noise(tx, ty)), {0.f, 1.f}); @@ -128,13 +140,17 @@ water_2d_app::water_2d_app(options const &, context const & ctx) // if (x == 0) // water(x, y) = 100.f - bed(x, y); + + water(x, y) = std::max(0.f, 1.f - bed(x, y)) * (0.5f + 0.5f * noise(tx, ty)); + + flowx(x, y) = (2.f * ty - 1.f) * water(x, y) * 10.f / N; } } // water(N / 2, N / 2) = 10000.f; } -float const dt = 0.002f; +float const dt = 0.001f; void water_2d_app::update() { @@ -144,11 +160,15 @@ void water_2d_app::update() float const dx = simulation_area[0].length() / N; float const dy = simulation_area[1].length() / N; float const g = 10.f; - float const friction = std::pow(0.5f, dt); + float const friction = std::pow(1.f, dt); float const viscosity = 0.f; float const sediment_capacity = 0.1f; float const erosion = 1.f; float const deposition = 10.f; + float const coriolis = 0.01f; + int const max_particles = 16 * 1024; + int const particles_per_frame = 16; + int const max_particle_lifetime = max_particles / particles_per_frame; // Init boundary flows for (int x = 0; x < N; ++x) @@ -160,13 +180,16 @@ void water_2d_app::update() // if (x < N / 4) flowy(x, N) = -3.f / N; // flowy(x, 0) = 5.f * std::sin(10.f * time) / N; + + flowy(x, 0) = 0.f; + flowy(x, N) = 0.f; } for (int y = 0; y < N; ++y) { // flowx(0, y) = 5.f * std::sin(10.f * time) / N; - // flowx(0, y) = 1.f / N; + // flowx(0, y) = 10.f / N; // flowx(N, y) = 1.f / N; // flowx(0, y) = 10.f * ((y + 0.5f) / N) / N; @@ -175,12 +198,12 @@ void water_2d_app::update() // if (y < N / 4) flowx(0, y) = 3.f / N; // if (y >= 3 * N / 4) flowx(N, y) = -3.f / N; - if (bed(0, y) < 0.75f) - flowx(0, y) = 4.f / N; + // if (bed(0, y) < 0.75f) + // flowx(0, y) = 4.f / N; // flowx(0, y) = 5.f * math::sqr(std::max(0.f, std::sin(15.f * time))) / N; - if (bed(N - 1, y) < 0.75f) - flowx(N, y) = 5.f / N; + // if (bed(N - 1, y) < 0.75f) + // flowx(N, y) = 5.f / N; } // Rain :) @@ -195,9 +218,13 @@ void water_2d_app::update() // Update X flows for (int y = 0; y < N; ++y) + { for (int x = 1; x < N; ++x) flowx(x, y) = friction * flowx(x, y) + g * dt * (water(x - 1, y) + bed(x - 1, y) - water(x, y) - bed(x, y)); + flowx(0, y) = friction * flowx(0, y) + g * dt * (water(N - 1, y) + bed(N - 1, y) - water(0, y) - bed(0, y)); + } + // Update Y flows for (int y = 1; y < N; ++y) for (int x = 0; x < N; ++x) @@ -236,7 +263,7 @@ void water_2d_app::update() { float outflow = 0.f; outflow += std::max(0.f, - flowx(x , y )); - outflow += std::max(0.f, flowx(x + 1, y )); + outflow += std::max(0.f, flowx((x + 1) % N, y )); outflow += std::max(0.f, - flowy(x , y )); outflow += std::max(0.f, flowy(x , y + 1)); @@ -247,7 +274,7 @@ void water_2d_app::update() float scale = std::min(1.f, max_outflow / outflow); if (flowx(x, y) < 0.f) flowx(x, y) *= scale; - if (flowx(x + 1, y) > 0.f) flowx(x + 1, y) *= scale; + if (flowx((x + 1) % N, y) > 0.f) flowx((x + 1) % N, y) *= scale; if (flowy(x, y) < 0.f) flowy(x, y) *= scale; if (flowy(x, y + 1) > 0.f) flowy(x, y + 1) *= scale; @@ -262,7 +289,7 @@ void water_2d_app::update() { float w_old = water(x, y); - water(x, y) += dt / dx / dy * (flowx(x, y) + flowy(x, y) - flowx(x + 1, y) - flowy(x, y + 1)); + water(x, y) += dt / dx / dy * (flowx(x, y) + flowy(x, y) - flowx((x + 1) % N, y) - flowy(x, y + 1)); float w_avg = (w_old + water(x, y)) / 2.f; @@ -270,7 +297,7 @@ void water_2d_app::update() if (w_avg > 0.f) { - v[0] = (flowx(x, y) + flowx(x + 1, y)) / 2.f / dx / w_avg; + v[0] = (flowx(x, y) + flowx((x + 1) % N, y)) / 2.f / dx / w_avg; v[1] = (flowy(x, y) + flowy(x, y + 1)) / 2.f / dy / w_avg; } @@ -278,6 +305,20 @@ void water_2d_app::update() } } + // Add coriolis force + for (int y = 0; y < N; ++y) + { + for (int x = 0; x < N; ++x) + { + auto n = math::ort(velocity(x, y)) * coriolis * dt / 2.f * std::sin(float(math::pi) / 2.f * ((y + 0.5f) * 2.f / N - 1.f)); + + flowx(x, y) += n[0]; + flowx((x + 1) % N, y) += n[0]; + flowy(x, y) += n[1]; + flowy(x, y + 1) += n[1]; + } + } + if (erosion_on) { // Erosion-deposition @@ -332,6 +373,36 @@ void water_2d_app::update() std::swap(sediment, new_sediment); } + // Init particles + for (int i = 0; i < particles_per_frame; ++i) + if (particles.size() < max_particles) + { + math::point pos{random::uniform(rng, 0.f, N), random::uniform(rng, 0.f, N)}; + if (water(std::floor(pos[0]), std::floor(pos[1])) > 1e-3f) + particles.push_back({pos, 0}); + } + + // Update particles + for (auto & p : particles) + { + p.position += velocity(std::min(N - 1, std::floor(p.position[0])), std::min(N - 1, std::floor(p.position[1]))) * (N * dt); + p.position[0] = math::fmod(p.position[0], float(N)); + p.position[1] = math::clamp(p.position[1], {0.f, N}); + p.lifetime += 1; + } + + // Destroy particles + for (int i = 0; i < particles.size();) + { + if (particles[i].lifetime >= max_particle_lifetime) + { + std::swap(particles[i], particles.back()); + particles.pop_back(); + } + else + ++i; + } + time += dt; } @@ -371,8 +442,8 @@ void water_2d_app::present() float w = 1.f - std::exp(- 1.f * water(x, y)); float s = 1.f - std::exp(- 1.f * sediment(x, y)); - if (water(x, y) > 1e-3f) - w = 0.5f + 0.5f * w; + // if (water(x, y) > 1e-3f) + // w = 0.5f + 0.5f * w; auto bed_color = gfx::to_coloru8(gfx::color_4f{0.96f, 0.72f, 0.53f, b}); // auto bed_color = gfx::to_coloru8(gfx::color_4f{0.3f, 0.5f, 0.1f, b}); @@ -398,20 +469,33 @@ void water_2d_app::present() { auto center = simulation_area.corner((x + 0.5f) * invN, (y + 0.5f) * invN); - auto v = water(x, y) * velocity(x, y) / 100.f; + auto v = water(x, y) * velocity(x, y) / 40.f; - float max_length = 0.02f; + float max_length = 0.05f; auto l = math::length(v); - auto color = gfx::to_coloru8(gfx::color_4f{1.f, std::exp(- 10.f * l), 0.f, 1.f}); + // auto color = gfx::to_coloru8(gfx::color_4f{1.f, std::exp(- 10.f * l), 0.f, 1.f}); + auto color = gfx::color_rgba{255, 255, 255, 255}; float s = std::min(1.f, l / max_length); + // s = 1.f; - painter.line(center, center + math::normalized(v) * max_length * s, s * 0.004f, s * 0.002f, color, color, false); + auto d = math::normalized(v) * max_length * s / 2.f; + + painter.line(center - d, center + d, s * 0.001f, 0.f, color, color, false); } } } + if (show_particles) + { + for (auto const & p : particles) + { + auto pos = math::lerp(simulation_area, math::vector{p.position[0] / N, p.position[1] / N}); + painter.circle(pos, 0.005f, {255, 255, 255, 127}, 6); + } + } + auto mouse = math::cast(state().mouse); mouse[0] = math::lerp(view_area[0], mouse[0] / state().size[0]); @@ -447,6 +531,11 @@ void water_2d_app::present() } painter.render(transform); + + { + painter.text({20.f, 20.f}, std::format("{} particles", particles.size()), {.scale = {2.f, 2.f}, .x = gfx::painter::x_align::left, .y = gfx::painter::y_align::top, .c = {0, 0, 0, 255}}); + painter.render(math::window_camera{state().size[0], state().size[1]}.transform()); + } } void water_2d_app::on_event(app::resize_event const & event) @@ -467,6 +556,9 @@ void water_2d_app::on_event(app::key_event const & event) if (event.down && event.key == app::keycode::V) show_velocity ^= true; + if (event.down && event.key == app::keycode::P) + show_particles ^= true; + if (event.down && event.key == app::keycode::E) erosion_on ^= true; diff --git a/examples/water_2d_hex.cpp b/examples/water_2d_hex.cpp new file mode 100644 index 00000000..08f43d40 --- /dev/null +++ b/examples/water_2d_hex.cpp @@ -0,0 +1,349 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace psemek; + +static const math::vector x_axis {1.f, 0.f}; +static const math::vector y_axis {0.5f, std::sqrt(0.75f)}; +static const math::vector z_axis = y_axis - x_axis; +static const int N = 64; + +static const float dt = 0.1f; +static const float dx = 1.f; +static float const g = 10.f; +static float const friction = std::pow(0.875f, dt); + +math::point to_world(int x, int y) +{ + return math::point{0.f, 0.f} + x_axis * (x - N / 2.f) + y_axis * (y - N / 2.f); +} + +math::point to_grid(math::point const & p) +{ + static auto matrix = *math::inverse(math::by_columns(x_axis, y_axis)); + static auto zero = math::point{0.f, 0.f}; + + return zero + matrix * (p - zero) + math::vector{N / 2.f, N / 2.f}; +} + +struct water_2d_hex_app + : app::application_base +{ + water_2d_hex_app(options const &, context const &); + + void update() override; + void present() override; + + void on_event(app::resize_event const & event) override; + void on_event(app::key_event const & event) override; + + void stop() override; + +private: + bool vsync_on_ = true; + std::function set_vsync_; + + random::generator rng_{random::device{}}; + + float aspect_ratio_ = 1.f; + math::vector screen_size_; + math::box view_area_; + gfx::painter painter_; + + bool paused_ = false; + float time_ = 0.f; + + bool show_velocity_ = false; + + util::ndarray bed_; + util::ndarray water_; + util::ndarray flow_x_; // flow_x(x, y) is (x-1, y) => (x, y) + util::ndarray flow_y_; // flow_y(x, y) is (x, y-1) => (x, y) + util::ndarray flow_z_; // flow_z(x, y) is (x, y-1) => (x-1, y) +}; + +water_2d_hex_app::water_2d_hex_app(options const &, context const & ctx) + : set_vsync_(ctx.vsync) +{ + set_vsync_(vsync_on_); + + bed_.resize({N + 1, N + 1}, 0.f); + water_.resize({N + 1, N + 1}, 0.f); + flow_x_.resize({N + 2, N + 1}, 0.f); + flow_y_.resize({N + 1, N + 2}, 0.f); + flow_z_.resize({N + 2, N + 2}, 0.f); + + random::uniform_sphere_vector_distribution d_grad; + + util::ndarray, 2> perlin_grad({17, 17}); + for (auto & v : perlin_grad) + v = d_grad(rng_); + + pcg::perlin noise(std::move(perlin_grad)); + + for (int y = 0; y <= N; ++y) + { + for (int x = 0; x <= N; ++x) + { + auto q = (to_world(x, y) - math::point{0.f, 0.f}) / (1.f * N); + auto p = q + math::vector{0.5f, 0.5f}; + + (void)p; + + // Canyon + bed_(x, y) = std::max(0.f, 10.f * std::abs(2.f * noise(p) - 1.f) - 1.5f); + + // Islands + // bed_(x, y) = 5.f * math::smoothstep(math::clamp(math::unlerp({0.45f, 0.55f}, noise(p) - 1.f * math::length(q)), {0.f, 1.f})); + + water_(x, y) = 0.f; + } + } +} + +void water_2d_hex_app::update() +{ + { + float y_extent = (N / 2) * y_axis[1]; + view_area_[1] = {- y_extent, y_extent}; + view_area_[0] = {- y_extent * aspect_ratio_, y_extent * aspect_ratio_}; + } + + if (state().mouse_button_down.contains(app::mouse_button::left)) + { + auto m = math::lerp(view_area_, math::vector{state().mouse[0] * 1.f / screen_size_[0], 1.f - state().mouse[1] * 1.f / screen_size_[1]}); + auto p = to_grid(m); + + int const R = 4; + + for (int dy = -R; dy <= R; ++dy) + { + for (int dx = -R; dx <= R; ++dx) + { + if (dx + dy < -R || dx + dy > R) continue; + + int mx = std::round(p[0]) + dx; + int my = std::round(p[1]) + dy; + + if (mx >= 0 && mx <= N && my >= 0 && my <= N) + { + auto c = to_world(mx, my); + water_(mx, my) += 10.f * std::exp(- 0.25f * math::distance_sqr(c, m)) * dt; + } + } + } + } + + if (paused_) + return; + + prof::profiler prof("update"); + + time_ += dt; + + // Init boundary flows + for (int i = 0; i <= N / 2; ++i) + if (bed_(N / 2 - i, i) < 0.5f) + flow_x_(N / 2 - i, i) = 300.f / N; + // flow_x_(N / 2 - i, i) = 0*std::pow(std::sin(0.5f * time_) * std::sin(math::pi * (i * 4.f / N)), 5.f) * 300.f / N; + + for (int i = 0; i <= N / 2; ++i) + if (bed_(N - i, N / 2 + i) < 0.5f) + flow_x_(N - i + 1, N / 2 + i) = 300.f / N; + + // Update X flows + for (int y = 0; y <= N; ++y) + // for (int x = 1; x <= N; ++x) + for (int x = std::max(1, N / 2 + 1 - y); x <= std::min(N, 3 * N / 2 - y); ++x) + // if (x + y - 1 >= N / 2 && x + y <= 3 * N / 2) + flow_x_(x, y) = friction * flow_x_(x, y) + g * dt * (water_(x - 1, y) + bed_(x - 1, y) - water_(x, y) - bed_(x, y)); + + // Update Y flows + for (int y = 1; y <= N; ++y) + // for (int x = 0; x <= N; ++x) + for (int x = std::max(0, N / 2 + 1 - y); x <= std::min(N, 3 * N / 2 - y); ++x) + // if (x + y - 1 >= N / 2 && x + y <= 3 * N / 2) + flow_y_(x, y) = friction * flow_y_(x, y) + g * dt * (water_(x, y - 1) + bed_(x, y - 1) - water_(x, y) - bed_(x, y)); + + // Update Z flows + for (int y = 1; y <= N; ++y) + // for (int x = 1; x <= N; ++x) + for (int x = std::max(1, N / 2 + 1 - y); x <= std::min(N, 3 * N / 2 + 1 - y); ++x) + // if (x + y - 1 >= N / 2 && x + y - 1 <= 3 * N / 2) + flow_z_(x, y) = friction * flow_z_(x, y) + g * dt * (water_(x, y - 1) + bed_(x, y - 1) - water_(x - 1, y) - bed_(x - 1, y)); + + // Scale flows + for (int y = 0; y <= N; ++y) + { + // for (int x = 0; x <= N; ++x) + for (int x = std::max(0, N / 2 - y); x <= std::min(N, 3 * N / 2 - y); ++x) + { + // if (x + y >= N / 2 && x + y <= 3 * N / 2) + { + float outflow = 0.f; + + float & fin1 = flow_x_(x , y ); + float & fin2 = flow_y_(x , y ); + float & fin3 = flow_z_(x + 1, y ); + + float & fout1 = flow_x_(x + 1, y ); + float & fout2 = flow_y_(x , y + 1); + float & fout3 = flow_z_(x , y + 1); + + outflow += std::max(0.f, -fin1); + outflow += std::max(0.f, -fin2); + outflow += std::max(0.f, -fin3); + outflow += std::max(0.f, fout1); + outflow += std::max(0.f, fout2); + outflow += std::max(0.f, fout3); + + if (outflow > 0.f) + { + float max_outflow = water_(x, y) * dx * dx / dt; + + float scale = std::min(1.f, max_outflow / outflow); + + fin1 *= (fin1 < 0.f ? scale : 1.f); + fin2 *= (fin2 < 0.f ? scale : 1.f); + fin3 *= (fin3 < 0.f ? scale : 1.f); + + fout1 *= (fout1 > 0.f ? scale : 1.f); + fout2 *= (fout2 > 0.f ? scale : 1.f); + fout3 *= (fout3 > 0.f ? scale : 1.f); + } + } + } + } + + // Update water + for (int y = 0; y <= N; ++y) + // for (int x = 0; x <= N; ++x) + for (int x = std::max(0, N / 2 - y); x <= std::min(N, 3 * N / 2 - y); ++x) + // if (x + y >= N / 2 && x + y <= 3 * N / 2) + water_(x, y) += dt / dx / dx * (flow_x_(x, y) + flow_y_(x, y) + flow_z_(x + 1,y) - flow_x_(x + 1, y) - flow_y_(x, y + 1) - flow_z_(x, y + 1)); +} + +void water_2d_hex_app::present() +{ + gl::ClearColor(0.f, 0.f, 0.f, 0.f); + gl::Clear(gl::COLOR_BUFFER_BIT); + + for (int y = 0; y < N; ++y) + { + for (int x = 0; x < N; ++x) + { + auto color = [this](int x, int y) + { + auto bed = gfx::color_4f{0.9f, 0.7f, 0.5f, - std::expm1(- bed_(x, y))}; + auto water = gfx::color_4f{0.0f, 0.25f, 1.f, - std::expm1(- water_(x, y))}; + + auto color = gfx::color_4f{0.f, 0.f, 0.f, 0.f}; + color = gfx::blend(color, bed); + color = gfx::blend(color, water); + return gfx::to_coloru8(color); + }; + + auto p00 = to_world(x, y); + auto p01 = to_world(x + 1, y); + auto p10 = to_world(x, y + 1); + auto p11 = to_world(x + 1, y + 1); + + auto b00 = color(x, y); + auto b01 = color(x + 1, y); + auto b10 = color(x, y + 1); + auto b11 = color(x + 1, y + 1); + + if (x + y >= N / 2 && x + y < (3 * N) / 2) + painter_.triangle(p00, p01, p10, b00, b01, b10); + + if (x + y + 1 >= N / 2 && x + y + 1 < (3 * N) / 2) + painter_.triangle(p10, p01, p11, b10, b01, b11); + } + } + + if (show_velocity_) + for (int y = 0; y <= N; ++y) + { + for (int x = 0; x <= N; ++x) + { + if (x + y >= N / 2 && x + y <= (3 * N) / 2) + { + auto p = to_world(x, y); + + auto vx = (flow_x_(x, y) + flow_x_(x + 1, y)) * 0.5f * x_axis; + auto vy = (flow_y_(x, y) + flow_y_(x, y + 1)) * 0.5f * y_axis; + auto vz = (flow_z_(x + 1, y) + flow_z_(x, y + 1)) * 0.5f * z_axis; + + auto v = (vx + vy + vz); + + auto M = 1.f; + auto l = math::length(v); + float s = std::min(1.f, l / M); + float c = -std::expm1(- 0.1f * l); + + auto color = gfx::to_coloru8(gfx::color_4f{1.f, 1.f - c, 1.f - c, 1.f}); + + painter_.line(p, p + v * (s / l), 0.25f * s, 0.f, color, color, true); + } + } + } + + painter_.render(math::orthographic_camera{view_area_}.transform()); +} + +void water_2d_hex_app::on_event(app::resize_event const & event) +{ + gl::Viewport(0, 0, event.size[0], event.size[1]); + screen_size_ = event.size; + aspect_ratio_ = (event.size[0] * 1.f) / event.size[1]; +} + +void water_2d_hex_app::on_event(app::key_event const & event) +{ + if (event.down) + { + switch (event.key) + { + case app::keycode::V: + vsync_on_ ^= true; + set_vsync_(vsync_on_); + break; + case app::keycode::C: + show_velocity_ ^= true; + break; + case app::keycode::SPACE: + paused_ ^= true; + break; + default: + break; + } + } +} + + +void water_2d_hex_app::stop() +{ + app::application_base::stop(); + prof::dump(); +} + +namespace psemek::app +{ + + std::unique_ptr make_application_factory() + { + return default_application_factory({.name = "Water 2D hex example", .multisampling = 4}); + } + +} diff --git a/examples/weather.cpp b/examples/weather.cpp new file mode 100644 index 00000000..18b9de1e --- /dev/null +++ b/examples/weather.cpp @@ -0,0 +1,881 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace psemek; + +auto make_perlin(random::generator & rng, int min_octave, int max_octave, float power) +{ + std::vector> octaves; + std::vector weights; + + random::uniform_sphere_vector_distribution random_vector{}; + + for (int octave = min_octave; octave < max_octave; ++octave) + { + int size = 1 << octave; + util::ndarray, 2> gradients({size + 1, size + 1}); + for (auto & g : gradients) + g = random_vector(rng); + octaves.emplace_back(std::move(gradients)); + weights.push_back(std::pow(power, - octave)); + } + + float weight_sum = 0.f; + for (auto w : weights) + weight_sum += w; + for (auto & w : weights) + w /= weight_sum; + + return pcg::fractal>(std::move(octaves), std::move(weights)); +} + +void make_force_field(random::generator & rng, util::ndarray, 2> & result, float scale) +{ + auto noise_1 = make_perlin(rng, 4, 6, 2.f); + auto noise_2 = make_perlin(rng, 4, 6, 2.f); + + for (int y = 0; y < result.height(); ++y) + { + for (int x = 0; x < result.width(); ++x) + { + math::point p{(x + 0.5f) / result.height(), (y + 0.5f) / result.width()}; + result(x, y) = scale * (math::vector{noise_1(p), noise_2(p)} * 2.f - math::vector{1.f, 1.f}); + } + } +} + +struct weather_app + : app::application_base +{ + static constexpr int N = 128; + + const bool static_mode = false; + + const float dt = 20.f; + const float viscosity = static_mode ? 0.01f : 0.f; + const bool temperature_advection = true; + const float advection_magnification = 1.f; + const float temperature_diffusion = 0.0004f; + const float cooling = 0.01f / 300.f; + const float cooling_factor = std::exp(- cooling * dt); + const float heating = 323.f * (std::exp(cooling * dt) - 1.f) / dt; + const float water_heating_factor = 0.9f; + const float coriolis = 0.001f; + const float coriolis_bands = 6.f; + const float band_force = 0.00001f; + const float friction = 0.f; + const float slope_friction = 1.f; + const float slope_force = 0.001f; + const float land_force = 0.05f; + const float buoyancy_factor = 0.0002f * 0; + const float vorticity_confinement = 0.f; + const float elevation_temperature_drop = 30.f; + const float evaporation = 1.0f; + const float max_humidity_factor = 1.f; + const float precipitation_factor = 0.0001f; + const float force_field_amplitude = 0.00005f; + const float random_forces = 0.25f * (static_mode ? 0.f : 1.f); + const float force_field_switch_duration = 720.f * 7.5f; // 7.5 days + const int force_field_switch_frames = std::round(force_field_switch_duration / dt); + // const float friction_factor = 1.f - std::exp(- friction * dt); + + const bool periodic_x = true; + + // random::generator rng{random::device{}}; + random::generator rng{0, 0}; + + gfx::pixmap_rgba biomes_map; + + float expected_temperature_at(int y, bool water) const + { + // float latitude = (y - N * 0.5f) * 2.f / N; + // return std::cos(latitude * float(math::pi)); + + return temperature_income_at(y) * (water ? water_heating_factor : 1.f) * dt / (std::exp(cooling * dt) - 1.f); + } + + float temperature_income_at(int y) const + { + // float latitude = (y - N * 0.5f) * 2.f / N; + float latitude = y * 1.f / N; + return heating * math::lerp(0.75f, 1.f, std::cos(latitude * float(math::pi) / 2.f)); + // return heating * math::lerp(0.8f, 1.f, 1.f - std::abs(latitude)); + } + + int wrap(int i) const + { + return (i + N) % N; + } + + weather_app(options const &, context const &) + { + simulation_box_ = {{{0.f, N}, {0.f, N}}}; + + terrain_.resize({N, N}, 0.f); + velocity_.resize({N, N}); + new_velocity_.resize({N, N}); + pressure_.resize({N, N}, 0.f); + temperature_.resize({N, N}, -1.f); + new_temperature_.resize({N, N}); + average_temperature_.resize({N, N}, 0.f); + force_field_main_.resize({N, N}); + force_field_current_.resize({N, N}); + force_field_next_.resize({N, N}); + vorticity_.resize({N, N}); + humidity_.resize({N, N}); + new_humidity_.resize({N, N}); + precipitation_.resize({N, N}); + average_precipitation_.resize({N, N}); + + + auto terrain_noise = make_perlin(rng, 2, 10, 1.6f); + + for (int y = 0; y < N; ++y) + { + for (int x = 0; x < N; ++x) + { + auto d = math::length(math::vector{x - N / 2.f, y - N / 2.f}) / (N / 2.f); + (void)d; + float value = terrain_noise((x + 0.5f) / N, (y + 0.5f) / N); + value = pow(value, 4.f) - d / 4.f; + value = math::lerp(1.f, 16.f, value); + terrain_(x, y) = value; + } + } + + auto heightmap = gfx::read_image(io::file_istream{std::filesystem::path{PSEMEK_EXAMPLES_DIR} / "heightmap_seed_1.png"}); + + for (int y = 0; y < N; ++y) + { + for (int x = 0; x < N; ++x) + { + terrain_(x, y) = ((heightmap(x, y) / 255.f) * 2048.f - 512.f) / 1024.f; + } + } + + random::uniform_ball_vector_distribution random_velocity{}; + + // for (auto & v : velocity_) + // { + // v = random_velocity(rng) * 0.01f; + // v += std::cos(0.5f * float(math::pi) * latitude * coriolis_bands + // } + + for (int y = 0; y < N; ++y) + { + for (int x = 0; x < N; ++x) + { + float latitude = (N * 0.5f - y) * 2.f / N; + velocity_(x, y) = random_velocity(rng) * 0.f + 0.f * math::vector{-std::cos(0.5f * float(math::pi) * latitude * coriolis_bands), 0.f}; + temperature_(x, y) = expected_temperature_at(y, terrain_(x, y) <= 0.f); + } + } + + for (int y = 0; y < N; ++y) + { + for (int x = 0; x < N; ++x) + { + if (terrain_(x, y) > 0.f) + continue; + + float max_humidity = std::max(0.f, temperature_(x, y) - 223.f) * max_humidity_factor; + humidity_(x, y) = max_humidity + dt * evaporation * std::max(0.f, temperature_(x, y) - 273.f) * (1.f - precipitation_factor * dt) / precipitation_factor / dt; + humidity_(x, y) = 0.f; + } + } + + make_force_field(rng, force_field_main_, 0.5f); + make_force_field(rng, force_field_next_, 0.5f); + + biomes_map = gfx::read_image(io::file_istream{std::filesystem::path{PSEMEK_EXAMPLES_DIR} / "biomes.png"}); + } + + void on_event(app::key_event const & event) override + { + if (event.down && event.key == app::keycode::SPACE) + paused_ ^= true; + + if (event.down && event.key == app::keycode::V) + show_velocity_ ^= true; + + if (event.down && event.key == app::keycode::T) + show_temperature_ ^= true; + + if (event.down && event.key == app::keycode::D) + show_temperature_delta_ ^= true; + + if (event.down && event.key == app::keycode::A) + show_average_temperature_delta_ ^= true; + + if (event.down && event.key == app::keycode::P) + show_pressure_ ^= true; + + if (event.down && event.key == app::keycode::H) + show_land_ ^= true; + + if (event.down && event.key == app::keycode::W) + show_water_vapor_ ^= true; + + if (event.down && event.key == app::keycode::R) + show_precipitation_ ^= true; + + if (event.down && event.key == app::keycode::Q) + show_average_precipitation_ ^= true; + + if (event.down && event.key == app::keycode::B) + show_biomes_ ^= true; + } + + void update() override + { + if (paused_) + return; + + // Update force field + if ((frame_ % force_field_switch_frames) == 0) + { + std::swap(force_field_current_, force_field_next_); + make_force_field(rng, force_field_next_, 0.5f); + } + [[maybe_unused]] float const force_field_t = ((frame_ % force_field_switch_frames) + 0.5f) / force_field_switch_frames; + + int xmin = periodic_x ? 0 : 1; + int xmax = periodic_x ? N : N - 1; // exclusive + + // Temperature source + for (int y = 0; y < N; ++y) + { + for (int x = 0; x < N; ++x) + { + bool const is_water = terrain_(x, y) <= 0.f; + // temperature_(x, y) = math::lerp(temperature_(x, y), expected_temperature_at(y), 1.f - std::exp(- heating * dt)); + temperature_(x, y) += dt * temperature_income_at(y) * (is_water ? water_heating_factor : 1.f); + temperature_(x, y) *= cooling_factor; + } + } + + // Evaporation + for (int y = 0; y < N; ++y) + { + for (int x = 0; x < N; ++x) + { + if (terrain_(x, y) <= 0.f) + humidity_(x, y) += dt * evaporation * std::max(0.f, temperature_(x, y) - 273.f); + + // float discharge = std::min(humidity_(x, y), precipitation_factor * dt); + // float discharge = humidity_(x, y) * precipitation_factor * dt; + // float max_humidity = temperature_(x, y) * max_humidity_factor; + float max_humidity = temperature_(x, y) * temperature_(x, y) * max_humidity_factor; + float discharge = std::max(0.f, humidity_(x, y) - max_humidity) * precipitation_factor * dt; + humidity_(x, y) -= discharge; + precipitation_(x, y) = discharge / dt; + } + } + + // Velocity & temperature advection + for (int i = 0; i < N; ++i) + { + new_temperature_(i, 0) = temperature_(i, 0); + new_temperature_(i, N - 1) = temperature_(i, N - 1); + + new_humidity_(i, 0) = humidity_(i, 0); + new_humidity_(i, N - 1) = humidity_(i, N - 1); + + if (!periodic_x) + { + new_temperature_(0, i) = temperature_(0, i); + new_temperature_(N - 1, i) = temperature_(N - 1, i); + + new_humidity_(0, i) = humidity_(0, i); + new_humidity_(N - 1, i) = humidity_(N - 1, i); + } + } + + for (int y = 1; y < N - 1; ++y) + { + for (int x = xmin; x < xmax; ++x) + { + auto v = velocity_(x, y); + auto p = math::point{x + 0.5f, y + 0.5f} - (advection_magnification * dt) * v; + p[0] = p[0] - 0.5f; + p[1] = math::clamp(p[1] - 0.5f, {0.f, N - 1.f}); + + if (!periodic_x) + p[0] = math::clamp(p[0], {0.f, N - 1.f}); + + int ix = std::floor(p[0]); + int iy = std::min(N - 1, std::floor(p[1])); + + if (!periodic_x) + ix = std::min(N - 1, ix); + + float tx = p[0] - ix; + float ty = p[1] - iy; + + new_velocity_(x, y) = math::lerp( + math::lerp(velocity_(wrap(ix + 0), iy + 0), velocity_(wrap(ix + 1), iy + 0), tx), + math::lerp(velocity_(wrap(ix + 0), iy + 1), velocity_(wrap(ix + 1), iy + 1), tx), + ty + ); + + new_temperature_(x, y) = math::lerp( + math::lerp(temperature_(wrap(ix + 0), iy + 0), temperature_(wrap(ix + 1), iy + 0), tx), + math::lerp(temperature_(wrap(ix + 0), iy + 1), temperature_(wrap(ix + 1), iy + 1), tx), + ty + ); + + new_humidity_(x, y) = math::lerp( + math::lerp(humidity_(wrap(ix + 0), iy + 0), humidity_(wrap(ix + 1), iy + 0), tx), + math::lerp(humidity_(wrap(ix + 0), iy + 1), humidity_(wrap(ix + 1), iy + 1), tx), + ty + ); + } + } + std::swap(velocity_, new_velocity_); + if (temperature_advection) std::swap(temperature_, new_temperature_); + std::swap(humidity_, new_humidity_); + + // Apply velocity diffusion + for (int y = 0; y < N; ++y) + for (int x = 0; x < N; ++x) + new_velocity_(x, y) = velocity_(x, y); + for (int y = 1; y < N - 1; ++y) + { + for (int x = xmin; x < xmax; ++x) + { + // Velocity Laplacian + auto laplacian = velocity_(wrap(x + 1), y) + velocity_(wrap(x - 1), y) + velocity_(x, y + 1) + velocity_(x, y - 1) - 4.f * velocity_(x, y); + new_velocity_(x, y) = velocity_(x, y) + viscosity * dt * laplacian; + } + } + std::swap(velocity_, new_velocity_); + + // Apply temperature diffusion + for (int y = 0; y < N; ++y) + for (int x = 0; x < N; ++x) + new_temperature_(x, y) = temperature_(x, y); + for (int y = 1; y < N - 1; ++y) + { + for (int x = xmin; x < xmax; ++x) + { + // Temperature Laplacian + auto laplacian = temperature_(wrap(x + 1), y) + temperature_(wrap(x - 1), y) + temperature_(x, y + 1) + temperature_(x, y - 1) - 4.f * temperature_(x, y); + new_temperature_(x, y) = temperature_(x, y) + temperature_diffusion * dt * laplacian; + } + } + std::swap(temperature_, new_temperature_); + + // Compute vorticity + for (int y = 1; y < N - 1; ++y) + for (int x = xmin; x < xmax; ++x) + vorticity_(x, y) = (velocity_(x, y + 1)[0] - velocity_(x, y - 1)[0]) / 2.f - (velocity_(wrap(x + 1), y)[1] - velocity_(wrap(x - 1), y)[1]) / 2; + + // Apply forces & friction + for (int y = 1; y < N - 1; ++y) + { + for (int x = xmin; x < xmax; ++x) + { + // float latitude = (N * 0.5f - y) * 2.f / N; + [[maybe_unused]] float latitude = (N - y) * 1.f / N; + // velocity_(x, y) += math::ort(velocity_(x, y)) * (coriolis * dt * std::sin(0.5f * float(math::pi) * latitude * coriolis_bands)); + velocity_(x, y) = math::rotate(velocity_(x, y), coriolis * dt * std::sin(0.5f * float(math::pi) * latitude * coriolis_bands)); + + // auto force = force_field_main_(x, y) + random_forces * math::lerp(force_field_current_(x, y), force_field_next_(x, y), force_field_t); + // velocity_(x, y) += (dt * force_field_amplitude) * force; + + // velocity_(x, y)[0] += dt * 0.000001f * std::cos(0.5f * float(math::pi) * latitude * 4.f); + // velocity_(x, y)[0] += dt * 0.000001f; + + velocity_(x, y)[0] += dt * band_force * std::sin(0.5f * float(math::pi) * latitude * coriolis_bands); + + [[maybe_unused]] math::vector terrain_gradient + { + (std::max(0.f, terrain_(x + 1, y)) - std::max(0.f, terrain_(x - 1, y))) / 2.f, + (std::max(0.f, terrain_(x, y + 1)) - std::max(0.f, terrain_(x, y - 1))) / 2.f, + }; + + [[maybe_unused]] math::vector temperature_gradient + { + (temperature_(x + 1, y) - temperature_(x - 1, y)) / 2.f, + (temperature_(x, y + 1) - temperature_(x, y - 1)) / 2.f, + }; + + velocity_(x, y) += temperature_gradient * buoyancy_factor * dt; + + [[maybe_unused]] float slope_factor = std::exp(- dt * slope_friction * math::dot(math::normalized(velocity_(x, y)), terrain_gradient)); + velocity_(x, y) *= std::min(1.f, slope_factor); + // velocity_(x, y) -= terrain_gradient * slope_force * dt; + + // [[maybe_unused]] float slope_factor = std::exp(- dt * slope_force * std::pow(math::length(terrain_gradient), 4.f)); + [[maybe_unused]] float land_factor = std::exp(- dt * land_force * std::pow(std::max(0.f, terrain_(x, y)), 1.f)); + velocity_(x, y) *= land_factor; + + // Directional external force + // velocity_(x, y)[1] += 0.001f * dt * std::sin(0.5f * float(math::pi) * latitude * coriolis_bands); + // velocity_(x, y) += math::direction(frame_ * dt * 2.f * float(math::pi) / 10080.f) * 0.001f * dt; + + math::vector vorticity_gradient + { + (vorticity_(wrap(x + 1), y) - vorticity_(wrap(x - 1), y)) / 2.f, + (vorticity_(x, y + 1) - vorticity_(x, y - 1)) / 2.f, + }; + if (auto l = math::length(vorticity_gradient); l > 0.f) + vorticity_gradient /= l; + + velocity_(x, y) += vorticity_confinement * dt * vorticity_(x, y) * math::ort(vorticity_gradient); + + float local_friction = friction * terrain_(x, y); + // velocity_(x, y) -= local_friction * velocity_(x, y) * math::length(velocity_(x, y)); + [[maybe_unused]] float local_friction_factor = std::exp(- local_friction * dt); + // velocity_(x, y) *= local_friction_factor; + } + } + + // Solve Poisson equation for pressure + for (int iteration = 0; iteration < 16; ++iteration) + { + int ymin = ((iteration % 2) == 0) ? 1 : N - 2; + int ymax = ((iteration % 2) == 0) ? N - 1 : 0; + int ystep = ((iteration % 2) == 0) ? 1 : -1; + + for (int y = ymin; y != ymax; y += ystep) + { + for (int x = xmin; x < xmax; ++x) + { + // Velocity divergence + float divergence = (velocity_(wrap(x + 1), y)[0] - velocity_(wrap(x - 1), y)[0] + velocity_(x, y + 1)[1] - velocity_(x, y - 1)[1]) / 2.f; + + // Gauss-Seidel iteration step + pressure_(x, y) = (pressure_(wrap(x - 1), y) + pressure_(wrap(x + 1), y) + pressure_(x, y - 1) + pressure_(x, y + 1) - divergence) / 4.f; + } + } + } + + // Apply boundary conditions for pressure + for (int i = 0; i < N; ++i) + { + if (!periodic_x) + { + pressure_(0, i) = pressure_(1, i); + pressure_(N - 1, i) = pressure_(N - 2, i); + } + pressure_(i, 0) = pressure_(i, 1); + pressure_(i, N - 1) = pressure_(i, N - 2); + } + if (!periodic_x) + { + pressure_(0, 0) = (pressure_(0, 1) + pressure_(1, 0)) / 2.f; + pressure_(N-1, 0) = (pressure_(N-1, 1) + pressure_(N-2, 0)) / 2.f; + pressure_(0, N-1) = (pressure_(0, N-2) + pressure_(1, N-2)) / 2.f; + pressure_(N-1, N-1) = (pressure_(N-1, N-2) + pressure_(N-2, N-1)) / 2.f; + } + + // Normalize pressure + float average_pressure = 0.f; + for (auto const & value : pressure_) + average_pressure += value; + average_pressure /= (1.f * N * N); + for (auto & value : pressure_) + value -= average_pressure; + + // Project velocity into divergence-free space + // by subtracting pressure gradient + for (int y = 1; y < N - 1; ++y) + { + for (int x = xmin; x < xmax; ++x) + { + // Pressure gradient + math::vector gradient{ + (pressure_(wrap(x + 1), y) - pressure_(wrap(x - 1), y)) / 2.f, + (pressure_(x, y + 1) - pressure_(x, y - 1)) / 2.f + }; + velocity_(x, y) -= gradient; + } + } + + // Apply boundary conditions for velocity + for (int i = 0; i < N; ++i) + { + if (!periodic_x) + { + float left_boundary_flow = 0.f;//0.01f * std::sin((i * 1.f / N) * float(math::pi) * 4.f); + float right_boundary_flow = -left_boundary_flow; + + velocity_(1, i)[0] = left_boundary_flow; + velocity_(N-2, i)[0] = right_boundary_flow; + + velocity_(0, i)[0] = - velocity_(1, i)[0]; + velocity_(N-1, i)[0] = - velocity_(N-2, i)[0]; + } + velocity_(i, 0)[1] = -velocity_(i, 1)[1]; + velocity_(i, N-2)[1] = -velocity_(i, N-2)[1]; + + // velocity_(i, 1)[0] = 0.01f; + // velocity_(i, N-2)[0] = 0.01f; + } + + // Uncomment to visualize the force field + // for (int y = 0; y < N; ++y) + // for (int x = 0; x < N; ++x) + // velocity_(x, y) = 100000.f * force_field_(x, y); + + // Uncomment to visualize the terrain gradient field + // for (int y = 1; y < N - 1; ++y) + // { + // for (int x = 1; x < N - 1; ++x) + // { + // math::vector terrain_gradient + // { + // (terrain_(x + 1, y) - terrain_(x - 1, y)) / 2.f, + // (terrain_(x, y + 1) - terrain_(x, y - 1)) / 2.f, + // }; + + // velocity_(x, y) = terrain_gradient; + // } + // } + + // Apply boundary conditions for humidity + for (int i = 0; i < N; ++i) + { + if (!periodic_x) + { + humidity_(0, i) = 0.f; + humidity_(N - 1, i) = 0.f; + } + humidity_(i, 0) = 0.f; + humidity_(i, N - 1) = 0.f; + } + + ++frame_; + + // Update all-time average temperature & precipitation + for (int y = 0; y < N; ++y) + { + for (int x = 0; x < N; ++x) + { + // float t = 1.f / frame_; + // float t = 1.f / std::min(8192, frame_); + float t = 1.f / std::min(86400.f / dt, frame_); // year average + + if (static_mode) + t = 1.f; + + average_temperature_(x, y) = math::lerp(average_temperature_(x, y), temperature_(x, y), t); + average_precipitation_(x, y) = math::lerp(average_precipitation_(x, y), precipitation_(x, y), t); + } + } + } + + void present() override + { + gl::ClearColor(0.f, 0.f, 0.f, 0.f); + gl::Clear(gl::COLOR_BUFFER_BIT); + + float const aspect_ratio = state().size[0] * 1.f / state().size[1]; + math::box view_box = math::expand(simulation_box_, 1.f); + if (view_box[0].length() / view_box[1].length() > aspect_ratio) + view_box[1] = math::expand(view_box[1], (view_box[0].length() / aspect_ratio - view_box[1].length()) / 2.f); + else + view_box[0] = math::expand(view_box[0], (view_box[1].length() * aspect_ratio - view_box[0].length()) / 2.f); + + std::optional> mouseover_cell; + { + auto mouse = math::lerp(view_box, math::vector{state().mouse[0] * 1.f / state().size[0], 1.f - state().mouse[1] * 1.f / state().size[1]}); + int x = std::floor(mouse[0]); + int y = std::floor(mouse[1]); + + if (x >= 0 && x < N && y >= 0 && y < N) + mouseover_cell = {x, y}; + } + + if (mouseover_cell && (state().mouse_button_down.contains(app::mouse_button::left) || state().mouse_button_down.contains(app::mouse_button::right))) + { + float delta = state().mouse_button_down.contains(app::mouse_button::left) ? 1.f : -1.f; + + int R = 4; + + for (int iy = -R; iy <= R; ++iy) + { + for (int ix = -R; ix <= R; ++ix) + { + int x = (*mouseover_cell)[0] + ix; + int y = (*mouseover_cell)[1] + iy; + + if (x >= 0 && x < N && y >= 0 && y < N) + { + auto d = math::vector{ix, iy} / 2.f; + terrain_(x, y) += delta * 0.05f * std::exp(- math::dot(d, d)); + } + } + } + } + + if (mouseover_cell && state().mouse_button_down.contains(app::mouse_button::middle)) + { + int R = 4; + + float average = 0.f; + int count = 0; + + for (int iy = -R; iy <= R; ++iy) + { + for (int ix = -R; ix <= R; ++ix) + { + int x = (*mouseover_cell)[0] + ix; + int y = (*mouseover_cell)[1] + iy; + + if (x >= 0 && x < N && y >= 0 && y < N) + { + average += terrain_(x, y); + count += 1; + } + } + } + + average /= count; + + for (int iy = -R; iy <= R; ++iy) + { + for (int ix = -R; ix <= R; ++ix) + { + int x = (*mouseover_cell)[0] + ix; + int y = (*mouseover_cell)[1] + iy; + + if (x >= 0 && x < N && y >= 0 && y < N) + { + auto d = math::vector{ix, iy} / 2.f; + terrain_(x, y) += (average - terrain_(x, y)) * 0.05f * std::exp(- math::dot(d, d)); + } + } + } + } + + [[maybe_unused]] float const pixel_size = view_box[0].length() / state().size[0]; + + auto map_color = [](float value, gfx::color_4f const & negative, gfx::color_4f const & positive){ + return math::lerp(negative, positive, 1.f/ (1.f + std::exp(- value))); + }; + + auto map_temperature = [&](float value) { + return map_color(2.f * std::round(value / 20.f), {0.125f, 0.5f, 1.f, 0.75f}, {1.f, 0.5f, 0.125f, 0.75f}); + }; + + auto map_biome = [this](float temperature, float precipitation) + { + auto x = math::clamp(math::unlerp({ -3.f, 5.f}, precipitation) * biomes_map.width() , {0, biomes_map.width() - 1}); + auto y = math::clamp(math::unlerp({-10.f, 30.f}, temperature ) * biomes_map.height(), {0, biomes_map.height() - 1}); + + return gfx::to_colorf(biomes_map(x, y)); + }; + + for (int y = 0; y < N; ++y) + { + for (int x = 0; x < N; ++x) + { + gfx::color_4f color = gfx::color_4f::zero(); + + if (show_land_ || show_biomes_) + { + if (!show_biomes_) + { + if (terrain_(x, y) <= 0.f) + color = {0.5f, 0.5f, 1.f, 1.f}; + else + color = {1.f, 1.f, 1.f, 1.f}; + } + else + { + if (terrain_(x, y) <= 0.f) + color = map_color(8.f * terrain_(x, y), {0.f, 0.f, 0.125f, 1.f}, {0.f, 1.f, 1.5f, 1.f}); + else + { + float temperature = average_temperature_(x, y) - 273.f - std::max(0.f, terrain_(x, y)) * elevation_temperature_drop; + // float precipitation = (std::log10(std::max(1e-9f, average_precipitation_(x, y))) + 3.f) * 2.f; + // float precipitation = std::pow(average_precipitation_(x, y) * 125.f, 2.f) * 8.f; + float precipitation = std::log2(std::max(1e-9f, average_precipitation_(x, y))); + color = map_biome(temperature, precipitation); + } + } + + if (show_land_ && x > 0 && x + 1 < N && y > 0 && y + 1 < N) + { + math::vector terrain_gradient + { + (terrain_(x + 1, y) - terrain_(x - 1, y)) / 2.f, + (terrain_(x, y + 1) - terrain_(x, y - 1)) / 2.f, + }; + + auto terrain_normal = math::normalized(math::vector{-terrain_gradient[0], -terrain_gradient[1], 0.125f}); + + float lightness = 0.5f + 0.5f * math::dot(terrain_normal, math::normalized(math::vector{1.f, 2.f, 3.f})); + + color = gfx::dark(color, 1.f - lightness); + } + } + + bool const is_water = terrain_(x, y) <= 0.f; + + if (show_temperature_) + color = map_temperature(temperature_(x, y) - 273.f); + + if (show_temperature_delta_) + color = gfx::blend(color, map_color((temperature_(x, y) - expected_temperature_at(y, is_water)), {0.125f, 0.5f, 1.f, 0.75f}, {1.f, 0.5f, 0.125f, 0.75f})); + + if (show_average_temperature_delta_) + color = gfx::blend(color, map_color((average_temperature_(x, y) - expected_temperature_at(y, is_water)), {0.125f, 0.5f, 1.f, 0.75f}, {1.f, 0.5f, 0.125f, 0.75f})); + + if (show_pressure_) + color = gfx::blend(color, map_color(10000.f * pressure_(x, y), {0.f, 0.f, 1.f, 0.75f}, {1.f, 0.f, 0.f, 0.75f})); + + if (show_water_vapor_) + color = gfx::blend(color, map_color(humidity_(x, y) * 0.00001f, {0.f, 1.f, 1.f, -0.75f}, {0.f, 1.f, 1.f, 0.75f})); + + if (show_precipitation_) + { + // color = gfx::blend(color, map_color(precipitation_(x, y), {1.f, 1.f, 1.f, -1.f}, {1.f, 1.f, 1.f, 1.f})); + + float alpha = 2.f / (1.f + std::exp(- 0.1f * precipitation_(x, y))) - 1.f; + + gfx::color_4f cloud_color{1.f, 1.f, 1.f, alpha * 0.875f}; + + if (y > 0 && y + 1 < N) + { + if (periodic_x || (x > 0 && x + 1 < N)) + { + math::vector gradient + { + (precipitation_(wrap(x + 1), y) - precipitation_(wrap(x - 1), y)) / 2.f, + (precipitation_(x, y + 1) - precipitation_(x, y - 1)) / 2.f, + }; + + auto normal = math::normalized(math::vector{-gradient[0], -gradient[1], 2.f}); + auto lightness = 0.5f + 0.5f * math::dot(normal, math::normalized(math::vector{1.f, 2.f, 3.f})); + cloud_color = gfx::dark(cloud_color, 1.f - lightness); + } + } + + color = gfx::blend(color, cloud_color); + } + + if (show_average_precipitation_) + color = gfx::blend(color, map_color(average_precipitation_(x, y), {0.f, 1.f, 1.f, -0.75f}, {0.f, 1.f, 1.f, 0.75f})); + + painter_.rect({{{x, x + 1.f}, {y, y + 1.f}}}, gfx::to_coloru8(color)); + } + } + + if (show_velocity_) + { + for (int y = 0; y < N; ++y) + { + for (int x = 0; x < N; ++x) + { + math::point center{x + 0.5f, y + 0.5f}; + auto v = velocity_(x, y); + auto color = gfx::color_4f::zero(); + if (auto l = math::length(v); l > 0.f) + { + float const magnification = 200.f; + float const max_length = 1.5f; + v *= 0.5f * max_length * (1.f - std::exp(- magnification * l)) / l; + + // color = gfx::lerp(gfx::color_4f{0.5f, 1.f, 0.f, 1.f}, gfx::color_4f{1.f, 0.f, 0.f, 1.f}, 1.f - std::exp(- 0.25f * magnification * l)); + color = map_color(0.1f * (temperature_(x, y) - 273.f), {0.125f, 0.5f, 1.f, 1.f}, {1.f, 0.5f, 0.125f, 1.f}); + } + auto n = math::ort(v) * 0.3f; + + painter_.triangle(center - v - n, center - v + n, center + v, gfx::to_coloru8(color)); + } + } + } + + auto push_text = [&, row = 0](std::string const & text) mutable + { + painter_.text(view_box.corner(0.f, 1.f) - math::vector{0.f, row * pixel_size * 2.f * 12.f}, text, {.scale = {2.f * pixel_size, - 2.f * pixel_size}, .x = gfx::painter::x_align::left, .y = gfx::painter::y_align::top, .c = {255, 255, 255, 255}}); + ++row; + }; + + push_text(std::format("Frame {}", frame_)); + push_text(std::format("Day {:.2f}", frame_ / (720.f / dt))); + + if (mouseover_cell) + { + int x = (*mouseover_cell)[0]; + int y = (*mouseover_cell)[1]; + painter_.rect({{{x, x + 1.f}, {y, y + 1.f}}}, {255, 255, 255, 127}); + + push_text(std::format("{} {}", x, y)); + push_text(std::format("V = {:.3f} {:.3f}", velocity_(x, y)[0] * 1000.f, velocity_(x, y)[1] * 1000.f)); + push_text(std::format("P = {:.3f}", pressure_(x, y) * 1000.f)); + push_text(std::format("T = {:.3f}", temperature_(x, y) - 273.f)); + push_text(std::format("A = {:.3f}", average_temperature_(x, y) - 273.f)); + push_text(std::format("E = {:.3f}", expected_temperature_at(y, terrain_(x, y) <= 0.f) - 273.f)); + push_text(std::format("H = {:.3f}", terrain_(x, y))); + push_text(std::format("W = {:.3f}", humidity_(x, y))); + push_text(std::format("R = {:.3f}", precipitation_(x, y))); + push_text(std::format("AR= {:.3f}", average_precipitation_(x, y))); + } + + painter_.render(math::orthographic_camera{view_box}.transform()); + } + +private: + gfx::painter painter_; + + math::box simulation_box_; + + bool paused_ = true; + bool show_velocity_ = true; + bool show_temperature_ = false; + bool show_temperature_delta_ = false; + bool show_average_temperature_delta_ = false; + bool show_pressure_ = false; + bool show_land_ = true; + bool show_biomes_ = true; + bool show_water_vapor_ = false; + bool show_precipitation_ = false; + bool show_average_precipitation_ = false; + + util::ndarray terrain_; + util::ndarray, 2> velocity_; + util::ndarray, 2> new_velocity_; + util::ndarray pressure_; + util::ndarray vorticity_; + + util::ndarray temperature_; + util::ndarray new_temperature_; + util::ndarray average_temperature_; + + util::ndarray humidity_; + util::ndarray new_humidity_; + util::ndarray precipitation_; + util::ndarray average_precipitation_; + + util::ndarray, 2> force_field_main_; + util::ndarray, 2> force_field_current_; + util::ndarray, 2> force_field_next_; + + int frame_ = 0; +}; + +namespace psemek::app +{ + + std::unique_ptr make_application_factory() + { + return default_application_factory({.name = "Weather simulation test"}); + } + +} diff --git a/examples/weather_v2.cpp b/examples/weather_v2.cpp new file mode 100644 index 00000000..d513ab93 --- /dev/null +++ b/examples/weather_v2.cpp @@ -0,0 +1,1265 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace psemek; + +auto make_perlin(random::generator & rng, int min_octave, int max_octave, float power) +{ + std::vector> octaves; + std::vector weights; + + random::uniform_sphere_vector_distribution random_vector{}; + + for (int octave = min_octave; octave < max_octave; ++octave) + { + int size = 1 << octave; + util::ndarray, 2> gradients({size + 1, size + 1}); + for (auto & g : gradients) + g = random_vector(rng); + octaves.emplace_back(std::move(gradients)); + weights.push_back(std::pow(power, - octave)); + } + + float weight_sum = 0.f; + for (auto w : weights) + weight_sum += w; + for (auto & w : weights) + w /= weight_sum; + + return pcg::fractal>(std::move(octaves), std::move(weights)); +} + +void add_random_scalar_field(random::generator & rng, util::ndarray & result, int min_octave, int max_octave, float scale) +{ + auto noise = make_perlin(rng, min_octave, max_octave, 2.f); + + for (int y = 0; y < result.height(); ++y) + { + for (int x = 0; x < result.width(); ++x) + { + math::point p{(x + 0.5f) / result.height(), (y + 0.5f) / result.width()}; + result(x, y) += noise(p) * scale; + } + } +} + +void add_random_vector_field(random::generator & rng, util::ndarray, 2> & result, int min_octave, int max_octave, float scale) +{ + auto noise_1 = make_perlin(rng, min_octave, max_octave, 2.f); + auto noise_2 = make_perlin(rng, min_octave, max_octave, 2.f); + + for (int y = 0; y < result.height(); ++y) + { + for (int x = 0; x < result.width(); ++x) + { + math::point p{(x + 0.5f) / result.height(), (y + 0.5f) / result.width()}; + result(x, y) += math::normalized(math::vector{2.f * noise_1(p) - 1.f, 2.f * noise_2(p) - 1.f}) * scale; + } + } +} + +template +void mult_random_scalar_field(random::generator & rng, util::ndarray & result, int min_octave, int max_octave, float min, float max) +{ + auto noise = make_perlin(rng, min_octave, max_octave, 2.f); + + for (int y = 0; y < result.height(); ++y) + { + for (int x = 0; x < result.width(); ++x) + { + math::point p{(x + 0.5f) / result.height(), (y + 0.5f) / result.width()}; + result(x, y) *= math::lerp(min, max, noise(p)); + } + } +} + +template +void add(util::ndarray & dst, util::ndarray const & src) +{ + auto src_begin = src.begin(); + auto src_end = src.end(); + auto dst_begin = dst.begin(); + for (; src_begin != src_end;) + *dst_begin++ += *src_begin++; +} + +template +void scale(util::ndarray & array, float factor) +{ + for (auto & v : array) + v *= factor; +} + +float do_lerp(float x, float y, float t) +{ + return math::lerp(x, y, t); +} + +math::vector do_lerp(math::vector const & x, math::vector const & y, float t) +{ + auto lx = length(x); + auto ly = length(y); + if (lx > 0.f && ly > 0.f) + return math::lerp(lx, ly, t) * math::slerp(x / lx, y / ly, t); + return math::lerp(x, y, t); +} + +template +void lerp(util::ndarray & dst, util::ndarray const & src0, util::ndarray const & src1, float t) +{ + auto src0_begin = src0.begin(); + auto src0_end = src0.end(); + auto src1_begin = src1.begin(); + auto dst_begin = dst.begin(); + + for (; src0_begin != src0_end;) + *dst_begin++ = do_lerp(*src0_begin++, *src1_begin++, t); +} + +template +T sample_bilinear(util::ndarray const & array, math::point p) +{ + p[0] = math::clamp(p[0] - 0.5f, {0.f, array.width() - 1.f}); + p[1] = math::clamp(p[1] - 0.5f, {0.f, array.height() - 1.f}); + + int ix = std::min(array.width() - 2, std::floor(p[0])); + int iy = std::min(array.height() - 2, std::floor(p[1])); + + float tx = p[0] - ix; + float ty = p[1] - iy; + + return math::lerp( + math::lerp(array(ix + 0, iy + 0), array(ix + 1, iy + 0), tx), + math::lerp(array(ix + 0, iy + 1), array(ix + 1, iy + 1), tx), + ty + ); +} + +std::string season_str(int season) +{ + switch (season) + { + case 0: return "Winter"; + case 1: return "Spring"; + case 2: return "Summer"; + case 3: return "Autumn"; + } + + return "???"; +} + +std::string day_night_str(int day_night) +{ + switch (day_night) + { + case 0: return "Night"; + case 1: return "Day"; + } + + return "???"; +} + +struct climate_snapshot +{ + util::ndarray, 2> wind_velocity; + util::ndarray temperature; + util::ndarray humidity; +}; + +static constexpr int N = 128; + +struct solver +{ + util::ndarray const & terrain; + random::generator & rng; + int season; + int day_night; + + climate_snapshot & snapshot; + + int solve_velocity_iterations = 0; + int max_solve_velocity_iterations = 1; + + int solve_temperature_iterations = 0; + int max_solve_temperature_iterations = N / 4; + + int solve_humidity_iterations = 0; + int max_solve_humidity_iterations = 3 * N; + + util::ndarray, 2> initial_random_wind_velocity; + util::ndarray initial_random_wind_velocity_potential; + util::ndarray pressure; + util::ndarray new_temperature; + util::ndarray new_humidity; + + int stage = 0; + bool finished = false; + + solver(util::ndarray const & terrain, random::generator & rng, int season, int day_night, climate_snapshot & snapshot, + util::ndarray const & base_wind_velocity_potential) + : terrain(terrain) + , rng(rng) + , season(season) + , day_night(day_night) + , snapshot(snapshot) + { + initial_random_wind_velocity.resize({N, N}, math::vector{0.f, 0.f}); + snapshot.wind_velocity.resize({N, N}, math::vector{0.f, 0.f}); + pressure.resize({N, N}, 0.f); + snapshot.temperature.resize({N, N}, 0.f); + new_temperature.resize({N, N}, 0.f); + snapshot.humidity.resize({N, N}, 0.f); + new_humidity.resize({N, N}, 0.f); + + initial_random_wind_velocity_potential = base_wind_velocity_potential.copy(); + add_random_scalar_field(rng, initial_random_wind_velocity_potential, 2, 5, 0.4f); + // add_random_vector_field(rng, initial_random_wind_velocity, 3, 6, 0.002f); + + for (int y = 1; y + 1 < N; ++y) + for (int x = 1; x + 1 < N; ++x) + { + math::vector potential_gradient + { + (initial_random_wind_velocity_potential(x + 1, y) - initial_random_wind_velocity_potential(x - 1, y)) * 0.5f, + (initial_random_wind_velocity_potential(x, y + 1) - initial_random_wind_velocity_potential(x, y - 1)) * 0.5f, + }; + + initial_random_wind_velocity(x, y) = math::ort(potential_gradient) * 0.5f; + } + + // mult_random_scalar_field(rng, initial_random_wind_velocity, 2, 5, 0.01f, 1.f); + + for (int y = 0; y < N; ++y) + for (int x = 0; x < N; ++x) + if (terrain(x, y) < 0.f) + initial_random_wind_velocity(x, y) *= 5.f; + else + initial_random_wind_velocity(x, y) *= std::exp(- 2.f * terrain(x, y)); + + snapshot.wind_velocity = initial_random_wind_velocity.copy(); + + for (int y = 0; y < N; ++y) + for (int x = 0; x < N; ++x) + snapshot.temperature(x, y) = expected_temperature(y); + } + + float expected_temperature(int y) + { + return math::lerp(40.f, -20.f, (y + 0.5f) / N) // Base latitude temperature in spring/autumn + - 15.f * std::cos(season * math::pi * 0.5f) // Seasonal offset of +/-15C + - 5.f * std::cos(day_night * math::pi) // Day-night fluctuation of +/-5C + ; + } + + float expected_humidity(int x, int y) + { + if (terrain(x, y) < 0.f) + return 1.5f; + else + return 0.f; + } + + void step() + { + if (stage == 0) + { + // for (int y = 1; y + 1 < N; ++y) + // { + // for (int x = 1; x + 1 < N; ++x) + // { + // float divergence = 0.f + // + (initial_random_wind_velocity(x + 1, y)[0] - initial_random_wind_velocity(x - 1, y)[0]) * 0.5f + // + (initial_random_wind_velocity(x, y + 1)[1] - initial_random_wind_velocity(x, y + 1)[1]) * 0.5f + // ; + + // // (sum - 4.f * p(x,y)) = divergence + // // sum - 4.f * p = divergence + // // p = (sum - divergence) / 4.f + // pressure(x, y) = (pressure(x - 1, y) + pressure(x + 1, y) + pressure(x, y - 1) + pressure(x, y + 1) - divergence) / 4.f; + // } + // } + + // for (int y = 1; y + 1 < N; ++y) + // { + // for (int x = 1; x + 1 < N; ++x) + // { + // math::vector pressure_gradient + // { + // (pressure(x + 1, y) - pressure(x - 1, y)) * 0.5f, + // (pressure(x, y + 1) - pressure(x, y - 1)) * 0.5f, + // }; + // snapshot.wind_velocity(x, y) = initial_random_wind_velocity(x, y) - pressure_gradient; + + // if (terrain(x, y) > 0.f) + // snapshot.wind_velocity(x, y) *= std::exp(- 2.f * terrain(x, y)); + // } + // } + + ++solve_velocity_iterations; + if (solve_velocity_iterations == max_solve_velocity_iterations) + ++stage; + return; + } + + if (stage == 1) + { + for (int y = 0; y < N; ++y) + { + for (int x = 0; x < N; ++x) + { + float step_time = 100.f; + + math::point p{x + 0.5f, y + 0.5f}; + float advection = sample_bilinear(snapshot.temperature, p - snapshot.wind_velocity(x, y) * step_time); + + float heating_speed = (terrain(x, y) < 0.f) ? 0.001f : 0.0001f; + float heating_delta = (expected_temperature(y) - snapshot.temperature(x, y)) * (- std::expm1(- heating_speed * step_time)); + + new_temperature(x, y) = heating_delta + advection; + } + } + std::swap(snapshot.temperature, new_temperature); + + for (int y = 1; y + 1 < N; ++y) + { + for (int x = 1; x + 1 < N; ++x) + { + float average = (0.f + + snapshot.temperature(x - 1, y) + + snapshot.temperature(x + 1, y) + + snapshot.temperature(x, y - 1) + + snapshot.temperature(x, y + 1) + ) * 0.25f; + + new_temperature(x, y) = math::lerp(snapshot.temperature(x, y), average, 0.5f); + } + } + std::swap(snapshot.temperature, new_temperature); + + ++solve_temperature_iterations; + if (solve_temperature_iterations >= max_solve_temperature_iterations) + ++stage; + return; + } + + if (stage == 2) + { + for (int y = 0; y < N; ++y) + { + for (int x = 0; x < N; ++x) + { + float step_time = 100.f; + + math::point p{x + 0.5f, y + 0.5f}; + float advection = sample_bilinear(snapshot.humidity, p - snapshot.wind_velocity(x, y) * step_time); + + float income = 0.f; + if (terrain(x, y) < 0.f) + income = step_time * 0.00012f; + else + // income = snapshot.humidity(x, y) * std::expm1(- 0.00006f * step_time); + income = -std::min(snapshot.humidity(x, y), step_time * 0.00001f); + + // float evaporation_speed = 0.0001f; + // float evaporation_delta = (expected_humidity(x, y) - snapshot.humidity(x, y)) * (- std::expm1(- evaporation_speed * step_time)); + + new_humidity(x, y) = income + advection; + } + } + std::swap(snapshot.humidity, new_humidity); + + for (int y = 1; y + 1 < N; ++y) + { + for (int x = 1; x + 1 < N; ++x) + { + float average = (0.f + + snapshot.humidity(x - 1, y) + + snapshot.humidity(x + 1, y) + + snapshot.humidity(x, y - 1) + + snapshot.humidity(x, y + 1) + ) * 0.25f; + + new_humidity(x, y) = math::lerp(snapshot.humidity(x, y), average, 0.5f); + } + } + std::swap(snapshot.humidity, new_humidity); + + ++solve_humidity_iterations; + if (solve_humidity_iterations >= max_solve_humidity_iterations) + { + ++stage; + finished = true; + } + return; + } + } +}; + +struct river_vertex +{ + // Empty for river deltas + std::optional> parent; + std::vector> children = {}; + float flow = 0.f; +}; + +struct river_net +{ + util::ndarray, 2> vertices; + util::ndarray water; + + std::vector> queue; +}; + +struct weather_app + : app::application_base +{ + random::generator rng{random::device{}}; + // random::generator rng{0, 0}; + + gfx::painter painter; + + math::box simulation_box; + + bool paused = true; + bool show_velocity = false; + bool show_temperature = false; + bool show_humidity = false; + bool show_biomes = true; + bool show_rivers = true; + bool show_clouds = true; + + gfx::pixmap_rgba biomes_map; + util::ndarray terrain; + + // 0 - Winter + // 1 - Spring + // 2 - Summer + // 3 - Autumn + int season = 0; + + // 0 - Night + // 1 - Day + int day_night = 0; + + util::ndarray, 2> base_wind_velocity; + util::ndarray base_wind_velocity_potential; + + climate_snapshot snapshots[4][2]; + climate_snapshot average; + climate_snapshot display_snapshot; + + std::optional solver; + + river_net rivers; + + util::ndarray cloud_density; + util::ndarray new_cloud_density; + + struct cloud_particle + { + math::point position; + int lifetime; + }; + + std::vector cloud_particles; + + float display_season = 0.f; + + util::clock<> frame_clock; + + weather_app(options const &, context const & context) + { + simulation_box = {{{0.f, N}, {0.f, N}}}; + + terrain.resize({N, N}, 0.f); + auto heightmap = gfx::read_image(io::file_istream{std::filesystem::path{PSEMEK_EXAMPLES_DIR} / "heightmap_seed_1.png"}); + for (int y = 0; y < N; ++y) + for (int x = 0; x < N; ++x) + terrain(x, y) = ((heightmap(x, y) / 255.f) * 2048.f - 512.f) / 1024.f; + + biomes_map = gfx::read_image(io::file_istream{std::filesystem::path{PSEMEK_EXAMPLES_DIR} / "biomes.png"}); + + // base_wind_velocity.resize({N, N}, math::vector{0.f, 0.f}); + // add_random_vector_field(rng, base_wind_velocity, 3, 6, 0.003f); + + base_wind_velocity_potential.resize({N, N}, 0.f); + add_random_scalar_field(rng, base_wind_velocity_potential, 1, 3, 0.6f); + + cloud_density.resize({N, N}, 0.f); + new_cloud_density.resize({N, N}, 0.f); + + context.vsync(true); + } + + void on_event(app::key_event const & event) override + { + application_base::on_event(event); + + if (event.down && event.key == app::keycode::SPACE) + paused ^= true; + + if (event.down && event.key == app::keycode::V) + show_velocity ^= true; + + if (event.down && event.key == app::keycode::T) + show_temperature ^= true; + + if (event.down && event.key == app::keycode::W) + show_humidity ^= true; + + if (event.down && event.key == app::keycode::B) + show_biomes ^= true; + + if (event.down && event.key == app::keycode::R) + show_rivers ^= true; + + if (event.down && event.key == app::keycode::C) + show_clouds ^= true; + } + + void update() override + { + float const dt = frame_clock.restart().count(); + + bool need_update_display_snapshot = false; + + if (solver && solver->finished) + { + solver.reset(); + ++day_night; + if (day_night == 2) + { + day_night = 0; + ++season; + } + + if (season == 4) + { + compute_average(); + init_river_deltas(); + need_update_display_snapshot = true; + } + } + + if (state().key_down.contains(app::keycode::LEFT)) + { + display_season -= 0.5f * dt; + need_update_display_snapshot = true; + } + + if (state().key_down.contains(app::keycode::RIGHT)) + { + display_season += 0.5f * dt; + need_update_display_snapshot = true; + } + + while (display_season < 0.f) + display_season += 1.f; + while (display_season >= 1.f) + display_season -= 1.f; + + if (season == 4 && need_update_display_snapshot) + { + update_display_snapshot(); + need_update_display_snapshot = false; + } + + if (!solver && season < 4) + solver.emplace(terrain, rng, season, day_night, snapshots[season][day_night], base_wind_velocity_potential); + + if (!paused) + { + if (solver) + { + for (int i = 0; i < 64; ++i) + // for (int i = 0; i < 1; ++i) + { + solver->step(); + if (solver->finished) + break; + } + } + else if (season == 4 && !rivers.queue.empty()) + { + for (int i = 0; i < 64; ++i) + { + propagate_rivers(); + if (rivers.queue.empty()) + { + compute_river_flow(); + paused = true; + break; + } + } + } + else + { + float const dt = 10.f; + + // if (false) + // for (int y = 0; y < N; ++y) + // { + // for (int x = 0; x < N; ++x) + // { + // new_cloud_density(x, y) = sample_bilinear(cloud_density, math::point{x + 0.5f, y + 0.5f} - display_snapshot.wind_velocity(x, y) * dt); + // } + // } + + // for (int y = 0; y < N; ++y) + // { + // for (int x = 0; x < N; ++x) + // { + // new_cloud_density(x, y) += (terrain(x, y) < 0.f ? 0.0001f : 0.f) * dt; + // } + // } + + // // if (false) + // for (int y = 1; y + 1 < N; ++y) + // { + // for (int x = 1; x + 1 < N; ++x) + // { + // auto gradient = math::vector { + // (new_cloud_density(x + 1, y) - new_cloud_density(x - 1, y)) / 2.f, + // (new_cloud_density(x, y + 1) - new_cloud_density(x, y - 1)) / 2.f, + // }; + + // if (math::length(gradient) > 0.f) + // cloud_density(x, y) = sample_bilinear(new_cloud_density, math::point{x + 0.5f, y + 0.5f} - math::normalized(gradient) * 1.f); + // else + // cloud_density(x, y) = new_cloud_density(x, y); + // } + // } + + // std::swap(new_cloud_density, cloud_density); + + int total_cloud_particles = N * N / 8; + + auto new_cloud_particle = [&] + { + while (true) + { + cloud_particle p{{random::uniform(rng, 0.f, N), random::uniform(rng, 0.f, N)}, 0}; + if (sample_bilinear(terrain, p.position) < 0.f) + return p; + } + }; + + if (cloud_particles.size() < total_cloud_particles) + for (int i = 0; i < 16; ++i) + cloud_particles.push_back(new_cloud_particle()); + + util::ndarray, 2> cloud_particle_index({N, N}); + + for (auto & p : cloud_particles) + { + p.position += sample_bilinear(display_snapshot.wind_velocity, p.position) * dt; + + p.lifetime += 1; + + auto r = math::distance_sqr(p.position, math::point{N/2.f, N/2.f}) / math::sqr(N/2.f); + auto t = std::min(1.f, 2.f * std::exp(- 1.f * r)); + + if (p.lifetime >= 1024 || p.position[0] < 0.f || p.position[0] > N || p.position[1] < 0.f || p.position[1] > N || (p.lifetime > 16 && random::uniform(rng) > t)) + { + p = new_cloud_particle(); + } + + cloud_particle_index(std::min(N-1, std::floor(p.position[0])), std::min(N-1, std::floor(p.position[1]))).push_back(&p); + } + + for (auto & p : cloud_particles) + { + int x = std::min(N-1, std::floor(p.position[0])); + int y = std::min(N-1, std::floor(p.position[1])); + + for (int tx = -1; tx <= 1; ++tx) + { + for (int ty = -1; ty <= 1; ++ty) + { + int xx = x + tx; + int yy = y + ty; + if (xx < 0 || xx + 1 >= N || yy < 0 || yy + 1 >= N) continue; + + for (auto q : cloud_particle_index(xx, yy)) + { + if (q == &p) continue; + auto r = p.position - q->position; + if (auto l = math::length(r); l < 1.f) + { + auto n = r / l; + p.position += n * dt * 0.02f; + } + } + } + } + } + } + } + } + + void compute_average() + { + average.wind_velocity.resize({N, N}, math::vector{0.f, 0.f}); + average.temperature.resize({N, N}, 0.f); + average.humidity.resize({N, N}, 0.f); + + for (auto const & season : snapshots) + for (auto const & s : season) + { + add(average.wind_velocity, s.wind_velocity); + add(average.temperature, s.temperature); + add(average.humidity, s.humidity); + } + + scale(average.wind_velocity, 1.f / 8.f); + scale(average.temperature, 1.f / 8.f); + scale(average.humidity, 1.f / 8.f); + } + + void init_river_deltas() + { + rivers.vertices.resize({N, N}); + rivers.water.resize({N, N}, false); + + for (int y = 1; y + 1 < N; ++y) + { + for (int x = 1; x + 1 < N; ++x) + { + int land = 0; + land += (terrain(x - 1, y - 1) >= 0.f ? 1 : 0); + land += (terrain(x + 0, y - 1) >= 0.f ? 1 : 0); + land += (terrain(x - 1, y + 0) >= 0.f ? 1 : 0); + land += (terrain(x + 0, y + 0) >= 0.f ? 1 : 0); + + if (land > 0 && land < 4) + { + rivers.queue.push_back({x, y}); + rivers.vertices(x, y).emplace(); + } + + if (land < 4) + rivers.water(x, y) = true; + } + } + } + + void propagate_rivers() + { + auto i = random::uniform(rng, 0, rivers.queue.size() - 1); + if (i + 1 < rivers.queue.size()) + std::swap(rivers.queue[i], rivers.queue.back()); + + auto p = rivers.queue.back(); + + math::vector const neighbours[4] + { + {-1, 0}, + { 1, 0}, + { 0, -1}, + { 0, 1}, + }; + + float height = sample_bilinear(terrain, math::cast(p)); + + std::vector> candidates; + for (auto n : neighbours) + { + auto q = p + n; + if (q[0] == 0 || q[0] == N || q[1] == 0 || q[1] == N) + continue; + + if (rivers.water(q[0], q[1])) + continue; + + if (rivers.vertices(q[0], q[1])) + continue; + + float nheight = sample_bilinear(terrain, math::cast(q)); + + if (nheight + 0.02f < height) + continue; + + candidates.push_back(q); + } + + if (candidates.size() <= 1) + rivers.queue.pop_back(); + + if (candidates.empty()) + { + // TODO: remove if no parent and not going anywhere? + return; + } + + auto next = random::uniform_from(rng, candidates); + + rivers.vertices(p[0], p[1])->children.push_back(next); + rivers.vertices(next[0], next[1]) = river_vertex{.parent = p}; + rivers.queue.push_back(next); + } + + void compute_river_flow() + { + for (int y = 0; y < N; ++y) + { + for (int x = 0; x < N; ++x) + { + if (auto & v = rivers.vertices(x, y); v && !v->parent) + { + compute_river_flow_at({x, y}); + } + } + } + } + + float compute_river_flow_at(math::point const & p) + { + auto & v = rivers.vertices(p[0], p[1]); + if (!v) return 0.f; + + if (v->children.empty()) + { + v->flow = std::max(0.f, sample_bilinear(average.humidity, math::cast(p)) - 0.05f); + } + else + { + for (auto c : v->children) + v->flow += compute_river_flow_at(c); + } + + return v->flow; + } + + void update_display_snapshot() + { + display_snapshot.wind_velocity.resize({N, N}, math::vector{0.f, 0.f}); + display_snapshot.temperature.resize({N, N}, 0.f); + display_snapshot.humidity.resize({N, N}, 0.f); + + int i = std::min(3, std::floor(4.f * display_season)); + int j = (i + 1) % 4; + float t = (4.f * display_season) - i; + + auto & s0 = snapshots[i][0]; + auto & s1 = snapshots[j][0]; + + lerp(display_snapshot.wind_velocity, s0.wind_velocity, s1.wind_velocity, t); + lerp(display_snapshot.temperature, s0.temperature, s1.temperature, t); + lerp(display_snapshot.humidity, s0.humidity, s1.humidity, t); + } + + void present() override + { + gl::ClearColor(0.f, 0.f, 0.f, 0.f); + gl::Clear(gl::COLOR_BUFFER_BIT); + + float const aspect_ratio = state().size[0] * 1.f / state().size[1]; + math::box view_box = math::expand(simulation_box, 1.f); + if (view_box[0].length() / view_box[1].length() > aspect_ratio) + view_box[1] = math::expand(view_box[1], (view_box[0].length() / aspect_ratio - view_box[1].length()) / 2.f); + else + view_box[0] = math::expand(view_box[0], (view_box[1].length() * aspect_ratio - view_box[0].length()) / 2.f); + + std::optional> mouseover_cell; + { + auto mouse = math::lerp(view_box, math::vector{state().mouse[0] * 1.f / state().size[0], 1.f - state().mouse[1] * 1.f / state().size[1]}); + int x = std::floor(mouse[0]); + int y = std::floor(mouse[1]); + + if (x >= 0 && x < N && y >= 0 && y < N) + mouseover_cell = {x, y}; + } + + [[maybe_unused]] float const pixel_size = view_box[0].length() / state().size[0]; + + auto map_color = [](float value, gfx::color_4f const & negative, gfx::color_4f const & positive){ + return math::lerp(negative, positive, 1.f / (1.f + std::exp(- value))); + }; + + auto map_color_positive = [](float value, gfx::color_4f const & zero, gfx::color_4f const & positive){ + return math::lerp(zero, positive, 2.f / (1.f + std::exp(- value)) - 1.f); + }; + + auto map_temperature = [&](float temperature) + { + return map_color(0.5f * std::floor(temperature / 10.f), {0.125f, 0.5f, 1.f, 0.75f}, {1.f, 0.5f, 0.125f, 0.75f}); + }; + + auto map_humidity = [&](float humidity) + { + return map_color_positive(std::round(10.f * humidity), {0.f, 1.f, 1.f, 0.f}, {0.f, 1.f, 1.f, 0.75f}); + }; + + auto map_biome = [this](float temperature, float precipitation) + { + auto x = math::clamp(math::unlerp({0.f, 0.5f}, precipitation) * biomes_map.width() , {0, biomes_map.width() - 1}); + auto y = math::clamp(math::unlerp({-10.f, 30.f}, temperature ) * biomes_map.height(), {0, biomes_map.height() - 1}); + + return gfx::to_colorf(biomes_map(x, y)); + }; + + auto & avg_snapshot = (season < 4) ? snapshots[season][day_night] : average; + auto & snapshot = (season < 4) ? snapshots[season][day_night] : display_snapshot; + + for (int y = 0; y < N; ++y) + { + for (int x = 0; x < N; ++x) + { + gfx::color_4f color = gfx::color_4f::zero(); + + bool const is_water = terrain(x, y) < 0.f; + bool const is_land = !is_water; + + if (is_water) + color = map_color(8.f * terrain(x, y), {0.f, 0.f, 0.125f, 1.f}, {0.f, 1.f, 1.5f, 1.f}); + else if (show_biomes) + { + float temperature = avg_snapshot.temperature(x, y) - std::max(0.f, terrain(x, y)) * 30.f; + color = map_biome(temperature, avg_snapshot.humidity(x, y)); + } + else + color = gfx::color_4f{0.3f, 0.5f, 0.1f, 1.f}; + + if (is_land && x > 0 && x + 1 < N && y > 0 && y + 1 < N) + { + math::vector terrain_gradient + { + (terrain(x + 1, y) - terrain(x - 1, y)) / 2.f, + (terrain(x, y + 1) - terrain(x, y - 1)) / 2.f, + }; + + auto terrain_normal = math::normalized(math::vector{-terrain_gradient[0], -terrain_gradient[1], 0.125f}); + + float lightness = 0.5f + 0.5f * math::dot(terrain_normal, math::normalized(math::vector{1.f, 2.f, 3.f})); + + color = gfx::dark(color, 1.f - lightness); + } + + if (show_temperature) + { + color = gfx::blend(color, map_temperature(snapshot.temperature(x, y))); + } + + if (show_humidity) + { + color = gfx::blend(color, map_humidity(snapshot.humidity(x, y))); + } + + // if (show_clouds) + // { + // color = gfx::blend(color, map_color_positive(cloud_density(x, y), {1.f, 1.f, 1.f, 0.f}, {1.f, 1.f, 1.f, 1.f})); + // } + + painter.rect({{{x, x + 1.f}, {y, y + 1.f}}}, gfx::to_coloru8(color)); + } + } + + if (show_velocity) + { + for (int y = 0; y < N; ++y) + { + for (int x = 0; x < N; ++x) + { + math::point center{x + 0.5f, y + 0.5f}; + auto v = snapshot.wind_velocity(x, y); + if (auto l = math::length(v); l > 0.f) + { + float const magnification = 200.f; + float const max_length = 1.5f; + v *= 0.5f * max_length * (1.f - std::exp(- magnification * l)) / l; + } + auto n = math::ort(v) * 0.3f; + + auto color = gfx::color_4f{1.f, 1.f, 1.f, 1.f}; + if (!show_temperature) + { + float t = 0.2f * snapshot.temperature(x, y); + if (t >= 0.f) + color = map_color_positive( t, color, {1.f, 0.5f, 0.125f, 1.f}); + else + color = map_color_positive(-t, color, {0.125f, 0.5f, 1.f, 1.f}); + } + + painter.triangle(center - v - n, center - v + n, center + v, gfx::to_coloru8(color)); + } + } + } + + if (show_rivers) + { + for (auto const & p : rivers.queue) + { + painter.circle(math::cast(p), 3.f * pixel_size, {0, 255, 255, 255}, 12); + } + + if (!rivers.vertices.empty()) + for (int y = 0; y < N; ++y) + { + for (int x = 0; x < N; ++x) + { + auto & v = rivers.vertices(x, y); + if (!v || !v->parent) continue; + + float width = 2.f; + if (rivers.queue.empty()) + width = 2.f * std::sqrt(v->flow); + + painter.line({x, y}, math::cast(*v->parent), width * pixel_size, {0, 255, 255, 255}, false); + } + } + } + + if (show_clouds) + { + for (auto const & p : cloud_particles) + painter.circle(p.position, 2.f, {255, 255, 255, std::min(255, p.lifetime)}, {255, 255, 255, 0}, 12); + } + + int row = 0; + auto push_text = [&](std::string const & text) mutable + { + painter.text(view_box.corner(0.f, 1.f) - math::vector{0.f, row * pixel_size * 2.f * 12.f}, text, {.scale = {2.f * pixel_size, - 2.f * pixel_size}, .x = gfx::painter::x_align::left, .y = gfx::painter::y_align::top, .c = {255, 255, 255, 255}}); + ++row; + }; + + if (solver) + { + push_text(std::format("{} {}", season_str(season), day_night_str(day_night))); + + if (solver->stage >= 0) + push_text(std::format("Velocity {}/{}", solver->solve_velocity_iterations, solver->max_solve_velocity_iterations)); + else + push_text(""); + + if (solver->stage >= 1) + push_text(std::format("Temperature {}/{}", solver->solve_temperature_iterations, solver->max_solve_temperature_iterations)); + else + push_text(""); + + if (solver->stage >= 2) + push_text(std::format("Humidity {}/{}", solver->solve_humidity_iterations, solver->max_solve_humidity_iterations)); + else + push_text(""); + + push_text(""); + } + + if (mouseover_cell) + { + int x = (*mouseover_cell)[0]; + int y = (*mouseover_cell)[1]; + painter.rect({{{x, x + 1.f}, {y, y + 1.f}}}, {255, 255, 255, 127}); + + push_text(std::format("{} {}", x, y)); + push_text(std::format("V = {:.3f}", length(snapshot.wind_velocity(x, y)) * 1000.f)); + push_text(std::format("H = {:.3f}", terrain(x, y) * 1024.f)); + push_text(std::format("T = {:.3f}", snapshot.temperature(x, y))); + push_text(std::format("W = {:.3f}", snapshot.humidity(x, y))); + push_text(""); + } + + if (season == 4) + { + ++row; + + float axis_width = 300.f; + float axis_height = 24.f; + + float axis_left = view_box[0].min + 30.f * pixel_size; + float axis_bottom = view_box[1].max - row * pixel_size * 2.f * 12.f - axis_height * pixel_size; + + auto line = [&](float x0, float y0, float x1, float y1, float w, gfx::color_rgba const & c) + { + painter.line( + {axis_left + x0 * axis_width * pixel_size, axis_bottom + y0 * axis_height * pixel_size}, + {axis_left + x1 * axis_width * pixel_size, axis_bottom + y1 * axis_height * pixel_size}, + pixel_size * w, c, false + ); + }; + + line(0.f, 0.5f, 1.f, 0.5f, 2.f, {255, 255, 255, 127}); + line(display_season, 0.f, display_season, 1.f, 4.f, {255, 255, 255, 255}); + + ++row; + ++row; + } + + if (mouseover_cell && season == 4) + { + int x = (*mouseover_cell)[0]; + int y = (*mouseover_cell)[1]; + + float plot_width = 300.f; + float plot_height = 150.f; + + float plot_left = view_box[0].min + 30.f * pixel_size; + float plot_bottom = view_box[1].max - row * pixel_size * 2.f * 12.f - plot_height * 1.5f * pixel_size; + + auto line = [&](float x0, float y0, float x1, float y1, gfx::color_rgba const & c) + { + painter.line( + {plot_left + x0 * plot_width * pixel_size, plot_bottom + y0 * plot_height * pixel_size}, + {plot_left + x1 * plot_width * pixel_size, plot_bottom + y1 * plot_height * pixel_size}, + pixel_size * 2.f, c, false + ); + }; + + auto begin_plot = [&]{ + painter.rect({{{plot_left, plot_left + plot_width * pixel_size}, {plot_bottom, plot_bottom + plot_height * pixel_size}}}, {255, 255, 255, 31}); + + for (int x = 0; x <= 4; ++x) + line(x / 4.f, 0.f, x / 4.f, 1.f, {255, 255, 255, (x == 0 || x == 4) ? 127 : 31}); + }; + + auto draw_plot = [&](std::vector const & ys, gfx::color_rgba const & c) + { + std::vector xs(5); + for (int i = 0; i <= 4; ++i) + xs[i] = i / 4.f; + + auto system = math::matrix::zero(); + auto coeffs = math::vector::zero(); + + for (int i = 0; i < 4; ++i) + { + float x0 = xs[i]; + system[2 * i + 0][4 * i + 0] = 1.f; + system[2 * i + 0][4 * i + 1] = x0; + system[2 * i + 0][4 * i + 2] = x0 * x0; + system[2 * i + 0][4 * i + 3] = x0 * x0 * x0; + coeffs[2 * i + 0] = ys[i]; + + float x1 = xs[i + 1]; + system[2 * i + 1][4 * i + 0] = 1.f; + system[2 * i + 1][4 * i + 1] = x1; + system[2 * i + 1][4 * i + 2] = x1 * x1; + system[2 * i + 1][4 * i + 3] = x1 * x1 * x1; + coeffs[2 * i + 1] = ys[i + 1]; + } + + for (int i = 0; i < 4; ++i) + { + int j = (i + 1) % 4; + float x0 = xs[i + 1]; + float x1 = xs[j]; + + system[8 + 2 * i + 0][4 * i + 1] = 1.f; + system[8 + 2 * i + 0][4 * i + 2] = 2.f * x0; + system[8 + 2 * i + 0][4 * i + 3] = 3.f * x0 * x0; + system[8 + 2 * i + 0][4 * j + 1] = - 1.f; + system[8 + 2 * i + 0][4 * j + 2] = - 2.f * x1; + system[8 + 2 * i + 0][4 * j + 3] = - 3.f * x1 * x1; + + system[8 + 2 * i + 1][4 * i + 2] = 2.f; + system[8 + 2 * i + 1][4 * i + 3] = 6.f * x0; + system[8 + 2 * i + 1][4 * j + 2] = - 2.f; + system[8 + 2 * i + 1][4 * j + 3] = - 6.f * x1; + } + + math::gauss(system, coeffs); + + auto eval = [&](int i, float x) + { + return 0.f + + coeffs[4 * i + 0] + + coeffs[4 * i + 1] * x + + coeffs[4 * i + 2] * x * x + + coeffs[4 * i + 3] * x * x * x + ; + }; + + for (int i = 0; i < 4; ++i) + { + int D = 8; + for (int j = 0; j < D; ++j) + { + float x0 = (i + (j + 0.f) / D) / 4.f; + float x1 = (i + (j + 1.f) / D) / 4.f; + line(x0, eval(i, x0), x1, eval(i, x1), c); + } + + if (i == std::min(3, std::floor(4.f * display_season))) + { + float x = display_season; + float y = eval(i, x); + painter.circle({plot_left + x * plot_width * pixel_size, plot_bottom + y * plot_height * pixel_size}, 4.f * pixel_size, c); + } + } + }; + + // Temperature + { + begin_plot(); + + for (int y = 0; y <= 10; ++y) + { + line(0.f, y / 10.f, 1.f, y / 10.f, {255, 255, 255, (y == 5) ? 127 : 31}); + } + + std::vector ys[2]; + + for (int d = 0; d < 2; ++d) + { + for (int s = 0; s <= 4; ++s) + { + float value = 0.5f + snapshots[s % 4][d].temperature(x, y) / 100.f; + ys[d].push_back(value); + } + } + + draw_plot(ys[0], gfx::color_rgba{0, 127, 255, 255}); + draw_plot(ys[1], gfx::color_rgba{255, 127, 0, 255}); + } + + plot_bottom -= (plot_height + 50.f) * pixel_size; + + // Humidity + { + begin_plot(); + + for (int y = 0; y <= 5; ++y) + { + line(0.f, y / 5.f, 1.f, y / 5.f, {255, 255, 255, (y == 0) ? 127 : 31}); + } + + std::vector ys; + + for (int s = 0; s <= 4; ++s) + { + float value = (snapshots[s % 4][0].humidity(x, y) + snapshots[s % 4][1].humidity(x, y)) / 2.f; + ys.push_back(value); + } + + draw_plot(ys, {0, 255, 255, 255}); + } + } + + painter.render(math::orthographic_camera{view_box}.transform()); + } +}; + +namespace psemek::app +{ + + std::unique_ptr make_application_factory() + { + return default_application_factory({.name = "Weather simulation test"}); + } + +} diff --git a/libs/app/include/psemek/app/application.hpp b/libs/app/include/psemek/app/application.hpp index b9cd1f36..544e0dd0 100644 --- a/libs/app/include/psemek/app/application.hpp +++ b/libs/app/include/psemek/app/application.hpp @@ -3,6 +3,7 @@ #include #if defined(PSEMEK_GRAPHICS_API_WEBGPU) +#include #include #include #include @@ -24,9 +25,9 @@ namespace psemek::app int multisampling = 4; bool highdpi = false; #if defined(PSEMEK_GRAPHICS_API_WEBGPU) - std::vector required_features; - std::optional required_limits; - std::optional required_native_limits; + std::vector required_features = {}; + std::optional required_limits = {}; + std::optional required_native_limits = {}; #endif }; @@ -37,10 +38,12 @@ namespace psemek::app std::function show_cursor; std::function relative_mouse_mode; std::function windowed; + std::function text_input; #if defined(PSEMEK_GRAPHICS_API_OPENGL) std::function vsync; #endif #if defined(PSEMEK_GRAPHICS_API_WEBGPU) + wgpu::instance instance; wgpu::adapter adapter; wgpu::surface surface; wgpu::device device; diff --git a/libs/app/include/psemek/app/application_base.hpp b/libs/app/include/psemek/app/application_base.hpp index 2dfa0448..637e22a1 100644 --- a/libs/app/include/psemek/app/application_base.hpp +++ b/libs/app/include/psemek/app/application_base.hpp @@ -18,6 +18,7 @@ namespace psemek::app void on_event(touch_up_event const &) override; void on_event(touch_move_event const &) override; void on_event(key_event const &) override; + void on_event(text_input_event const &) override; void stop() override; bool running() const override; diff --git a/libs/app/include/psemek/app/event_handler.hpp b/libs/app/include/psemek/app/event_handler.hpp index 3094555a..913d708a 100644 --- a/libs/app/include/psemek/app/event_handler.hpp +++ b/libs/app/include/psemek/app/event_handler.hpp @@ -16,6 +16,7 @@ namespace psemek::app virtual void on_event(touch_up_event const &) {} virtual void on_event(touch_move_event const &) {} virtual void on_event(key_event const &) {} + virtual void on_event(text_input_event const &) {} virtual ~event_handler() {} }; diff --git a/libs/app/include/psemek/app/event_state.hpp b/libs/app/include/psemek/app/event_state.hpp index 62526d71..387e9fa2 100644 --- a/libs/app/include/psemek/app/event_state.hpp +++ b/libs/app/include/psemek/app/event_state.hpp @@ -61,4 +61,7 @@ namespace psemek::app state.key_down.erase(event.key); } + inline void apply(event_state &, text_input_event const &) + {} + } diff --git a/libs/app/include/psemek/app/events.hpp b/libs/app/include/psemek/app/events.hpp index e3e21ade..ea0fe495 100644 --- a/libs/app/include/psemek/app/events.hpp +++ b/libs/app/include/psemek/app/events.hpp @@ -2,6 +2,8 @@ #include +#include + namespace psemek::app { @@ -172,4 +174,9 @@ namespace psemek::app bool down; }; + struct text_input_event + { + std::string input; + }; + } diff --git a/libs/app/include/psemek/app/scene_application.hpp b/libs/app/include/psemek/app/scene_application.hpp index 129af5a9..84f37eed 100644 --- a/libs/app/include/psemek/app/scene_application.hpp +++ b/libs/app/include/psemek/app/scene_application.hpp @@ -18,6 +18,7 @@ namespace psemek::app void on_event(touch_up_event const &) override; void on_event(touch_move_event const &) override; void on_event(key_event const &) override; + void on_event(text_input_event const &) override; void stop() override; diff --git a/libs/app/source/application_base.cpp b/libs/app/source/application_base.cpp index a6352ac9..53b56213 100644 --- a/libs/app/source/application_base.cpp +++ b/libs/app/source/application_base.cpp @@ -48,6 +48,9 @@ namespace psemek::app apply(state_, event); } + void application_base::on_event(text_input_event const &) + {} + void application_base::stop() { running_ = false; diff --git a/libs/app/source/scene_application.cpp b/libs/app/source/scene_application.cpp index 5f18715d..0936bec2 100644 --- a/libs/app/source/scene_application.cpp +++ b/libs/app/source/scene_application.cpp @@ -48,6 +48,11 @@ namespace psemek::app on_event_impl(event); } + void scene_application::on_event(text_input_event const & event) + { + on_event_impl(event); + } + void scene_application::stop() { if (auto scene = current_scene()) diff --git a/libs/async/CMakeLists.txt b/libs/async/CMakeLists.txt index d911e5fc..97977802 100644 --- a/libs/async/CMakeLists.txt +++ b/libs/async/CMakeLists.txt @@ -3,4 +3,8 @@ file(GLOB_RECURSE PSEMEK_ASYNC_SOURCES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "s psemek_add_library(psemek-async ${PSEMEK_ASYNC_HEADERS} ${PSEMEK_ASYNC_SOURCES}) target_include_directories(psemek-async PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") -target_link_libraries(psemek-async PUBLIC psemek-log psemek-util) +target_link_libraries(psemek-async PUBLIC + psemek-log + psemek-util + psemek-prof +) diff --git a/libs/async/include/psemek/async/event_loop.hpp b/libs/async/include/psemek/async/event_loop.hpp index 35b7d9db..f19ca05e 100644 --- a/libs/async/include/psemek/async/event_loop.hpp +++ b/libs/async/include/psemek/async/event_loop.hpp @@ -17,6 +17,8 @@ namespace psemek::async void stop() override; + void clear() override; + void wait() override; void wait_for(clock::duration period) override; diff --git a/libs/async/include/psemek/async/executor.hpp b/libs/async/include/psemek/async/executor.hpp index 74fab0a5..ae7a1348 100644 --- a/libs/async/include/psemek/async/executor.hpp +++ b/libs/async/include/psemek/async/executor.hpp @@ -4,7 +4,6 @@ #include #include -#include namespace psemek::async { @@ -30,6 +29,9 @@ namespace psemek::async // NB: the executor must call stop() from destructor. virtual void stop() = 0; + // Clear the pending tasks queue. + virtual void clear() = 0; + // Wait for all the tasks to be executed. // May take forever. virtual void wait() = 0; diff --git a/libs/async/include/psemek/async/future.hpp b/libs/async/include/psemek/async/future.hpp index e6863889..ca1cda75 100644 --- a/libs/async/include/psemek/async/future.hpp +++ b/libs/async/include/psemek/async/future.hpp @@ -40,6 +40,18 @@ namespace psemek::async using type = util::function; }; + template + struct future_result_type + { + using type = T &; + }; + + template <> + struct future_result_type + { + using type = void; + }; + struct cancel_token {}; @@ -55,7 +67,8 @@ namespace psemek::async std::weak_ptr weak_cancel; std::mutex then_mutex; - typename then_function::type then_func; + std::vector::type> then_funcs; + std::vector> then_exception_funcs; }; } @@ -85,14 +98,16 @@ namespace psemek::async using result_type = T; future() = default; - future(future&&) = default; + future(future const &) = default; + future(future &&) = default; future(std::shared_ptr> state, std::shared_ptr cancel) : state_(std::move(state)) , cancel_(std::move(cancel)) {} - future & operator = (future&&) = default; + future & operator = (future const &) = default; + future & operator = (future &&) = default; ~future() { @@ -136,7 +151,7 @@ namespace psemek::async return has_value_unsafe(); } - T get() + typename detail::future_result_type::type get() { if (!state_) throw empty_future_error{}; @@ -148,7 +163,7 @@ namespace psemek::async if constexpr (std::is_same_v) return; else - return std::move(*(state_->value)); + return *(state_->value); } else std::rethrow_exception(state_->exception); @@ -239,8 +254,10 @@ namespace psemek::async state_->value = value; } state_->value_cv.notify_all(); - if (state_->then_func) - state_->then_func(*state_->value); + + std::lock_guard lock{state_->then_mutex}; + for (auto & func : state_->then_funcs) + func(*state_->value); } void set_value(T && value) @@ -252,8 +269,10 @@ namespace psemek::async state_->value = std::move(value); } state_->value_cv.notify_all(); - if (state_->then_func) - state_->then_func(*state_->value); + + std::lock_guard lock{state_->then_mutex}; + for (auto & func : state_->then_funcs) + func(*state_->value); } void set_exception(std::exception_ptr e) @@ -263,6 +282,10 @@ namespace psemek::async std::lock_guard lock{state_->value_mutex}; if (state_->value || state_->exception) throw satisfied_promise_error{}; state_->exception = std::move(e); + + std::lock_guard lock_then{state_->then_mutex}; + for (auto & func : state_->then_exception_funcs) + func(state_->exception); } state_->value_cv.notify_all(); } @@ -324,8 +347,10 @@ namespace psemek::async state_->value = true; } state_->value_cv.notify_all(); - if (state_->then_func) - state_->then_func(); + + std::lock_guard lock{state_->then_mutex}; + for (auto & func : state_->then_funcs) + func(); } void set_exception(std::exception_ptr e) @@ -335,6 +360,10 @@ namespace psemek::async std::lock_guard lock{state_->value_mutex}; if (state_->value || state_->exception) throw satisfied_promise_error{}; state_->exception = std::move(e); + + std::lock_guard lock_then{state_->then_mutex}; + for (auto & func : state_->then_exception_funcs) + func(state_->exception); } state_->value_cv.notify_all(); } @@ -362,6 +391,14 @@ namespace psemek::async return p.get_future(); } + template + future make_ready_future(T const & x) + { + promise p; + p.set_value(x); + return p.get_future(); + } + template struct packaged_task; @@ -488,7 +525,7 @@ namespace psemek::async auto fut = t.get_future(); std::lock_guard lock{state_->then_mutex}; - state_->then_func = std::move(t); + state_->then_funcs.push_back(std::move(t)); return fut; } else @@ -498,7 +535,7 @@ namespace psemek::async auto fut = t.get_future(); std::lock_guard lock{state_->then_mutex}; - state_->then_func = std::move(t); + state_->then_funcs.push_back(std::move(t)); return fut; } } diff --git a/libs/async/include/psemek/async/synchronous_executor.hpp b/libs/async/include/psemek/async/synchronous_executor.hpp index 45b6fa3f..cc273176 100644 --- a/libs/async/include/psemek/async/synchronous_executor.hpp +++ b/libs/async/include/psemek/async/synchronous_executor.hpp @@ -14,6 +14,8 @@ namespace psemek::async void stop() override; + void clear() override; + void wait() override; void wait_for(clock::duration period) override; diff --git a/libs/async/include/psemek/async/threadpool.hpp b/libs/async/include/psemek/async/threadpool.hpp index 381d3d1a..852349e2 100644 --- a/libs/async/include/psemek/async/threadpool.hpp +++ b/libs/async/include/psemek/async/threadpool.hpp @@ -4,7 +4,6 @@ #include #include -#include #include #include #include @@ -32,6 +31,8 @@ namespace psemek::async void stop() override; + void clear() override; + void wait() override; void wait_for(clock::duration period) override; diff --git a/libs/async/source/event_loop.cpp b/libs/async/source/event_loop.cpp index 546694c8..0fdac888 100644 --- a/libs/async/source/event_loop.cpp +++ b/libs/async/source/event_loop.cpp @@ -1,7 +1,5 @@ #include -#include - namespace psemek::async { @@ -20,6 +18,11 @@ namespace psemek::async queue_.clear(); } + void event_loop::clear() + { + queue_.clear(); + } + void event_loop::wait() { while (!queue_.empty()) diff --git a/libs/async/source/synchronous_executor.cpp b/libs/async/source/synchronous_executor.cpp index 57179ce3..1662b854 100644 --- a/libs/async/source/synchronous_executor.cpp +++ b/libs/async/source/synchronous_executor.cpp @@ -1,8 +1,6 @@ #include #include -#include - namespace psemek::async { @@ -19,6 +17,9 @@ namespace psemek::async void synchronous_executor::stop() {} + void synchronous_executor::clear() + {} + void synchronous_executor::wait() {} diff --git a/libs/async/source/threadpool.cpp b/libs/async/source/threadpool.cpp index c5b5d417..f163b935 100644 --- a/libs/async/source/threadpool.cpp +++ b/libs/async/source/threadpool.cpp @@ -3,19 +3,13 @@ #include #include #include +#include #include namespace psemek::async { - namespace - { - - struct stop_execution{}; - - } - threadpool::threadpool(std::string const & name, std::size_t thread_count) : working_count_{0} { @@ -25,10 +19,13 @@ namespace psemek::async threads_.emplace_back([this, tname = std::move(tname)]() mutable { log::thread_registrator reg(std::move(tname)); - for (bool running = true; running;) + while (true) { auto task = task_queue_.pop(); + if (!task) + break; + { std::lock_guard lock{working_count_mutex_}; ++working_count_; @@ -36,12 +33,9 @@ namespace psemek::async try { + prof::profiler prof("task"); task(); } - catch (stop_execution const &) - { - running = false; - } catch (util::exception const & e) { log::error() << "Unhandled exception in threadpool executor: " << e; @@ -86,11 +80,16 @@ namespace psemek::async for (auto const & thread: threads_) { unused(thread); - task_queue_.push([]{ throw stop_execution{}; }); + task_queue_.push(nullptr); } threads_.clear(); } + void threadpool::clear() + { + task_queue_.clear(); + } + void threadpool::wait() { std::unique_lock lock{working_count_mutex_}; diff --git a/libs/audio/include/psemek/audio/channel.hpp b/libs/audio/include/psemek/audio/channel.hpp index ad7732bf..75081f07 100644 --- a/libs/audio/include/psemek/audio/channel.hpp +++ b/libs/audio/include/psemek/audio/channel.hpp @@ -18,12 +18,12 @@ namespace psemek::audio stream_ptr stream() const { - return std::atomic_load(&stream_); + return stream_.load(); } stream_ptr stream(stream_ptr new_stream) { - return std::atomic_exchange(&stream_, std::move(new_stream)); + return stream_.exchange(std::move(new_stream)); } stream_ptr stop() @@ -37,7 +37,7 @@ namespace psemek::audio } private: - stream_ptr stream_; + std::atomic stream_; }; using channel_ptr = std::shared_ptr; diff --git a/libs/bt/CMakeLists.txt b/libs/bt/CMakeLists.txt index a03ab8c1..f39a5379 100644 --- a/libs/bt/CMakeLists.txt +++ b/libs/bt/CMakeLists.txt @@ -4,6 +4,3 @@ file(GLOB_RECURSE PSEMEK_BT_SOURCES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "sour psemek_add_library(psemek-bt ${PSEMEK_BT_HEADERS} ${PSEMEK_BT_SOURCES}) target_include_directories(psemek-bt PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") target_link_libraries(psemek-bt PUBLIC psemek-util psemek-log) -if(PSEMEK_BT_LOG) - target_compile_definitions(psemek-bt PUBLIC -DPSEMEK_BT_LOG=1) -endif() diff --git a/libs/bt/include/psemek/bt/conditional.hpp b/libs/bt/include/psemek/bt/conditional.hpp index 3e4c5b23..d8a94f4d 100644 --- a/libs/bt/include/psemek/bt/conditional.hpp +++ b/libs/bt/include/psemek/bt/conditional.hpp @@ -39,10 +39,7 @@ namespace psemek::bt { if (condition_result_) { - if (*condition_result_) - return true_branch_->update(dt, args...); - else - return false_branch_->update(dt, args...); + return current_node()->update(dt, args...); } else { @@ -51,11 +48,8 @@ namespace psemek::bt if (auto finished = std::get_if(&result)) { condition_result_ = finished->result; - if (*condition_result_) - true_branch_->start(args...); - else - false_branch_->start(args...); - return running{}; + current_node()->start(args...); + return current_node()->update(Time{}, args...); } return result; @@ -71,10 +65,7 @@ namespace psemek::bt { if (condition_result_) { - if (*condition_result_) - return true_branch_->event(event, args...); - else - return false_branch_->event(event, args...); + return current_node()->event(event, args...); } else return condition_->event(event, args...); @@ -85,6 +76,11 @@ namespace psemek::bt node_ptr true_branch_; node_ptr false_branch_; std::optional condition_result_; + + node * current_node() + { + return *condition_result_ ? true_branch_.get() : false_branch_.get(); + } }; template diff --git a/libs/bt/include/psemek/bt/log.hpp b/libs/bt/include/psemek/bt/log.hpp deleted file mode 100644 index 3d785abc..00000000 --- a/libs/bt/include/psemek/bt/log.hpp +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#ifdef PSEMEK_BT_LOG -#include -#define bt_log ::psemek::log::debug -#else -#include -#define bt_log ::psemek::util::null_ostream -#endif diff --git a/libs/bt/include/psemek/bt/selector.hpp b/libs/bt/include/psemek/bt/selector.hpp index 9acdb188..9a46c4eb 100644 --- a/libs/bt/include/psemek/bt/selector.hpp +++ b/libs/bt/include/psemek/bt/selector.hpp @@ -32,7 +32,7 @@ namespace psemek::bt status update(Time dt, Args ... args) override { - if (current_index_ < children_.size()) + while (current_index_ < children_.size()) { if (!current_started_) { @@ -41,13 +41,14 @@ namespace psemek::bt } auto result = children_[current_index_]->update(dt, args...); + dt = Time{}; if (auto f = std::get_if(&result)) { if (f->result) return finished{true}; ++current_index_; current_started_ = false; - return running{}; + continue; } else return result; diff --git a/libs/bt/include/psemek/bt/sequence.hpp b/libs/bt/include/psemek/bt/sequence.hpp index 49395087..a207a441 100644 --- a/libs/bt/include/psemek/bt/sequence.hpp +++ b/libs/bt/include/psemek/bt/sequence.hpp @@ -32,7 +32,7 @@ namespace psemek::bt status update(Time dt, Args ... args) override { - if (current_index_ < children_.size()) + while (current_index_ < children_.size()) { if (!current_started_) { @@ -41,13 +41,14 @@ namespace psemek::bt } auto result = children_[current_index_]->update(dt, args...); + dt = Time{}; if (auto f = std::get_if(&result)) { if (!f->result) return finished{false}; ++current_index_; current_started_ = false; - return running{}; + continue; } else return result; diff --git a/libs/cg/include/psemek/cg/kdtree.hpp b/libs/cg/include/psemek/cg/kdtree.hpp index 13373606..93666fa6 100644 --- a/libs/cg/include/psemek/cg/kdtree.hpp +++ b/libs/cg/include/psemek/cg/kdtree.hpp @@ -77,6 +77,9 @@ namespace psemek::cg template kdtree(Iterator begin, Iterator end); + template + void assign(Iterator begin, Iterator end); + bool empty() const { return nodes_.empty(); } bool insert(value_type && value); @@ -84,11 +87,19 @@ namespace psemek::cg // TODO: implement bool remove(point_type const & point) const; + void clear(); + // TODO: implement // TODO: alternative non-const version that allows modifying value.data value_type const * find(point_type const & point) const; - value_type const & closest(point_type const & target); + value_type const & closest(point_type const & target) const; + + template + Iterator closer_than(point_type const & target, scalar_type max_distance, Iterator out) const; + + template + void closer_than_map(point_type const & target, scalar_type max_distance, Callback && callback) const; private: @@ -106,6 +117,7 @@ namespace psemek::cg { value_type value; node_id children[2] {null, null}; + std::uint32_t size = 0; }; std::vector nodes_; @@ -116,6 +128,12 @@ namespace psemek::cg bool insert_impl(value_type && value, node_id id, std::uint32_t split_axis); value_type const * closest_impl(point_type const & target, scalar_type & best_distance_sqr, node_id id, std::uint32_t split_axis) const; + + template + Iterator closer_than_impl(point_type const & target, scalar_type max_distance_sqr, Iterator out, node_id id, std::uint32_t split_axis) const; + + template + void closer_than_map_impl(point_type const & target, scalar_type max_distance_sqr, Callback & callback, node_id id, std::uint32_t split_axis) const; }; template @@ -125,6 +143,14 @@ namespace psemek::cg build_node_impl(begin, end, 0); } + template + template + void kdtree::assign(Iterator begin, Iterator end) + { + clear(); + build_node_impl(begin, end, 0); + } + template bool kdtree::insert(value_type && value) { @@ -132,7 +158,13 @@ namespace psemek::cg } template - kdtree::value_type const & kdtree::closest(point_type const & target) + void kdtree::clear() + { + nodes_.clear(); + } + + template + kdtree::value_type const & kdtree::closest(point_type const & target) const { if (nodes_.empty()) throw util::exception("empty kdtree"); @@ -141,6 +173,26 @@ namespace psemek::cg return *closest_impl(target, best_distance_sqr, 0, 0); } + template + template + Iterator kdtree::closer_than(point_type const & target, scalar_type max_distance, Iterator out) const + { + if (nodes_.empty()) + return out; + + return closer_than_impl(target, math::sqr(max_distance), out, 0, 0); + } + + template + template + void kdtree::closer_than_map(point_type const & target, scalar_type max_distance, Callback && callback) const + { + if (nodes_.empty()) + return; + + return closer_than_map_impl(target, math::sqr(max_distance), callback, 0, 0); + } + template template kdtree::node_id kdtree::build_node_impl(Iterator begin, Iterator end, std::uint32_t split_axis) @@ -160,6 +212,7 @@ namespace psemek::cg auto result = static_cast(nodes_.size()); auto & node = nodes_.emplace_back(); node.value = std::move(*middle); + node.size = (end - begin); nodes_[result].children[0] = build_node_impl(begin, middle, next_axis(split_axis)); nodes_[result].children[1] = build_node_impl(std::next(middle), end, next_axis(split_axis)); @@ -174,6 +227,7 @@ namespace psemek::cg { auto & node = nodes_.emplace_back(); node.value = std::move(value); + node.size = 1; return true; } @@ -184,7 +238,7 @@ namespace psemek::cg if (node_point == point) return false; - int child = (node.point[split_axis] < point[split_axis]) ? 0 : 1; + int child = (point[split_axis] < node_point[split_axis]) ? 0 : 1; if (node.children[child] == null) { @@ -193,10 +247,18 @@ namespace psemek::cg auto & child_node = nodes_.emplace_back(); child_node.value = std::move(value); + child_node.size = 1; + nodes_[id].size += 1; return true; } - return insert_impl(std::move(value), node.children[child], next_axis(split_axis)); + if (insert_impl(std::move(value), node.children[child], next_axis(split_axis))) + { + nodes_[id].size += 1; + return true; + } + + return false; } template @@ -224,4 +286,46 @@ namespace psemek::cg return result; } + template + template + Iterator kdtree::closer_than_impl(point_type const & target, scalar_type max_distance_sqr, Iterator out, node_id id, std::uint32_t split_axis) const + { + auto const & node = nodes_[id]; + auto const & node_point = detail::get_point(node.value); + + if (math::distance_sqr(node_point, target) < max_distance_sqr) + *out++ = node.value; + + auto delta = target[split_axis] - node_point[split_axis]; + auto delta_sqr = math::sqr(delta); + + if (node.children[0] != null && (delta < 0 || delta_sqr < max_distance_sqr)) + out = closer_than_impl(target, max_distance_sqr, out, node.children[0], next_axis(split_axis)); + + if (node.children[1] != null && (delta >= 0 || delta_sqr < max_distance_sqr)) + out = closer_than_impl(target, max_distance_sqr, out, node.children[1], next_axis(split_axis)); + + return out; + } + + template + template + void kdtree::closer_than_map_impl(point_type const & target, scalar_type max_distance_sqr, Callback & callback, node_id id, std::uint32_t split_axis) const + { + auto const & node = nodes_[id]; + auto const & node_point = detail::get_point(node.value); + + if (math::distance_sqr(node_point, target) < max_distance_sqr) + callback(node.value); + + auto delta = target[split_axis] - node_point[split_axis]; + auto delta_sqr = math::sqr(delta); + + if (node.children[0] != null && (delta < 0 || delta_sqr < max_distance_sqr)) + closer_than_map_impl(target, max_distance_sqr, callback, node.children[0], next_axis(split_axis)); + + if (node.children[1] != null && (delta >= 0 || delta_sqr < max_distance_sqr)) + closer_than_map_impl(target, max_distance_sqr, callback, node.children[1], next_axis(split_axis)); + } + } diff --git a/libs/ecs/include/psemek/ecs/container.hpp b/libs/ecs/include/psemek/ecs/container.hpp index f33ab6f1..8ecd12e7 100644 --- a/libs/ecs/include/psemek/ecs/container.hpp +++ b/libs/ecs/include/psemek/ecs/container.hpp @@ -22,11 +22,8 @@ namespace psemek::ecs // TODO: // - Fully document which functions can be called from which callbacks - // - Modification callbacks implementation (const-only) // - Tables serialization // - Refactor query caches - // - Index API - // - Index implementation struct container { @@ -82,6 +79,9 @@ namespace psemek::ecs */ void destroy(handle entity); + /** Same as destroy(), but via finally() */ + void destroy_finally(handle entity); + /** Get an accessor for an entity, which provides access to the entity's components. * It is designed to be a single-use object; it cannot be stored as a reference to * the entity. Use handle for that instead. @@ -172,6 +172,10 @@ namespace psemek::ecs template void detach(handle entity); + /** Same as detach(), but via finally() */ + template + void detach_finally(handle entity); + /** Create a query cache that can be used to speed up `apply()` calls. * * The constness of the component types is ignored. @@ -296,12 +300,7 @@ namespace psemek::ecs * The constructor function must have the same signature as a function * passed to the `apply()` call. * - * The constuctor is not considered to be a modification of the entity, i.e. it - * doesn't trigger modification callbacks. - * * @param function A function to be applied to created entity's components - * @return An ownerwhip token; destroying this token removes - * the constructor from this container * @warning If any two of the passed component types are equal, the call fails with * a compilation error * @warning If the constructor modifies the entity's archetype (i.e. attaches or @@ -335,11 +334,7 @@ namespace psemek::ecs * * Note that there is no way to cancel the entity's destruction. * - * The destructor is not considered to be a modification of the entity, i.e. it - * doesn't trigger modification callbacks. - * * @param function A function to be applied to a to-be destroyed entity's components - * @return An ownerwhip token; destroying it removes the destructor from the container * @warning If any two of the passed component types are equal, the call fails with * a compilation error * @warning If the destructor modifies the entity's archetype (i.e. attaches or @@ -349,45 +344,36 @@ namespace psemek::ecs template void destructor(Function && function); - /** Register a component modification callback. Each time an entity that has - * the specified set of components is modified, and the modification affects - * one of this callback's component types, the callback is called. + /** Call a callback after exiting all currently executing ECS container methods. * - * The component types can be const-qualified, in which case the corresponding function must - * also accept the corresponding components by a const reference. + * If no ECS container method is currently executed, call the callback immediately instead. * - * The component types can be equal to ecs::without, indicating that entities having - * this component type will not be watched by this callback. These component types are not - * included in the called function signature. + * The function must have one of the following signatures: + * void() + * void(container) * - * If the modification occurred via an accessor, the callback is called - * after the accessor is destroyed, allowing for transaction-like modification. + * This method is meant to be called from inside other callbacks (typically + * constructors and destructors) and can be used to add extra modifications + * to entities that don't clash with other callbacks and archetype changes. * - * If the modification occurred via an `apply()` or `batch_apply()` call, - * the callback is called immediately after this call. + * Generally, if a constructor, destructor, apply or batch apply function + * changes the archetype of some entities, it should do so using `finally` + * instead of doing that directly. * - * Note that modifications from within a modification callback are not considered - * as modifications, i.e. they don't recursively invoke modification callbacks. - * - * The callback function must have the same signature as a function - * passed to the `apply()` call. - * - * @param function A function to be applied to modified entity's components - * @return An ownerwhip token; destroying it removes the modification callback from the container - * @warning If any two of the passed component types are equal, the call fails with - * a compilation error - * @warning If the modification callback modifies the entity's archetype (i.e. attaches or - * detaches components), the behavior is undefined - * - * TODO: can we allow the callback to modify the archetype? + * @param function A callback to be called before the topmost currently executing + * ECS container method returns. + * @warning If a callback adds new callbacks via `finally`, those will also get executed, + * which potentially leads to an infinite loop. */ - // TODO: implement - template - void watch(Function && function); + template + void finally(Function && function); template Index & index(Args && ... args); + template + Index & index_factory(Factory && factory); + template std::size_t memory_usage(); @@ -412,10 +398,18 @@ namespace psemek::ecs util::object_pool> uuid_list_pool_; util::object_pool> uuid_set_pool_; + std::size_t method_recursion_depth_ = 0; + std::vector> finally_callbacks_; + + #ifdef PSEMEK_DEBUG + util::hash_set currently_changing_archetype_; + #endif + detail::table * insert_table(std::vector> columns); void do_destroy(handle entity); void remove_row(detail::table & table, std::uint32_t row, util::span entities); void finalize_iteration(detail::table & table); + void finalize_method(); }; template @@ -429,6 +423,8 @@ namespace psemek::ecs { static_assert(detail::all_different_types_v...>, "all component types must be different"); + ++method_recursion_depth_; + (register_component>(), ...); detail::component_uuid_helper...> uuids; @@ -449,6 +445,9 @@ namespace psemek::ecs auto row = table->row_count(); auto id = entity_list_.create(table, row); handle handle{id, entity_list_.get_entities()[id].epoch}; + #ifdef PSEMEK_DEBUG + currently_changing_archetype_.insert(handle); + #endif [[maybe_unused]] accessor accessor = get(handle); table->push_row(handle); @@ -456,6 +455,14 @@ namespace psemek::ecs table->trigger_constructors(*this, row); + #ifdef PSEMEK_DEBUG + currently_changing_archetype_.erase(handle); + #endif + + finalize_method(); + + --method_recursion_depth_; + return handle; } @@ -464,6 +471,13 @@ namespace psemek::ecs { static_assert(detail::all_different_types_v...>, "all component types must be different"); + #ifdef PSEMEK_DEBUG + assert(!currently_changing_archetype_.contains(entity)); + currently_changing_archetype_.insert(entity); + #endif + + ++method_recursion_depth_; + (register_component(), ...); auto uuids = uuid_list_pool_.get(); @@ -479,6 +493,9 @@ namespace psemek::ecs for (auto const & uuid : attached_uuid_set) uuids.push_back(uuid); + if (archetype_changed) + data->table->trigger_destructors(*this, data->row, attached_uuid_set, {}); + auto table = table_container_.get(uuids); if (!table) @@ -508,13 +525,21 @@ namespace psemek::ecs ((accessor.get>() = std::forward(components)), ...); if (archetype_changed) - table->trigger_constructors(*this, data->row, attached_uuid_set); + table->trigger_constructors(*this, data->row, attached_uuid_set, {}); attached_uuid_set.clear(); uuids.clear(); uuid_set_pool_.put(std::move(attached_uuid_set)); uuid_list_pool_.put(std::move(uuids)); + + #ifdef PSEMEK_DEBUG + currently_changing_archetype_.erase(entity); + #endif + + finalize_method(); + + --method_recursion_depth_; } template @@ -522,6 +547,13 @@ namespace psemek::ecs { static_assert(detail::all_different_types_v...>, "all component types must be different"); + #ifdef PSEMEK_DEBUG + assert(!currently_changing_archetype_.contains(entity)); + currently_changing_archetype_.insert(entity); + #endif + + ++method_recursion_depth_; + auto detached_uuid_set = uuid_set_pool_.get(); (detached_uuid_set.insert(std::remove_const_t::uuid()), ...); @@ -530,10 +562,9 @@ namespace psemek::ecs auto * data = entity_list_.get_entities().begin() + entity.id; for (auto const & column : data->table->columns()) { - if (detached_uuid_set.contains(column->uuid())) - detached_uuid_set.insert(column->uuid()); - else - uuids.push_back(column->uuid()); + auto const column_uuid = column->uuid(); + if (!detached_uuid_set.contains(column_uuid)) + uuids.push_back(column_uuid); } bool const archetype_changed = sizeof...(Components) > 0; @@ -541,7 +572,7 @@ namespace psemek::ecs auto table = table_container_.get(uuids); if (archetype_changed) - data->table->trigger_destructors(*this, data->row, detached_uuid_set); + data->table->trigger_destructors(*this, data->row, {}, detached_uuid_set); // Destructors could lead to reallocation of entity list data = entity_list_.get_entities().begin() + entity.id; @@ -568,12 +599,32 @@ namespace psemek::ecs data->row = new_row; } + if (archetype_changed) + table->trigger_constructors(*this, data->row, {}, detached_uuid_set); + detached_uuid_set.clear(); uuids.clear(); detached_uuid_set.clear(); uuid_list_pool_.put(std::move(uuids)); uuid_set_pool_.put(std::move(detached_uuid_set)); + + #ifdef PSEMEK_DEBUG + currently_changing_archetype_.erase(entity); + #endif + + finalize_method(); + + --method_recursion_depth_; + } + + template + void container::detach_finally(handle entity) + { + finally([entity](container & world){ + if (world.alive(entity)) + world.detach(entity); + }); } template @@ -590,6 +641,8 @@ namespace psemek::ecs { static_assert(detail::all_different_types_v...>, "all component types must be different"); + ++method_recursion_depth_; + using invocable_type = typename detail::filter_with, Function>::type; static_assert(invocable_type::value, "function is not invocable with these components"); @@ -617,6 +670,10 @@ namespace psemek::ecs finalize_iteration(*entry.table); } + finalize_method(); + + --method_recursion_depth_; + return cache; } @@ -625,6 +682,8 @@ namespace psemek::ecs { static_assert(detail::all_different_types_v...>, "all component types must be different"); + ++method_recursion_depth_; + using invocable_type = typename detail::filter_with, Function>::type; static_assert(invocable_type::value, "function is not batch-invocable with these components"); @@ -649,6 +708,10 @@ namespace psemek::ecs finalize_iteration(*entry.table); } + finalize_method(); + + --method_recursion_depth_; + return cache; } @@ -664,9 +727,12 @@ namespace psemek::ecs auto id = next_constructor_id_++; auto constructor_factory = [function = std::move(function)](std::vector const & column_indices) -> detail::table_callback { - return [function, column_indices](container & container, detail::table & table, std::uint32_t row, util::hash_set const & attached_components, bool force){ + return [function, column_indices](container & container, detail::table & table, std::uint32_t row, util::hash_set const & attached_components, util::hash_set const & detached_components, bool force){ - bool const invoke = force || (detail::contains_helper::contains(attached_components) || ...); + bool const invoke = force + || (detail::contains_helper::contains(attached_components) || ...) + || (detail::contains_helper::contains_without(detached_components) || ...) + ; if (!invoke) return; @@ -699,9 +765,11 @@ namespace psemek::ecs auto id = next_destructor_id_++; auto destructor_factory = [function = std::move(function)](std::vector const & column_indices) -> detail::table_callback { - return [function, column_indices](container & container, detail::table & table, std::uint32_t row, util::hash_set const & detached_components, bool force){ - - bool const invoke = force || (detail::contains_helper::contains(detached_components) || ...); + return [function, column_indices](container & container, detail::table & table, std::uint32_t row, util::hash_set const & attached_components, util::hash_set const & detached_components, bool force){ + bool const invoke = force + || (detail::contains_helper::contains_without(attached_components) || ...) + || (detail::contains_helper::contains(detached_components) || ...) + ; if (!invoke) return; @@ -722,12 +790,37 @@ namespace psemek::ecs entry.table->add_destructor({id, destructor_factory(entry.columns_indices)}); } + template + void container::finally(Function && function) + { + util::function wrapper; + if constexpr (std::is_invocable_v) + { + wrapper = std::move(function); + } + else + { + wrapper = [function = std::move(function)](ecs::container &){ function(); }; + } + + if (method_recursion_depth_ == 0) + wrapper(*this); + else + finally_callbacks_.push_back(std::move(wrapper)); + } + template Index & container::index(Args && ... args) { return index_container_.get(*this, std::forward(args)...); } + template + Index & container::index_factory(Factory && factory) + { + return index_container_.set(std::move(factory)); + } + template std::size_t container::memory_usage() { diff --git a/libs/ecs/include/psemek/ecs/detail/callback.hpp b/libs/ecs/include/psemek/ecs/detail/callback.hpp index 02fa9e61..cb154e11 100644 --- a/libs/ecs/include/psemek/ecs/detail/callback.hpp +++ b/libs/ecs/include/psemek/ecs/detail/callback.hpp @@ -16,7 +16,7 @@ namespace psemek::ecs struct table; - using table_callback = util::function const &, bool)>; + using table_callback = util::function const &, util::hash_set const &, bool)>; struct ordered_table_callback { diff --git a/libs/ecs/include/psemek/ecs/detail/index_container.hpp b/libs/ecs/include/psemek/ecs/detail/index_container.hpp index a13bdbe5..d0e71160 100644 --- a/libs/ecs/include/psemek/ecs/detail/index_container.hpp +++ b/libs/ecs/include/psemek/ecs/detail/index_container.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include @@ -24,19 +25,21 @@ namespace psemek::ecs::detail { auto uuid = Index::uuid(); if (auto it = storage_.find(uuid); it != storage_.end()) - return *reinterpret_cast(it->second.get()); + return *reinterpret_cast(it->second()); if constexpr (std::is_constructible_v) { - auto ptr = std::make_shared(container, std::forward(args)...); - storage_.insert({uuid, ptr}); - return *ptr; + auto ptr = std::make_unique(container, std::forward(args)...); + auto result = ptr.get(); + storage_.insert({uuid, [ptr = std::move(ptr)]{ return ptr.get(); }}); + return *result; } else if constexpr (std::is_constructible_v) { - auto ptr = std::make_shared(std::forward(args)...); - storage_.insert({uuid, ptr}); - return *ptr; + auto ptr = std::make_unique(std::forward(args)...); + auto result = ptr.get(); + storage_.insert({uuid, [ptr = std::move(ptr)]{ return ptr.get(); }}); + return *result; } else { @@ -44,8 +47,20 @@ namespace psemek::ecs::detail } } + template + Index & set(Factory && factory) + { + auto uuid = Index::uuid(); + if (storage_.contains(uuid)) + throw util::exception("Index " + util::type_name() + " is already set"); + + auto result = factory(); + storage_[Index::uuid()] = std::move(factory); + return *result; + } + private: - util::hash_map> storage_; + util::hash_map> storage_; }; } diff --git a/libs/ecs/include/psemek/ecs/detail/table.hpp b/libs/ecs/include/psemek/ecs/detail/table.hpp index ad2edf8a..5dea109a 100644 --- a/libs/ecs/include/psemek/ecs/detail/table.hpp +++ b/libs/ecs/include/psemek/ecs/detail/table.hpp @@ -74,10 +74,10 @@ namespace psemek::ecs::detail void add_destructor(ordered_table_callback callback); void trigger_constructors(container & container, std::uint32_t row); - void trigger_constructors(container & container, std::uint32_t row, util::hash_set const & attached_components); + void trigger_constructors(container & container, std::uint32_t row, util::hash_set const & attached_components, util::hash_set const & detached_components); void trigger_destructors(container & container, std::uint32_t row); - void trigger_destructors(container & container, std::uint32_t row, util::hash_set const & detached_components); + void trigger_destructors(container & container, std::uint32_t row, util::hash_set const & attached_components, util::hash_set const & detached_components); std::size_t memory_usage() const; diff --git a/libs/ecs/include/psemek/ecs/detail/without.hpp b/libs/ecs/include/psemek/ecs/detail/without.hpp index 9841c3fd..ed4f895e 100644 --- a/libs/ecs/include/psemek/ecs/detail/without.hpp +++ b/libs/ecs/include/psemek/ecs/detail/without.hpp @@ -87,6 +87,12 @@ namespace psemek::ecs::detail { return container.contains(Component::uuid()); } + + template + static bool contains_without(Container const &) + { + return false; + } }; template @@ -97,6 +103,12 @@ namespace psemek::ecs::detail { return false; } + + template + static bool contains_without(Container const & container) + { + return container.contains(Component::uuid()); + } }; } diff --git a/libs/ecs/include/psemek/ecs/dispatcher.hpp b/libs/ecs/include/psemek/ecs/dispatcher.hpp index b534413e..a51aaa39 100644 --- a/libs/ecs/include/psemek/ecs/dispatcher.hpp +++ b/libs/ecs/include/psemek/ecs/dispatcher.hpp @@ -7,6 +7,19 @@ namespace psemek::ecs { + namespace detail + { + + struct default_context_factory + { + int operator()() const noexcept + { + return 0; + } + }; + + } + struct dispatcher { dispatcher() = default; @@ -42,12 +55,14 @@ namespace psemek::ecs }); } - template - void system(System system) + template + void system(System system, ContextFactory && context_factory = ContextFactory{}) { - handlers_[Event::uuid()].push_back([system = std::move(system), this, cache = container_->cache()](void const * event_ptr) mutable { + handlers_[Event::uuid()].push_back([system = std::move(system), this, cache = container_->cache(), context_factory = std::move(context_factory)](void const * event_ptr) mutable { auto const & event = *reinterpret_cast(event_ptr); + [[maybe_unused]] auto context = context_factory(); + container_->apply([&](ecs::container & container, ecs::handle entity, FilteredComponents & ... filtered_components){ if constexpr (std::invocable) { diff --git a/libs/ecs/include/psemek/ecs/handle.hpp b/libs/ecs/include/psemek/ecs/handle.hpp index ef2c054d..2066ad01 100644 --- a/libs/ecs/include/psemek/ecs/handle.hpp +++ b/libs/ecs/include/psemek/ecs/handle.hpp @@ -3,6 +3,7 @@ #include #include +#include namespace psemek::ecs { @@ -46,4 +47,18 @@ namespace std } }; + template + struct formatter<::psemek::ecs::handle, Char> + { + constexpr auto parse(std::format_parse_context & ctx) + { + return ctx.begin(); + } + + auto format(::psemek::ecs::handle const & handle, std::format_context & ctx) const + { + return std::format_to(ctx.out(), "({},{})", handle.id, handle.epoch); + } + }; + } diff --git a/libs/ecs/source/container.cpp b/libs/ecs/source/container.cpp index 944a858d..5396c6e0 100644 --- a/libs/ecs/source/container.cpp +++ b/libs/ecs/source/container.cpp @@ -1,6 +1,8 @@ #include #include +#include + namespace psemek::ecs { @@ -11,13 +13,36 @@ namespace psemek::ecs void container::destroy(handle entity) { + #ifdef PSEMEK_DEBUG + assert(!currently_changing_archetype_.contains(entity)); + currently_changing_archetype_.insert(entity); + #endif + assert(alive(entity)); + ++method_recursion_depth_; + auto const data = entity_list_.get_entities()[entity.id]; data.table->trigger_destructors(*this, data.row); do_destroy(entity); entity_list_.destroy(entity.id); + + #ifdef PSEMEK_DEBUG + currently_changing_archetype_.erase(entity); + #endif + + finalize_method(); + + --method_recursion_depth_; + } + + void container::destroy_finally(handle entity) + { + finally([entity](container & world){ + if (world.alive(entity)) + world.destroy(entity); + }); } accessor container::get(handle entity) @@ -50,6 +75,8 @@ namespace psemek::ecs std::optional container::try_clone(handle entity) { + ++method_recursion_depth_; + auto const data = entity_list_.get_entities()[entity.id]; if (!data.table->non_copyable_components().empty()) return std::nullopt; @@ -66,6 +93,10 @@ namespace psemek::ecs table->trigger_constructors(*this, row); + finalize_method(); + + --method_recursion_depth_; + return handle; } @@ -123,6 +154,21 @@ namespace psemek::ecs } } + void container::finalize_method() + { + if (method_recursion_depth_ > 1) + return; + + while (!finally_callbacks_.empty()) + { + auto callbacks = std::move(finally_callbacks_); + finally_callbacks_.clear(); + + for (auto & callback : callbacks) + callback(*this); + } + } + std::size_t container::entity_count() { return entity_list_.size(); diff --git a/libs/ecs/source/detail/table.cpp b/libs/ecs/source/detail/table.cpp index aa4c185f..27f8011f 100644 --- a/libs/ecs/source/detail/table.cpp +++ b/libs/ecs/source/detail/table.cpp @@ -1,6 +1,8 @@ #include #include +#include + namespace psemek::ecs::detail { @@ -159,25 +161,25 @@ namespace psemek::ecs::detail void table::trigger_constructors(container & container, std::uint32_t row) { for (auto const & callback : *constructors_) - callback.callback(container, *this, row, column_uuid_set_, true); + callback.callback(container, *this, row, column_uuid_set_, {}, true); } - void table::trigger_constructors(container & container, std::uint32_t row, util::hash_set const & attached_components) + void table::trigger_constructors(container & container, std::uint32_t row, util::hash_set const & attached_components, util::hash_set const & detached_components) { for (auto const & callback : *constructors_) - callback.callback(container, *this, row, attached_components, false); + callback.callback(container, *this, row, attached_components, detached_components, false); } void table::trigger_destructors(container & container, std::uint32_t row) { for (auto const & callback : *destructors_) - callback.callback(container, *this, row, column_uuid_set_, true); + callback.callback(container, *this, row, {}, column_uuid_set_, true); } - void table::trigger_destructors(container & container, std::uint32_t row, util::hash_set const & detached_components) + void table::trigger_destructors(container & container, std::uint32_t row, util::hash_set const & attached_components, util::hash_set const & detached_components) { for (auto const & callback : *destructors_) - callback.callback(container, *this, row, detached_components, false); + callback.callback(container, *this, row, attached_components, detached_components, false); } std::size_t table::memory_usage() const diff --git a/libs/fonts/include/psemek/fonts/font.hpp b/libs/fonts/include/psemek/fonts/font.hpp index a70f77fc..1b73bd7c 100644 --- a/libs/fonts/include/psemek/fonts/font.hpp +++ b/libs/fonts/include/psemek/fonts/font.hpp @@ -11,7 +11,7 @@ #include #include -namespace psemek::fonts +namespace psemek::fonts::inline v1 { enum class font_type diff --git a/libs/fonts/include/psemek/fonts/font_v2.hpp b/libs/fonts/include/psemek/fonts/font_v2.hpp index ed2976b8..53639b11 100644 --- a/libs/fonts/include/psemek/fonts/font_v2.hpp +++ b/libs/fonts/include/psemek/fonts/font_v2.hpp @@ -92,4 +92,7 @@ namespace psemek::fonts std::unique_ptr load_freetype_font(wgpu::device device, std::filesystem::path const & path); + // Font paths are specified in order + std::unique_ptr load_combined_freetype_font(wgpu::device device, std::vector const & paths); + } diff --git a/libs/fonts/include/psemek/fonts/kerned_font.hpp b/libs/fonts/include/psemek/fonts/kerned_font.hpp index 79ce1a47..0eaa75be 100644 --- a/libs/fonts/include/psemek/fonts/kerned_font.hpp +++ b/libs/fonts/include/psemek/fonts/kerned_font.hpp @@ -3,7 +3,7 @@ #include #include -namespace psemek::fonts +namespace psemek::fonts::inline v1 { struct kerned_font diff --git a/libs/fonts/include/psemek/fonts/msdf_font.hpp b/libs/fonts/include/psemek/fonts/msdf_font.hpp index 3963d5be..4d8254c4 100644 --- a/libs/fonts/include/psemek/fonts/msdf_font.hpp +++ b/libs/fonts/include/psemek/fonts/msdf_font.hpp @@ -3,7 +3,7 @@ #include #include -namespace psemek::fonts +namespace psemek::fonts::inline v1 { struct msdf_font diff --git a/libs/fonts/source/default_fonts.cpp b/libs/fonts/source/default_fonts.cpp index d04973a2..0ede52fc 100644 --- a/libs/fonts/source/default_fonts.cpp +++ b/libs/fonts/source/default_fonts.cpp @@ -13,7 +13,7 @@ #include -namespace psemek::fonts +namespace psemek::fonts::inline v1 { std::unique_ptr make_default_monospace_9x12_font() diff --git a/libs/fonts/source/freetype.cpp b/libs/fonts/source/freetype.cpp index 615c3cde..38063bd3 100644 --- a/libs/fonts/source/freetype.cpp +++ b/libs/fonts/source/freetype.cpp @@ -72,11 +72,19 @@ namespace psemek::fonts struct face_shared { wgpu::device device; - FT_Face face; + + struct face_data + { + util::blob data; + FT_Face face; + }; + + std::vector faces = {}; ~face_shared() { - FT_Done_Face(face); + for (auto const & face : faces) + FT_Done_Face(face.face); } }; @@ -88,6 +96,9 @@ namespace psemek::fonts , type_(type) , size_(size) { + for (auto const & face : state_->faces) + ft_check_result(FT_Set_Char_Size(face.face, 0, size_ << 6, 0, 0), "Failed to select freetype face size: "); + font::shape("x", {}); xheight_ = -shaped_text_[0].position[1].min; shaped_text_.clear(); @@ -100,7 +111,7 @@ namespace psemek::fonts std::string_view name() const override { - return FT_Get_Postscript_Name(state_->face); + return FT_Get_Postscript_Name(state_->faces[0].face); } math::vector size() const override @@ -134,7 +145,7 @@ namespace psemek::fonts struct page { - util::array pixmap; + util::ndarray pixmap; int current_row_start = 0; int current_row_height = 0; @@ -147,6 +158,12 @@ namespace psemek::fonts std::vector pages_; + struct glyph_mapping_data + { + std::uint32_t font_index; + std::uint32_t glyph_index; + }; + struct glyph_data { int page; @@ -155,8 +172,8 @@ namespace psemek::fonts math::vector advance; }; - util::hash_map glyph_mapping_; - util::hash_map glyphs_; + util::hash_map glyph_mapping_; + util::hash_map> glyphs_; std::vector shaped_text_; @@ -167,37 +184,53 @@ namespace psemek::fonts shaped_text_.clear(); - auto face = state_->face; - - ft_check_result(FT_Set_Char_Size(face, 0, size_ << 6, 0, 0), "Failed to select freetype face size: "); - bool need_update_pages = false; for (char32_t ch : string) { - std::uint32_t glyph_id; + glyph_mapping_data mapping{0, 0}; if (auto it = glyph_mapping_.find(ch); it != glyph_mapping_.end()) { - glyph_id = it->second; + mapping = it->second; } else { - glyph_id = FT_Get_Char_Index(face, ch); - glyph_mapping_[ch] = glyph_id; + int glyph_index = 0; + for (int font_index = 0; font_index < state_->faces.size(); ++font_index) + { + glyph_index = FT_Get_Char_Index(state_->faces[font_index].face, ch); + if (glyph_index != 0) + { + mapping = {font_index, glyph_index}; + break; + } + } + + if (glyph_index == 0) + { + mapping.font_index = 0; + mapping.glyph_index = FT_Get_Char_Index(state_->faces[0].face, '?'); + } + + glyph_mapping_[ch] = mapping; } + auto face = state_->faces[mapping.font_index].face; + glyph_data * data = nullptr; - if (auto it = glyphs_.find(glyph_id); it != glyphs_.end()) + auto & glyphs = glyphs_[mapping.font_index]; + + if (auto it = glyphs.find(mapping.glyph_index); it != glyphs.end()) { data = &(it->second); } else { - data = &glyphs_[glyph_id]; + data = &glyphs[mapping.glyph_index]; - FT_Load_Glyph(face, glyph_id, type_ == font_type::bitmap ? FT_LOAD_TARGET_LIGHT : FT_LOAD_DEFAULT); + FT_Load_Glyph(face, mapping.glyph_index, type_ == font_type::bitmap ? FT_LOAD_TARGET_LIGHT : FT_LOAD_DEFAULT); FT_Render_Glyph(face->glyph, type_ == font_type::bitmap ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_SDF); if (face->glyph->bitmap.width + 2 * padding > page_size || face->glyph->bitmap.rows + 2 * padding > page_size) @@ -206,7 +239,7 @@ namespace psemek::fonts data->part = {{{0, 0}, {0, 0}}}; data->offset = {0, 0}; data->advance = {size_, 0}; - log::warning() << "Glyph with ID " << glyph_id << " (U+" << std::hex << (int)ch << ") is larger than font atlas page: " << face->glyph->bitmap.width << "x" << face->glyph->bitmap.rows; + log::warning() << "Glyph with ID " << mapping.glyph_index << " (U+" << std::hex << (int)ch << ") is larger than font atlas page: " << face->glyph->bitmap.width << "x" << face->glyph->bitmap.rows; } else { @@ -315,7 +348,7 @@ namespace psemek::fonts std::string_view name() const override { - return FT_Get_Postscript_Name(state_->face); + return FT_Get_Postscript_Name(state_->faces[0].face); } std::unique_ptr create(font_type type, int size) override @@ -333,15 +366,26 @@ namespace psemek::fonts std::unique_ptr load_freetype_font(wgpu::device device, std::filesystem::path const & path) { - FT_Face face; - auto path_str = path.string(); - ft_check_result(FT_New_Face(ft_library(), path_str.data(), 0, &face), "Failed to load font " + path_str + ": "); + return load_combined_freetype_font(device, {path}); + } - auto result = std::make_unique(std::make_shared(device, face)); + std::unique_ptr load_combined_freetype_font(wgpu::device device, std::vector const & paths) + { + auto result = std::make_shared(device); - log::debug() << "Loaded font " << result->name() << " (" << path << ")"; + for (auto const & path : paths) + { + auto font_data = io::read_full(io::file_istream{path}); - return result; + FT_Face face = nullptr; + ft_check_result(FT_New_Memory_Face(ft_library(), reinterpret_cast(font_data.data()), font_data.size(), 0, &face), util::to_string("Failed to load font ", path, ": ")); + + result->faces.push_back({std::move(font_data), face}); + + log::debug() << "Loaded font " << FT_Get_Postscript_Name(face) << " (" << path << ")"; + } + + return std::make_unique(std::move(result)); } } diff --git a/libs/fonts/source/kerned_font.cpp b/libs/fonts/source/kerned_font.cpp index 0e8b2846..8ded8d5d 100644 --- a/libs/fonts/source/kerned_font.cpp +++ b/libs/fonts/source/kerned_font.cpp @@ -3,7 +3,7 @@ #include #include -namespace psemek::fonts +namespace psemek::fonts::inline v1 { kerned_font::kerned_font(bmfont_data data, gfx::texture_2d atlas) diff --git a/libs/fonts/source/msdf_font.cpp b/libs/fonts/source/msdf_font.cpp index 43678560..ee8765b9 100644 --- a/libs/fonts/source/msdf_font.cpp +++ b/libs/fonts/source/msdf_font.cpp @@ -1,7 +1,7 @@ #include #include -namespace psemek::fonts +namespace psemek::fonts::inline v1 { std::unique_ptr make_msdf_font(io::istream && description, io::istream && texture) diff --git a/libs/gfx/include/psemek/gfx/color.hpp b/libs/gfx/include/psemek/gfx/color.hpp index 2f2e78f6..b4d01a28 100644 --- a/libs/gfx/include/psemek/gfx/color.hpp +++ b/libs/gfx/include/psemek/gfx/color.hpp @@ -317,6 +317,77 @@ namespace psemek::gfx return generic_color{dark(c.c, darkness)}; } + template + math::vector rgb_to_hsv(math::vector const & rgb) + { + T max = std::max({rgb[0], rgb[1], rgb[2]}); + T min = std::min({rgb[0], rgb[1], rgb[2]}); + T delta = max - min; + + T hue = T{0}; + + if (delta > T{0}) + { + if (rgb[0] > rgb[1] && rgb[0] > rgb[2]) + { + hue = (rgb[1] - rgb[2]) / delta; + } + else if (rgb[1] > rgb[2]) + { + hue = 2.f + (rgb[2] - rgb[0]) / delta; + } + else + { + hue = 4.f + (rgb[0] - rgb[1]) / delta; + } + } + + hue *= math::rad(60); + + if (hue < 0) + hue += math::rad(360); + + math::vector result; + result[0] = hue; + result[1] = (max > T{0}) ? (delta / max) : T{0}; + result[2] = max; + + return result; + } + + template + math::vector hsv_to_rgb(math::vector const & hsv) + { + auto c = hsv[1] * hsv[2]; + auto x = c * (T{1} - std::abs(std::fmod(hsv[0] / math::rad(60), 2.f) - 1.f)); + auto m = hsv[2] - c; + + math::vector result; + + if (hsv[0] < math::rad(60)) + result = {c, x, T{0}}; + else if (hsv[0] < math::rad(120)) + result = {x, c, T{0}}; + else if (hsv[0] < math::rad(180)) + result = {T{0}, c, x}; + else if (hsv[0] < math::rad(240)) + result = {T{0}, x, c}; + else if (hsv[0] < math::rad(300)) + result = {x, T{0}, c}; + else + result = {c, T{0}, x}; + + result += math::vector{m, m, m}; + + return result; + } + + template + T luminance(math::vector const & c) + { + return c[0] * T(0.299) + c[1] * T(0.587) + c[2] * T(0.114); + } + std::optional parse_color(std::string_view const & text); } diff --git a/libs/gfx/include/psemek/gfx/detail/stb_image.h b/libs/gfx/include/psemek/gfx/detail/stb_image.h index 5e807a0a..9eedabed 100644 --- a/libs/gfx/include/psemek/gfx/detail/stb_image.h +++ b/libs/gfx/include/psemek/gfx/detail/stb_image.h @@ -1,4 +1,4 @@ -/* stb_image - v2.28 - public domain image loader - http://nothings.org/stb +/* stb_image - v2.30 - public domain image loader - http://nothings.org/stb no warranty implied; use at your own risk Do this: @@ -48,6 +48,8 @@ LICENSE RECENT REVISION HISTORY: + 2.30 (2024-05-31) avoid erroneous gcc warning + 2.29 (2023-05-xx) optimizations 2.28 (2023-01-29) many error fixes, security errors, just tons of stuff 2.27 (2021-07-11) document stbi_info better, 16-bit PNM support, bug fixes 2.26 (2020-07-13) many minor fixes @@ -1072,8 +1074,8 @@ static int stbi__addints_valid(int a, int b) return a <= INT_MAX - b; } -// returns 1 if the product of two signed shorts is valid, 0 on overflow. -static int stbi__mul2shorts_valid(short a, short b) +// returns 1 if the product of two ints fits in a signed short, 0 on overflow. +static int stbi__mul2shorts_valid(int a, int b) { if (b == 0 || b == -1) return 1; // multiplication by 0 is always 0; check for -1 so SHRT_MIN/b doesn't overflow if ((a >= 0) == (b >= 0)) return a <= SHRT_MAX/b; // product is positive, so similar to mul2sizes_valid @@ -3384,13 +3386,13 @@ static int stbi__decode_jpeg_header(stbi__jpeg *z, int scan) return 1; } -static int stbi__skip_jpeg_junk_at_end(stbi__jpeg *j) +static stbi_uc stbi__skip_jpeg_junk_at_end(stbi__jpeg *j) { // some JPEGs have junk at end, skip over it but if we find what looks // like a valid marker, resume there while (!stbi__at_eof(j->s)) { - int x = stbi__get8(j->s); - while (x == 255) { // might be a marker + stbi_uc x = stbi__get8(j->s); + while (x == 0xff) { // might be a marker if (stbi__at_eof(j->s)) return STBI__MARKER_none; x = stbi__get8(j->s); if (x != 0x00 && x != 0xff) { @@ -4176,6 +4178,7 @@ typedef struct { stbi_uc *zbuffer, *zbuffer_end; int num_bits; + int hit_zeof_once; stbi__uint32 code_buffer; char *zout; @@ -4242,9 +4245,20 @@ stbi_inline static int stbi__zhuffman_decode(stbi__zbuf *a, stbi__zhuffman *z) int b,s; if (a->num_bits < 16) { if (stbi__zeof(a)) { - return -1; /* report error for unexpected end of data. */ + if (!a->hit_zeof_once) { + // This is the first time we hit eof, insert 16 extra padding btis + // to allow us to keep going; if we actually consume any of them + // though, that is invalid data. This is caught later. + a->hit_zeof_once = 1; + a->num_bits += 16; // add 16 implicit zero bits + } else { + // We already inserted our extra 16 padding bits and are again + // out, this stream is actually prematurely terminated. + return -1; + } + } else { + stbi__fill_bits(a); } - stbi__fill_bits(a); } b = z->fast[a->code_buffer & STBI__ZFAST_MASK]; if (b) { @@ -4309,6 +4323,13 @@ static int stbi__parse_huffman_block(stbi__zbuf *a) int len,dist; if (z == 256) { a->zout = zout; + if (a->hit_zeof_once && a->num_bits < 16) { + // The first time we hit zeof, we inserted 16 extra zero bits into our bit + // buffer so the decoder can just do its speculative decoding. But if we + // actually consumed any of those bits (which is the case when num_bits < 16), + // the stream actually read past the end so it is malformed. + return stbi__err("unexpected end","Corrupt PNG"); + } return 1; } if (z >= 286) return stbi__err("bad huffman code","Corrupt PNG"); // per DEFLATE, length codes 286 and 287 must not appear in compressed data @@ -4320,7 +4341,7 @@ static int stbi__parse_huffman_block(stbi__zbuf *a) dist = stbi__zdist_base[z]; if (stbi__zdist_extra[z]) dist += stbi__zreceive(a, stbi__zdist_extra[z]); if (zout - a->zout_start < dist) return stbi__err("bad dist","Corrupt PNG"); - if (zout + len > a->zout_end) { + if (len > a->zout_end - zout) { if (!stbi__zexpand(a, zout, len)) return 0; zout = a->zout; } @@ -4464,6 +4485,7 @@ static int stbi__parse_zlib(stbi__zbuf *a, int parse_header) if (!stbi__parse_zlib_header(a)) return 0; a->num_bits = 0; a->code_buffer = 0; + a->hit_zeof_once = 0; do { final = stbi__zreceive(a,1); type = stbi__zreceive(a,2); @@ -4619,9 +4641,8 @@ enum { STBI__F_up=2, STBI__F_avg=3, STBI__F_paeth=4, - // synthetic filters used for first scanline to avoid needing a dummy row of 0s - STBI__F_avg_first, - STBI__F_paeth_first + // synthetic filter used for first scanline to avoid needing a dummy row of 0s + STBI__F_avg_first }; static stbi_uc first_row_filter[5] = @@ -4630,29 +4651,56 @@ static stbi_uc first_row_filter[5] = STBI__F_sub, STBI__F_none, STBI__F_avg_first, - STBI__F_paeth_first + STBI__F_sub // Paeth with b=c=0 turns out to be equivalent to sub }; static int stbi__paeth(int a, int b, int c) { - int p = a + b - c; - int pa = abs(p-a); - int pb = abs(p-b); - int pc = abs(p-c); - if (pa <= pb && pa <= pc) return a; - if (pb <= pc) return b; - return c; + // This formulation looks very different from the reference in the PNG spec, but is + // actually equivalent and has favorable data dependencies and admits straightforward + // generation of branch-free code, which helps performance significantly. + int thresh = c*3 - (a + b); + int lo = a < b ? a : b; + int hi = a < b ? b : a; + int t0 = (hi <= thresh) ? lo : c; + int t1 = (thresh <= lo) ? hi : t0; + return t1; } static const stbi_uc stbi__depth_scale_table[9] = { 0, 0xff, 0x55, 0, 0x11, 0,0,0, 0x01 }; +// adds an extra all-255 alpha channel +// dest == src is legal +// img_n must be 1 or 3 +static void stbi__create_png_alpha_expand8(stbi_uc *dest, stbi_uc *src, stbi__uint32 x, int img_n) +{ + int i; + // must process data backwards since we allow dest==src + if (img_n == 1) { + for (i=x-1; i >= 0; --i) { + dest[i*2+1] = 255; + dest[i*2+0] = src[i]; + } + } else { + STBI_ASSERT(img_n == 3); + for (i=x-1; i >= 0; --i) { + dest[i*4+3] = 255; + dest[i*4+2] = src[i*3+2]; + dest[i*4+1] = src[i*3+1]; + dest[i*4+0] = src[i*3+0]; + } + } +} + // create the png data from post-deflated data static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 raw_len, int out_n, stbi__uint32 x, stbi__uint32 y, int depth, int color) { - int bytes = (depth == 16? 2 : 1); + int bytes = (depth == 16 ? 2 : 1); stbi__context *s = a->s; stbi__uint32 i,j,stride = x*out_n*bytes; stbi__uint32 img_len, img_width_bytes; + stbi_uc *filter_buf; + int all_ok = 1; int k; int img_n = s->img_n; // copy it into a local for later @@ -4664,8 +4712,11 @@ static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 r a->out = (stbi_uc *) stbi__malloc_mad3(x, y, output_bytes, 0); // extra bytes to write off the end into if (!a->out) return stbi__err("outofmem", "Out of memory"); + // note: error exits here don't need to clean up a->out individually, + // stbi__do_png always does on error. if (!stbi__mad3sizes_valid(img_n, x, depth, 7)) return stbi__err("too large", "Corrupt PNG"); img_width_bytes = (((img_n * x * depth) + 7) >> 3); + if (!stbi__mad2sizes_valid(img_width_bytes, y, img_width_bytes)) return stbi__err("too large", "Corrupt PNG"); img_len = (img_width_bytes + 1) * y; // we used to check for exact match between raw_len and img_len on non-interlaced PNGs, @@ -4673,189 +4724,137 @@ static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 r // so just check for raw_len < img_len always. if (raw_len < img_len) return stbi__err("not enough pixels","Corrupt PNG"); + // Allocate two scan lines worth of filter workspace buffer. + filter_buf = (stbi_uc *) stbi__malloc_mad2(img_width_bytes, 2, 0); + if (!filter_buf) return stbi__err("outofmem", "Out of memory"); + + // Filtering for low-bit-depth images + if (depth < 8) { + filter_bytes = 1; + width = img_width_bytes; + } + for (j=0; j < y; ++j) { - stbi_uc *cur = a->out + stride*j; - stbi_uc *prior; + // cur/prior filter buffers alternate + stbi_uc *cur = filter_buf + (j & 1)*img_width_bytes; + stbi_uc *prior = filter_buf + (~j & 1)*img_width_bytes; + stbi_uc *dest = a->out + stride*j; + int nk = width * filter_bytes; int filter = *raw++; - if (filter > 4) - return stbi__err("invalid filter","Corrupt PNG"); - - if (depth < 8) { - if (img_width_bytes > x) return stbi__err("invalid width","Corrupt PNG"); - cur += x*out_n - img_width_bytes; // store output to the rightmost img_len bytes, so we can decode in place - filter_bytes = 1; - width = img_width_bytes; + // check filter type + if (filter > 4) { + all_ok = stbi__err("invalid filter","Corrupt PNG"); + break; } - prior = cur - stride; // bugfix: need to compute this after 'cur +=' computation above // if first row, use special filter that doesn't sample previous row if (j == 0) filter = first_row_filter[filter]; - // handle first byte explicitly - for (k=0; k < filter_bytes; ++k) { - switch (filter) { - case STBI__F_none : cur[k] = raw[k]; break; - case STBI__F_sub : cur[k] = raw[k]; break; - case STBI__F_up : cur[k] = STBI__BYTECAST(raw[k] + prior[k]); break; - case STBI__F_avg : cur[k] = STBI__BYTECAST(raw[k] + (prior[k]>>1)); break; - case STBI__F_paeth : cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(0,prior[k],0)); break; - case STBI__F_avg_first : cur[k] = raw[k]; break; - case STBI__F_paeth_first: cur[k] = raw[k]; break; - } + // perform actual filtering + switch (filter) { + case STBI__F_none: + memcpy(cur, raw, nk); + break; + case STBI__F_sub: + memcpy(cur, raw, filter_bytes); + for (k = filter_bytes; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + cur[k-filter_bytes]); + break; + case STBI__F_up: + for (k = 0; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + prior[k]); + break; + case STBI__F_avg: + for (k = 0; k < filter_bytes; ++k) + cur[k] = STBI__BYTECAST(raw[k] + (prior[k]>>1)); + for (k = filter_bytes; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + ((prior[k] + cur[k-filter_bytes])>>1)); + break; + case STBI__F_paeth: + for (k = 0; k < filter_bytes; ++k) + cur[k] = STBI__BYTECAST(raw[k] + prior[k]); // prior[k] == stbi__paeth(0,prior[k],0) + for (k = filter_bytes; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k-filter_bytes], prior[k], prior[k-filter_bytes])); + break; + case STBI__F_avg_first: + memcpy(cur, raw, filter_bytes); + for (k = filter_bytes; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + (cur[k-filter_bytes] >> 1)); + break; } - if (depth == 8) { - if (img_n != out_n) - cur[img_n] = 255; // first pixel - raw += img_n; - cur += out_n; - prior += out_n; - } else if (depth == 16) { - if (img_n != out_n) { - cur[filter_bytes] = 255; // first pixel top byte - cur[filter_bytes+1] = 255; // first pixel bottom byte - } - raw += filter_bytes; - cur += output_bytes; - prior += output_bytes; - } else { - raw += 1; - cur += 1; - prior += 1; - } + raw += nk; - // this is a little gross, so that we don't switch per-pixel or per-component - if (depth < 8 || img_n == out_n) { - int nk = (width - 1)*filter_bytes; - #define STBI__CASE(f) \ - case f: \ - for (k=0; k < nk; ++k) - switch (filter) { - // "none" filter turns into a memcpy here; make that explicit. - case STBI__F_none: memcpy(cur, raw, nk); break; - STBI__CASE(STBI__F_sub) { cur[k] = STBI__BYTECAST(raw[k] + cur[k-filter_bytes]); } break; - STBI__CASE(STBI__F_up) { cur[k] = STBI__BYTECAST(raw[k] + prior[k]); } break; - STBI__CASE(STBI__F_avg) { cur[k] = STBI__BYTECAST(raw[k] + ((prior[k] + cur[k-filter_bytes])>>1)); } break; - STBI__CASE(STBI__F_paeth) { cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k-filter_bytes],prior[k],prior[k-filter_bytes])); } break; - STBI__CASE(STBI__F_avg_first) { cur[k] = STBI__BYTECAST(raw[k] + (cur[k-filter_bytes] >> 1)); } break; - STBI__CASE(STBI__F_paeth_first) { cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k-filter_bytes],0,0)); } break; - } - #undef STBI__CASE - raw += nk; - } else { - STBI_ASSERT(img_n+1 == out_n); - #define STBI__CASE(f) \ - case f: \ - for (i=x-1; i >= 1; --i, cur[filter_bytes]=255,raw+=filter_bytes,cur+=output_bytes,prior+=output_bytes) \ - for (k=0; k < filter_bytes; ++k) - switch (filter) { - STBI__CASE(STBI__F_none) { cur[k] = raw[k]; } break; - STBI__CASE(STBI__F_sub) { cur[k] = STBI__BYTECAST(raw[k] + cur[k- output_bytes]); } break; - STBI__CASE(STBI__F_up) { cur[k] = STBI__BYTECAST(raw[k] + prior[k]); } break; - STBI__CASE(STBI__F_avg) { cur[k] = STBI__BYTECAST(raw[k] + ((prior[k] + cur[k- output_bytes])>>1)); } break; - STBI__CASE(STBI__F_paeth) { cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k- output_bytes],prior[k],prior[k- output_bytes])); } break; - STBI__CASE(STBI__F_avg_first) { cur[k] = STBI__BYTECAST(raw[k] + (cur[k- output_bytes] >> 1)); } break; - STBI__CASE(STBI__F_paeth_first) { cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k- output_bytes],0,0)); } break; - } - #undef STBI__CASE - - // the loop above sets the high byte of the pixels' alpha, but for - // 16 bit png files we also need the low byte set. we'll do that here. - if (depth == 16) { - cur = a->out + stride*j; // start at the beginning of the row again - for (i=0; i < x; ++i,cur+=output_bytes) { - cur[filter_bytes+1] = 255; - } - } - } - } - - // we make a separate pass to expand bits to pixels; for performance, - // this could run two scanlines behind the above code, so it won't - // intefere with filtering but will still be in the cache. - if (depth < 8) { - for (j=0; j < y; ++j) { - stbi_uc *cur = a->out + stride*j; - stbi_uc *in = a->out + stride*j + x*out_n - img_width_bytes; - // unpack 1/2/4-bit into a 8-bit buffer. allows us to keep the common 8-bit path optimal at minimal cost for 1/2/4-bit - // png guarante byte alignment, if width is not multiple of 8/4/2 we'll decode dummy trailing data that will be skipped in the later loop + // expand decoded bits in cur to dest, also adding an extra alpha channel if desired + if (depth < 8) { stbi_uc scale = (color == 0) ? stbi__depth_scale_table[depth] : 1; // scale grayscale values to 0..255 range + stbi_uc *in = cur; + stbi_uc *out = dest; + stbi_uc inb = 0; + stbi__uint32 nsmp = x*img_n; - // note that the final byte might overshoot and write more data than desired. - // we can allocate enough data that this never writes out of memory, but it - // could also overwrite the next scanline. can it overwrite non-empty data - // on the next scanline? yes, consider 1-pixel-wide scanlines with 1-bit-per-pixel. - // so we need to explicitly clamp the final ones - + // expand bits to bytes first if (depth == 4) { - for (k=x*img_n; k >= 2; k-=2, ++in) { - *cur++ = scale * ((*in >> 4) ); - *cur++ = scale * ((*in ) & 0x0f); + for (i=0; i < nsmp; ++i) { + if ((i & 1) == 0) inb = *in++; + *out++ = scale * (inb >> 4); + inb <<= 4; } - if (k > 0) *cur++ = scale * ((*in >> 4) ); } else if (depth == 2) { - for (k=x*img_n; k >= 4; k-=4, ++in) { - *cur++ = scale * ((*in >> 6) ); - *cur++ = scale * ((*in >> 4) & 0x03); - *cur++ = scale * ((*in >> 2) & 0x03); - *cur++ = scale * ((*in ) & 0x03); + for (i=0; i < nsmp; ++i) { + if ((i & 3) == 0) inb = *in++; + *out++ = scale * (inb >> 6); + inb <<= 2; } - if (k > 0) *cur++ = scale * ((*in >> 6) ); - if (k > 1) *cur++ = scale * ((*in >> 4) & 0x03); - if (k > 2) *cur++ = scale * ((*in >> 2) & 0x03); - } else if (depth == 1) { - for (k=x*img_n; k >= 8; k-=8, ++in) { - *cur++ = scale * ((*in >> 7) ); - *cur++ = scale * ((*in >> 6) & 0x01); - *cur++ = scale * ((*in >> 5) & 0x01); - *cur++ = scale * ((*in >> 4) & 0x01); - *cur++ = scale * ((*in >> 3) & 0x01); - *cur++ = scale * ((*in >> 2) & 0x01); - *cur++ = scale * ((*in >> 1) & 0x01); - *cur++ = scale * ((*in ) & 0x01); + } else { + STBI_ASSERT(depth == 1); + for (i=0; i < nsmp; ++i) { + if ((i & 7) == 0) inb = *in++; + *out++ = scale * (inb >> 7); + inb <<= 1; } - if (k > 0) *cur++ = scale * ((*in >> 7) ); - if (k > 1) *cur++ = scale * ((*in >> 6) & 0x01); - if (k > 2) *cur++ = scale * ((*in >> 5) & 0x01); - if (k > 3) *cur++ = scale * ((*in >> 4) & 0x01); - if (k > 4) *cur++ = scale * ((*in >> 3) & 0x01); - if (k > 5) *cur++ = scale * ((*in >> 2) & 0x01); - if (k > 6) *cur++ = scale * ((*in >> 1) & 0x01); } - if (img_n != out_n) { - int q; - // insert alpha = 255 - cur = a->out + stride*j; + + // insert alpha=255 values if desired + if (img_n != out_n) + stbi__create_png_alpha_expand8(dest, dest, x, img_n); + } else if (depth == 8) { + if (img_n == out_n) + memcpy(dest, cur, x*img_n); + else + stbi__create_png_alpha_expand8(dest, cur, x, img_n); + } else if (depth == 16) { + // convert the image data from big-endian to platform-native + stbi__uint16 *dest16 = (stbi__uint16*)dest; + stbi__uint32 nsmp = x*img_n; + + if (img_n == out_n) { + for (i = 0; i < nsmp; ++i, ++dest16, cur += 2) + *dest16 = (cur[0] << 8) | cur[1]; + } else { + STBI_ASSERT(img_n+1 == out_n); if (img_n == 1) { - for (q=x-1; q >= 0; --q) { - cur[q*2+1] = 255; - cur[q*2+0] = cur[q]; + for (i = 0; i < x; ++i, dest16 += 2, cur += 2) { + dest16[0] = (cur[0] << 8) | cur[1]; + dest16[1] = 0xffff; } } else { STBI_ASSERT(img_n == 3); - for (q=x-1; q >= 0; --q) { - cur[q*4+3] = 255; - cur[q*4+2] = cur[q*3+2]; - cur[q*4+1] = cur[q*3+1]; - cur[q*4+0] = cur[q*3+0]; + for (i = 0; i < x; ++i, dest16 += 4, cur += 6) { + dest16[0] = (cur[0] << 8) | cur[1]; + dest16[1] = (cur[2] << 8) | cur[3]; + dest16[2] = (cur[4] << 8) | cur[5]; + dest16[3] = 0xffff; } } } } - } else if (depth == 16) { - // force the image data from big-endian to platform-native. - // this is done in a separate pass due to the decoding relying - // on the data being untouched, but could probably be done - // per-line during decode if care is taken. - stbi_uc *cur = a->out; - stbi__uint16 *cur16 = (stbi__uint16*)cur; - - for(i=0; i < x*y*out_n; ++i,cur16++,cur+=2) { - *cur16 = (cur[0] << 8) | cur[1]; - } } + STBI_FREE(filter_buf); + if (!all_ok) return 0; + return 1; } @@ -5161,9 +5160,11 @@ static int stbi__parse_png_file(stbi__png *z, int scan, int req_comp) // non-paletted with tRNS = constant alpha. if header-scanning, we can stop now. if (scan == STBI__SCAN_header) { ++s->img_n; return 1; } if (z->depth == 16) { - for (k = 0; k < s->img_n; ++k) tc16[k] = (stbi__uint16)stbi__get16be(s); // copy the values as-is + for (k = 0; k < s->img_n && k < 3; ++k) // extra loop test to suppress false GCC warning + tc16[k] = (stbi__uint16)stbi__get16be(s); // copy the values as-is } else { - for (k = 0; k < s->img_n; ++k) tc[k] = (stbi_uc)(stbi__get16be(s) & 255) * stbi__depth_scale_table[z->depth]; // non 8-bit images will be larger + for (k = 0; k < s->img_n && k < 3; ++k) + tc[k] = (stbi_uc)(stbi__get16be(s) & 255) * stbi__depth_scale_table[z->depth]; // non 8-bit images will be larger } } break; diff --git a/libs/gfx/include/psemek/gfx/gltf_accessor_iterator.hpp b/libs/gfx/include/psemek/gfx/gltf_accessor_iterator.hpp index 16af4e84..890b69eb 100644 --- a/libs/gfx/include/psemek/gfx/gltf_accessor_iterator.hpp +++ b/libs/gfx/include/psemek/gfx/gltf_accessor_iterator.hpp @@ -93,14 +93,12 @@ namespace psemek::gfx static auto pointer(math::matrix & value) { - return &value[0][0]; + return value.values().begin(); } static void finalize(math::matrix & value) { - math::matrix temp; - std::copy(value.coords, value.coords + R * C, temp.coords); - value = math::transpose(temp); + value = math::transpose(value); } }; diff --git a/libs/gfx/include/psemek/gfx/gltf_animation.hpp b/libs/gfx/include/psemek/gfx/gltf_animation.hpp index ac5f95b7..df3e8491 100644 --- a/libs/gfx/include/psemek/gfx/gltf_animation.hpp +++ b/libs/gfx/include/psemek/gfx/gltf_animation.hpp @@ -1,9 +1,7 @@ #pragma once #include -#include -#include -#include +#include #include #include @@ -147,7 +145,7 @@ namespace psemek::gfx math::interval range() const; - math::affine_transform operator()(float time) const; + math::trs operator()(float time) const; gltf_scale_animation const & scale() const { return scale_; } gltf_rotation_animation const & rotation() const { return rotation_; } diff --git a/libs/gfx/include/psemek/gfx/mesh.hpp b/libs/gfx/include/psemek/gfx/mesh.hpp index 89d37fc1..52153bdb 100644 --- a/libs/gfx/include/psemek/gfx/mesh.hpp +++ b/libs/gfx/include/psemek/gfx/mesh.hpp @@ -195,11 +195,11 @@ namespace psemek::gfx GLenum index_type() const { return info_.index_type_; } buffer & vertex_buffer() { return vertex_buffer_; } - buffer & index_buffer() { return index_buffer_; } + auto & index_buffer() { return index_buffer_; } buffer & instance_buffer() { return instance_buffer_; } buffer const & vertex_buffer() const { return vertex_buffer_; } - buffer const & index_buffer() const { return index_buffer_; } + auto const & index_buffer() const { return index_buffer_; } buffer const & instance_buffer() const { return instance_buffer_; } // Drawing commands @@ -211,7 +211,7 @@ namespace psemek::gfx private: array array_; buffer vertex_buffer_ = buffer::null(); - buffer index_buffer_ = buffer::null(); + basic_buffer index_buffer_ = basic_buffer::null(); buffer instance_buffer_ = buffer::null(); struct mesh_info @@ -298,11 +298,8 @@ namespace psemek::gfx assert((index_count % (*n)) == 0); if (!index_buffer_) - { - index_buffer_ = buffer{}; - array_.bind(); - gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, index_buffer_.id()); - } + index_buffer_ = {}; + array_.bind(); index_buffer_.load(indices, index_count, usage); info_.index_count_ = index_count; info_.indexed_ = true; diff --git a/libs/gfx/include/psemek/gfx/painter.hpp b/libs/gfx/include/psemek/gfx/painter.hpp index f86c44c0..d22cbb24 100644 --- a/libs/gfx/include/psemek/gfx/painter.hpp +++ b/libs/gfx/include/psemek/gfx/painter.hpp @@ -56,6 +56,7 @@ namespace psemek::gfx void triangle(math::point const & p0, math::point const & p1, math::point const & p2, color const & c0, color const & c1, color const & c2); void rect(math::box const & box, color const & c); void circle(math::point const & center, float radius, color const & c, int quality = 24); + void circle(math::point const & center, float radius, color const & c0, color const & c1, int quality = 24); void line(math::point const & p0, math::point const & p1, float width, color const & c, bool smooth = true); void line(math::point const & p0, math::point const & p1, float w0, float w1, color const & c0, color const & c1, bool smooth = true); void besier(math::point const & p0, math::point const & p1, math::point const & p2, float width, color const & c, int quality = 8, bool smooth = true); diff --git a/libs/gfx/include/psemek/gfx/pixmap.hpp b/libs/gfx/include/psemek/gfx/pixmap.hpp index 00566687..524d28bc 100644 --- a/libs/gfx/include/psemek/gfx/pixmap.hpp +++ b/libs/gfx/include/psemek/gfx/pixmap.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include @@ -10,7 +10,7 @@ namespace psemek::gfx { template - using basic_pixmap = util::array; + using basic_pixmap = util::ndarray; using pixmap_monochrome = basic_pixmap; using pixmap_rgb = basic_pixmap; @@ -48,7 +48,7 @@ namespace psemek::gfx // Utilities template - auto to_srgb(util::array pm, float g = 1.f / 2.2f) + auto to_srgb(util::ndarray pm, float g = 1.f / 2.2f) { for (auto & c : pm) c = to_srgb(c, g); @@ -56,7 +56,7 @@ namespace psemek::gfx } template - auto to_linear(util::array pm, float g = 1.f / 2.2f) + auto to_linear(util::ndarray pm, float g = 1.f / 2.2f) { return to_srgb(std::move(pm), 1.f / g); } diff --git a/libs/gfx/include/psemek/gfx/texture.hpp b/libs/gfx/include/psemek/gfx/texture.hpp index 88cb4f09..de20fa8f 100644 --- a/libs/gfx/include/psemek/gfx/texture.hpp +++ b/libs/gfx/include/psemek/gfx/texture.hpp @@ -54,10 +54,10 @@ namespace psemek::gfx void load_srgb(math::vector const & size, Pixel const * data = nullptr); template - void load(util::array const & p); + void load(util::ndarray const & p); template - void load_srgb(util::array const & p); + void load_srgb(util::ndarray const & p); #ifndef PSEMEK_GLES void pixels(GLenum format, GLenum type, void * data, int layer = 0) const; @@ -142,7 +142,7 @@ namespace psemek::gfx } template - void load(int f, util::array const & p) + void load(int f, util::ndarray const & p) { math::vector size; for (std::size_t i = 0; i < 2; ++i) size[i] = p.dim(i); @@ -392,7 +392,7 @@ namespace psemek::gfx template template - void basic_texture::load(util::array const & p) + void basic_texture::load(util::ndarray const & p) { math::vector size; for (std::size_t i = 0; i < D; ++i) size[i] = p.dim(i); @@ -401,7 +401,7 @@ namespace psemek::gfx template template - void basic_texture::load_srgb(util::array const & p) + void basic_texture::load_srgb(util::ndarray const & p) { math::vector size; for (std::size_t i = 0; i < D; ++i) size[i] = p.dim(i); diff --git a/libs/gfx/include/psemek/gfx/texture_atlas.hpp b/libs/gfx/include/psemek/gfx/texture_atlas.hpp index b46d81b4..0060ba42 100644 --- a/libs/gfx/include/psemek/gfx/texture_atlas.hpp +++ b/libs/gfx/include/psemek/gfx/texture_atlas.hpp @@ -19,9 +19,9 @@ namespace psemek::gfx texture_2d & texture() { return texture_; } texture_2d const & texture() const { return texture_; } - util::array const & pixmap() const { return atlas_.array(); } + util::ndarray const & pixmap() const { return atlas_.array(); } - std::pair insert(Key const & key, util::array const & data); + std::pair insert(Key const & key, util::ndarray const & data); texture_view find(Key const & key) const; texture_view at(Key const & key) const; @@ -43,7 +43,7 @@ namespace psemek::gfx {} template - std::pair texture_atlas_2d::insert(Key const & key, util::array const & data) + std::pair texture_atlas_2d::insert(Key const & key, util::ndarray const & data) { auto result = atlas_.insert(key, data); if (result.second) diff --git a/libs/gfx/source/gltf_animation.cpp b/libs/gfx/source/gltf_animation.cpp index bfd28800..efb2f351 100644 --- a/libs/gfx/source/gltf_animation.cpp +++ b/libs/gfx/source/gltf_animation.cpp @@ -90,11 +90,9 @@ namespace psemek::gfx return scale_.range() | rotation_.range() | translation_.range(); } - math::affine_transform gltf_animation::operator()(float time) const + math::trs gltf_animation::operator()(float time) const { - return math::translation(translation_(time)).transform() - * math::quaternion_rotation(rotation_(time)).transform() - * math::scale(scale_(time)).transform(); + return {translation_(time), rotation_(time), scale_(time)}; } } diff --git a/libs/gfx/source/mesh.cpp b/libs/gfx/source/mesh.cpp index 455225b0..6cb2afa7 100644 --- a/libs/gfx/source/mesh.cpp +++ b/libs/gfx/source/mesh.cpp @@ -144,11 +144,8 @@ namespace psemek::gfx assert(vertex_buffer_); if (!index_buffer_) - { - index_buffer_ = buffer{}; - array_.bind(); - gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, index_buffer_.id()); - } + index_buffer_ = {}; + array_.bind(); vertex_buffer_.load(vertices, vertex_size * vertex_count, usage); index_buffer_.load(indices, index_size(index_type) * index_count, usage); info_.vertex_count_ = vertex_count; diff --git a/libs/gfx/source/painter.cpp b/libs/gfx/source/painter.cpp index 051d9db1..28bbac6d 100644 --- a/libs/gfx/source/painter.cpp +++ b/libs/gfx/source/painter.cpp @@ -216,14 +216,19 @@ namespace psemek::gfx } void painter::circle(math::point const & p, float r, color const & c, int quality) + { + circle(p, r, c, c, quality); + } + + void painter::circle(math::point const & p, float r, color const & c0, color const & c1, int quality) { std::uint32_t const base = impl().vertices.size(); - impl().vertices.push_back({{p[0], p[1], 0.f}, c}); + impl().vertices.push_back({{p[0], p[1], 0.f}, c0}); for (int i = 0; i < quality; ++i) { float const a = (math::pi * 2.f * i) / quality; - impl().vertices.push_back({{p[0] + r * std::cos(a), p[1] + r * std::sin(a), 0.f}, c}); + impl().vertices.push_back({{p[0] + r * std::cos(a), p[1] + r * std::sin(a), 0.f}, c1}); } for (int i = 0; i < quality; ++i) @@ -301,18 +306,37 @@ namespace psemek::gfx math::vector painter::text_size(std::string_view str, font f) { // TODO: multiline text - math::vector s; + math::vector font_size; switch (f) { case font::font_9x12: - s = {9.f, 12.f}; + font_size = {9.f, 12.f}; break; default: throw util::unknown_enum_value_exception(f); } - s[0] *= str.size(); - return s; + int max_line_width = 0; + int line_count = 0; + + int last_line_start = 0; + for (int i = 0; i < str.size(); ++i) + { + if (str[i] == '\n') + { + math::make_max(max_line_width, i - last_line_start); + last_line_start = i; + ++line_count; + } + } + + if (last_line_start + 1 != str.size()) + { + math::make_max(max_line_width, static_cast(str.size()) - last_line_start); + ++line_count; + } + + return {font_size[0] * max_line_width, font_size[1] * line_count}; } void painter::text(math::point const & p, std::string_view str, text_options const & opts) @@ -489,17 +513,17 @@ namespace psemek::gfx { auto const size = math::pointwise_mult(text_size(str, opts.f), opts.scale); - math::vector pen { 0.f, 0.f, 0.f }; + math::vector origin { 0.f, 0.f, 0.f }; switch (opts.x) { case x_align::left: break; case x_align::center: - pen[0] -= size[0] / 2.f; + origin[0] -= size[0] / 2.f; break; case x_align::right: - pen[0] -= size[0]; + origin[0] -= size[0]; break; default: throw util::unknown_enum_value_exception(opts.x); @@ -510,15 +534,17 @@ namespace psemek::gfx case y_align::top: break; case y_align::center: - pen[1] -= size[1] / 2.f; + origin[1] -= size[1] / 2.f; break; case y_align::bottom: - pen[1] -= size[1]; + origin[1] -= size[1]; break; default: throw util::unknown_enum_value_exception(opts.y); } + auto pen = origin; + math::vector const sx = {9.f * opts.scale[0], 0.f, 0.f}; math::vector const sy = {0.f, 12.f * opts.scale[1], 0.f}; @@ -529,6 +555,13 @@ namespace psemek::gfx for (char c : str) { + if (c == '\n') + { + pen[0] = origin[0]; + pen += sy; + continue; + } + // Guard against unsigned char #ifdef __GNUC__ #pragma GCC diagnostic push diff --git a/libs/gfx/source/program.cpp b/libs/gfx/source/program.cpp index 05668990..a092048a 100644 --- a/libs/gfx/source/program.cpp +++ b/libs/gfx/source/program.cpp @@ -153,47 +153,47 @@ namespace psemek::gfx void program::uniform_proxy::operator = (math::matrix const & m) { - gl::UniformMatrix2fv(location_, 1, gl::TRUE, m.coords); + gl::UniformMatrix2fv(location_, 1, gl::TRUE, &m[0][0]); } void program::uniform_proxy::operator = (math::matrix const & m) { - gl::UniformMatrix3x2fv(location_, 1, gl::TRUE, m.coords); + gl::UniformMatrix3x2fv(location_, 1, gl::TRUE, &m[0][0]); } void program::uniform_proxy::operator = (math::matrix const & m) { - gl::UniformMatrix4x2fv(location_, 1, gl::TRUE, m.coords); + gl::UniformMatrix4x2fv(location_, 1, gl::TRUE, &m[0][0]); } void program::uniform_proxy::operator = (math::matrix const & m) { - gl::UniformMatrix2x3fv(location_, 1, gl::TRUE, m.coords); + gl::UniformMatrix2x3fv(location_, 1, gl::TRUE, &m[0][0]); } void program::uniform_proxy::operator = (math::matrix const & m) { - gl::UniformMatrix3fv(location_, 1, gl::TRUE, m.coords); + gl::UniformMatrix3fv(location_, 1, gl::TRUE, &m[0][0]); } void program::uniform_proxy::operator = (math::matrix const & m) { - gl::UniformMatrix4x3fv(location_, 1, gl::TRUE, m.coords); + gl::UniformMatrix4x3fv(location_, 1, gl::TRUE, &m[0][0]); } void program::uniform_proxy::operator = (math::matrix const & m) { - gl::UniformMatrix2x4fv(location_, 1, gl::TRUE, m.coords); + gl::UniformMatrix2x4fv(location_, 1, gl::TRUE, &m[0][0]); } void program::uniform_proxy::operator = (math::matrix const & m) { - gl::UniformMatrix3x4fv(location_, 1, gl::TRUE, m.coords); + gl::UniformMatrix3x4fv(location_, 1, gl::TRUE, &m[0][0]); } void program::uniform_proxy::operator = (math::matrix const & m) { - gl::UniformMatrix4fv(location_, 1, gl::TRUE, m.coords); + gl::UniformMatrix4fv(location_, 1, gl::TRUE, &m[0][0]); } void program::uniform_proxy::operator = (math::interval const & i) diff --git a/libs/gfx/source/renderer/deferred.cpp b/libs/gfx/source/renderer/deferred.cpp index 03d6be49..f96a9810 100644 --- a/libs/gfx/source/renderer/deferred.cpp +++ b/libs/gfx/source/renderer/deferred.cpp @@ -1069,7 +1069,7 @@ void main(){} camera_distance[i] = dist; } - util::array bins({opts.grid_size[0], opts.grid_size[1], opts.grid_size[2]}); + util::ndarray bins({opts.grid_size[0], opts.grid_size[1], opts.grid_size[2]}); math::box lit_bbox; math::box casts_shadow_bbox; diff --git a/libs/group/include/psemek/group/cyclic.hpp b/libs/group/include/psemek/group/cyclic.hpp index aad2a145..132c66e2 100644 --- a/libs/group/include/psemek/group/cyclic.hpp +++ b/libs/group/include/psemek/group/cyclic.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -55,6 +56,14 @@ namespace psemek::group return repr_; } + group::permutation permutation() const + { + typename group::permutation::repr_type repr; + for (Repr i = 0; i < N; ++i) + repr[i] = (i + repr_) % N; + return group::permutation::from_repr(repr); + } + struct value_iterator { using difference_type = Repr; diff --git a/libs/group/include/psemek/group/dihedral.hpp b/libs/group/include/psemek/group/dihedral.hpp index a1a5e364..cc199a35 100644 --- a/libs/group/include/psemek/group/dihedral.hpp +++ b/libs/group/include/psemek/group/dihedral.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -87,6 +88,25 @@ namespace psemek::group return repr_ >= N; } + group::permutation permutation() const + { + if (repr_ < N) + { + typename group::permutation::repr_type repr; + for (Repr i = 0; i < N; ++i) + repr[i] = (i + repr_) % N; + return group::permutation::from_repr(repr); + } + else + { + typename group::permutation::repr_type repr; + for (Repr i = 0; i < N; ++i) + repr[i] = (N - i) % N; + + return rotation(repr_ % N).permutation() * group::permutation::from_repr(repr); + } + } + struct value_iterator { using difference_type = Repr; diff --git a/libs/group/include/psemek/group/permutation.hpp b/libs/group/include/psemek/group/permutation.hpp new file mode 100644 index 00000000..f8efffa1 --- /dev/null +++ b/libs/group/include/psemek/group/permutation.hpp @@ -0,0 +1,154 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace psemek::group +{ + + template + struct permutation + { + static_assert(std::is_integral_v && std::is_unsigned_v); + static_assert(std::numeric_limits::max() > N); + + using repr_type = std::array; + + static constexpr std::size_t size() + { + return N; + } + + permutation() = default; + + static permutation identity() + { + return permutation{}; + } + + static permutation from_repr(repr_type const & value) + { + return permutation{value}; + } + + repr_type const & value() const + { + return repr_; + } + + repr_type operator() (repr_type const & value) const + { + repr_type result; + for (Repr i = 0; i < N; ++i) + result[repr_[i]] = value[i]; + return result; + } + + struct value_iterator + { + using difference_type = Repr; + using value_type = permutation; + using pointer = value_type *; + using reference = value_type &; + using iterator_category = std::forward_iterator_tag; + + repr_type repr; + bool end; + + permutation operator *() const + { + return permutation{repr}; + } + + value_iterator & operator++() + { + end = std::next_permutation(repr.begin(), repr.end()); + return *this; + } + + friend bool operator == (value_iterator const & it1, value_iterator const & it2) + { + return it1.repr == it2.repr && it1.end == it2.end; + } + }; + + static auto values() + { + repr_type repr; + std::iota(repr.begin(), repr.end(), Repr{0}); + return util::range{value_iterator{repr, false}, value_iterator{repr, true}}; + } + + private: + repr_type repr_{0}; + + explicit permutation(repr_type const & repr) + : repr_(repr) + {} + }; + + template + bool operator == (permutation const & g1, permutation const & g2) + { + return g1.value() == g2.value(); + } + + template + auto operator <=> (permutation const & g1, permutation const & g2) + { + return g1.value() <=> g2.value(); + } + + template + permutation operator * (permutation const & g1, permutation const & g2) + { + typename permutation::repr_type repr; + for (Repr i = 0; i < N; ++i) + repr[i] = g1.value()[g2.value()[i]]; + return permutation::from_repr(repr); + } + + template + permutation inverse(permutation const & g) + { + typename permutation::repr_type repr; + for (Repr i = 0; i < N; ++i) + repr[g.value()[i]] = i; + return permutation::from_repr(repr); + } + + template + OStream & operator << (OStream & os, permutation const & g) + { + os << 'S' << N << '('; + for (Repr i = 0; i < N; ++i) + { + if (i > 0) + os << ", "; + os << g.value()[i]; + } + os << ')'; + return os; + } + + template + void write(OStream & out, permutation const & g) + { + write(out, g.value()); + } + + template + void read(IStream & in, permutation & g) + { + typename permutation::repr_type value; + read(in, value); + g = permutation::from_repr(value); + } + +} diff --git a/libs/io/source/file_stream.cpp b/libs/io/source/file_stream.cpp index 7c1cc481..5e0a5b7e 100644 --- a/libs/io/source/file_stream.cpp +++ b/libs/io/source/file_stream.cpp @@ -1,27 +1,72 @@ #include #include +#include #include -#include namespace psemek::io { - static void throw_fopen [[noreturn]] (std::filesystem::path const & path) + namespace { - throw util::system_error(std::error_code{errno, std::system_category()}, "Failed to open " + path.string()); - } - static FILE * safe_fopen(std::filesystem::path const & path, const char * mode) - { - std::string path_str = path.string(); - auto f = std::fopen(path_str.c_str(), mode); - if (!f) throw_fopen(path); - return f; + void throw_fopen [[noreturn]] (std::filesystem::path const & path) + { + throw util::system_error(std::error_code{errno, std::system_category()}, "Failed to open " + path.string()); + } + +#ifdef _WIN32 + wchar_t const * fopen_read_mode() + { + return L"rb"; + } + + wchar_t const * fopen_write_mode(unsigned flags) + { + switch (flags) + { + case 0: return L"wb"; + case file_ostream::append: return L"ab"; + } + + throw util::exception("Unknown file_ostream open flags"); + } + + FILE * safe_fopen(std::filesystem::path const & path, wchar_t const * mode) + { + auto f = _wfopen(path.c_str(), mode); + if (!f) throw_fopen(path); + return f; + } +#else + char const * fopen_read_mode() + { + return "rb"; + } + + char const * fopen_write_mode(unsigned flags) + { + switch (flags) + { + case 0: return "wb"; + case file_ostream::append: return "ab"; + } + + throw util::exception("Unknown file_ostream open flags"); + } + + FILE * safe_fopen(std::filesystem::path const & path, char const * mode) + { + auto f = std::fopen(path.c_str(), mode); + if (!f) throw_fopen(path); + return f; + } +#endif + } file_istream::file_istream(std::filesystem::path const & path) - : file_{safe_fopen(path.c_str(), "rb")} + : file_{safe_fopen(path, fopen_read_mode())} {} void file_istream::reset() @@ -43,18 +88,8 @@ namespace psemek::io return std::feof(file_) != 0; } - static char const * fopen_write_mode(unsigned flags) - { - switch (flags) - { - case 0: return "wb"; - case file_ostream::append: return "ab"; - default: throw util::exception("Unknown file_ostream open flags"); - } - } - file_ostream::file_ostream(std::filesystem::path const & path, unsigned flags) - : file_{safe_fopen(path.c_str(), fopen_write_mode(flags))} + : file_{safe_fopen(path, fopen_write_mode(flags))} {} void file_ostream::reset() diff --git a/libs/journal/CMakeLists.txt b/libs/journal/CMakeLists.txt new file mode 100644 index 00000000..33a6feea --- /dev/null +++ b/libs/journal/CMakeLists.txt @@ -0,0 +1,6 @@ +file(GLOB_RECURSE PSEMEK_JOURNAL_HEADERS RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "include/*.hpp") +file(GLOB_RECURSE PSEMEK_JOURNAL_SOURCES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "source/*.cpp") + +psemek_add_library(psemek-journal ${PSEMEK_JOURNAL_HEADERS} ${PSEMEK_JOURNAL_SOURCES}) +target_include_directories(psemek-journal PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" ${SQLite3_INCLUDE_DIRS}) +target_link_libraries(psemek-journal PUBLIC psemek-util psemek-log) diff --git a/libs/journal/include/psemek/journal/event.hpp b/libs/journal/include/psemek/journal/event.hpp new file mode 100644 index 00000000..7a8a7272 --- /dev/null +++ b/libs/journal/include/psemek/journal/event.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +namespace psemek::journal +{ + + struct event_metadata + { + std::string source_file; + int source_line; + std::string name; + std::vector columns; + }; + + struct event_data + { + std::string time; + std::vector values; + }; + + struct event + { + event_metadata metadata; + event_data data; + }; + + std::string current_time(); + +} diff --git a/libs/journal/include/psemek/journal/journal.hpp b/libs/journal/include/psemek/journal/journal.hpp new file mode 100644 index 00000000..b81a35da --- /dev/null +++ b/libs/journal/include/psemek/journal/journal.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +namespace psemek::journal +{ + + struct journal + { + journal(); + ~journal(); + + bool enabled() const; + void set_enabled(bool enabled); + + void log_event(event const & event); + + std::vector> select(); + + private: + psemek_declare_pimpl + }; + +} diff --git a/libs/journal/include/psemek/journal/log_event.hpp b/libs/journal/include/psemek/journal/log_event.hpp new file mode 100644 index 00000000..b1dc8f12 --- /dev/null +++ b/libs/journal/include/psemek/journal/log_event.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include + +#include + +#define psemek_journal_log_event_extract_key(key, value) key +#define psemek_journal_log_event_extract_value(key, value) std::format("{}", value) + +#define psemek_journal_log_event_single_attribute_key(r, data, elem) psemek_journal_log_event_extract_key elem, +#define psemek_journal_log_event_single_attribute_value(r, data, elem) psemek_journal_log_event_extract_value elem, + +#define psemek_journal_log_event(JOURNAL, NAME, ATTRIBUTES) \ + if ((JOURNAL).enabled()) \ + (JOURNAL).log_event({{ \ + .source_file = __FILE__, \ + .source_line = __LINE__, \ + .name = NAME, \ + .columns = { BOOST_PP_SEQ_FOR_EACH(psemek_journal_log_event_single_attribute_key, _, ATTRIBUTES) }, \ + }, { \ + .time = ::psemek::journal::current_time(), \ + .values = { BOOST_PP_SEQ_FOR_EACH(psemek_journal_log_event_single_attribute_value, _, ATTRIBUTES) }, \ + }}) diff --git a/libs/journal/source/event.cpp b/libs/journal/source/event.cpp new file mode 100644 index 00000000..4ebc68c6 --- /dev/null +++ b/libs/journal/source/event.cpp @@ -0,0 +1,15 @@ +#include + +#include +#include + +namespace psemek::journal +{ + + std::string current_time() + { + const auto now = std::chrono::high_resolution_clock::now(); + return std::format("{:%FT%TZ}", now); + } + +} diff --git a/libs/journal/source/journal.cpp b/libs/journal/source/journal.cpp new file mode 100644 index 00000000..4c252344 --- /dev/null +++ b/libs/journal/source/journal.cpp @@ -0,0 +1,100 @@ +#include +#include + +#include + +namespace psemek::journal +{ + + struct journal::impl + { + struct table + { + event_metadata metadata; + mutable std::vector events; + }; + + struct table_hash + { + std::uint64_t operator()(std::string const & name) const noexcept + { + return std::hash{}(name); + } + + std::uint64_t operator()(table const & table) const noexcept + { + return std::hash{}(table.metadata.name); + } + }; + + struct table_equal + { + bool operator()(std::string const & name1, table const & table2) const noexcept + { + return name1 == table2.metadata.name; + } + + bool operator()(table const & table1, table const & table2) const noexcept + { + return table1.metadata.name == table2.metadata.name; + } + }; + + bool enabled = true; + util::hash_set tables; + + void log_event(event const & event) + { + if (!enabled) + return; + + auto table_it = tables.find(event.metadata.name); + if (table_it == tables.end()) + table_it = tables.insert(table{event.metadata, {}}).first; + table_it->events.push_back(event.data); + } + + std::vector> select() + { + if (!enabled) + return {}; + + std::vector> result; + + for (auto const & table : tables) + for (auto const & event : table.events) + result.push_back({&table.metadata, &event}); + + std::sort(result.begin(), result.end(), [](auto const & e1, auto const & e2){ return e1.second->time < e2.second->time; }); + + return result; + } + }; + + journal::journal() + : pimpl_(make_impl()) + {} + + journal::~journal() = default; + + bool journal::enabled() const + { + return impl().enabled; + } + + void journal::set_enabled(bool enabled) + { + impl().enabled = enabled; + } + + void journal::log_event(event const & event) + { + impl().log_event(event); + } + + std::vector> journal::select() + { + return impl().select(); + } + +} diff --git a/libs/log/source/log.cpp b/libs/log/source/log.cpp index 448d1ea3..a5eab065 100644 --- a/libs/log/source/log.cpp +++ b/libs/log/source/log.cpp @@ -24,7 +24,7 @@ namespace psemek::log std::atomic max_thread_name_length = 3; std::mutex thread_names_mutex; - util::hash_map thread_names; + util::hash_map> thread_names; std::mutex sinks_mutex; std::vector> sinks; @@ -165,10 +165,14 @@ namespace psemek::log if (it != thread_names.end()) throw util::exception("Thread \"" + name + "\" already registered!"); - thread_names[id] = name; + thread_names[id] = std::make_unique(name); } - max_thread_name_length = std::max(max_thread_name_length.load(), name.size()); + { + std::size_t current_max = max_thread_name_length.load(); + while(current_max < name.size() && !max_thread_name_length.compare_exchange_weak(current_max, name.size())) + {} + } put_message(level::info, "Thread \"" + name + "\" registered"); } @@ -187,7 +191,7 @@ namespace psemek::log throw util::exception(os.str()); } - name = std::move(it->second); + name = std::move(*(it->second)); thread_names.erase(it); } @@ -238,7 +242,7 @@ namespace psemek::log if (it == thread_names.end()) thread_name = &unknown_thread_name; else - thread_name = &(it->second); + thread_name = it->second.get(); } put_message(l, message, *thread_name); diff --git a/libs/math/CMakeLists.txt b/libs/math/CMakeLists.txt index 719f67de..a4d3514c 100644 --- a/libs/math/CMakeLists.txt +++ b/libs/math/CMakeLists.txt @@ -1,9 +1,7 @@ option(PSEMEK_ROBUST_PREDICATES "Use robust geometric predicates" OFF) -if(NOT (CMAKE_SYSTEM_NAME STREQUAL Emscripten)) - find_package(Boost REQUIRED) -endif() if(PSEMEK_ROBUST_PREDICATES) + find_package(Boost REQUIRED CONFIG) find_package(GMP REQUIRED) endif() @@ -12,8 +10,9 @@ file(GLOB_RECURSE PSEMEK_GEOM_SOURCES "source/*.cpp") psemek_add_library(psemek-math ${PSEMEK_GEOM_HEADERS} ${PSEMEK_GEOM_SOURCES}) target_include_directories(psemek-math PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") -target_link_libraries(psemek-math PUBLIC psemek-util psemek-group Boost::boost) +target_link_libraries(psemek-math PUBLIC psemek-util psemek-group) if(PSEMEK_ROBUST_PREDICATES) + target_link_libraries(psemek-math PUBLIC Boost::boost) target_link_libraries(psemek-math PUBLIC gmp) endif() diff --git a/libs/math/include/psemek/math/box.hpp b/libs/math/include/psemek/math/box.hpp index 18038144..80c9a944 100644 --- a/libs/math/include/psemek/math/box.hpp +++ b/libs/math/include/psemek/math/box.hpp @@ -1,10 +1,10 @@ #pragma once -#include #include #include #include +#include namespace psemek::math { @@ -12,7 +12,7 @@ namespace psemek::math template struct box { - typename detail::array, N>::type axes; + interval axes[N]; using point_type = point; using vector_type = vector; @@ -348,3 +348,26 @@ namespace psemek::math } } + +namespace std +{ + + template + struct formatter<::psemek::math::box, Char> + : formatter<::psemek::math::interval, Char> + { + using formatter<::psemek::math::interval, Char>::parse; + + template + auto format(::psemek::math::box const & b, FormatContext & ctx) const + { + for (std::size_t i = 0; i < b.dimension(); ++i) + { + if (i > 0) ctx.advance_to(std::format_to(ctx.out(), "x")); + ctx.advance_to(formatter<::psemek::math::interval, Char>::format(b[i], ctx)); + } + return ctx.out(); + } + }; + +} diff --git a/libs/math/include/psemek/math/concat.hpp b/libs/math/include/psemek/math/concat.hpp index 9da409fc..db871c53 100644 --- a/libs/math/include/psemek/math/concat.hpp +++ b/libs/math/include/psemek/math/concat.hpp @@ -1,18 +1,18 @@ #pragma once -#include +#include namespace psemek::math { - template - auto concat(vector const & ... vs) + template