From 6fc98f320e6e8763e27ad0fb9e8e3fd4aeb6e795 Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Mon, 23 Dec 2024 00:47:01 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20initial=20sprint-1=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .clang-format | 8 +- CMakeLists.txt | 3 +- ashura/bench/bench.cc | 1 + ashura/config.json | 41 +- ashura/config.schema.json | 202 +- ashura/editor/editor.cc | 98 +- ashura/engine/assets.h | 393 +- ashura/engine/canvas.cc | 516 +- ashura/engine/canvas.h | 86 +- ashura/engine/color.h | 26 +- ashura/engine/engine.cc | 620 ++- ashura/engine/engine.h | 68 +- ashura/engine/font.h | 92 +- ashura/engine/font_atlas.h | 42 - ashura/engine/font_impl.cc | 39 +- ashura/engine/font_impl.h | 374 +- ashura/engine/gpu_context.cc | 820 --- ashura/engine/gpu_context.h | 312 -- ashura/engine/gpu_system.cc | 845 +++ ashura/engine/gpu_system.h | 429 ++ ashura/engine/image_decoder.cc | 113 +- ashura/engine/image_decoder.h | 43 +- ashura/engine/input.h | 326 +- ashura/engine/light.h | 6 +- ashura/engine/passes.cc | 563 +- ashura/engine/passes.h | 200 +- ashura/engine/rect_pack.h | 737 +-- ashura/engine/render_text.h | 285 +- ashura/engine/renderer.cc | 44 +- ashura/engine/renderer.h | 123 +- ashura/engine/scene.h | 2 +- ashura/engine/shader.cc | 585 +-- ashura/engine/shader.h | 42 +- ashura/engine/tests/render_text.cc | 38 +- ashura/engine/tests/text_compositor.cc | 12 +- ashura/engine/text.cc | 142 +- ashura/engine/text.h | 8 +- ashura/engine/text_compositor.cc | 288 +- ashura/engine/text_compositor.h | 115 +- ashura/engine/view.h | 395 +- ashura/engine/view_system.h | 347 +- ashura/engine/views.cc | 40 +- ashura/engine/views.h | 1275 +++-- ashura/engine/window.cc | 202 +- ashura/engine/window.h | 8 +- ashura/gpu/gpu.cc | 2 +- ashura/gpu/gpu.h | 231 +- ashura/gpu/image.h | 4 +- ashura/gpu/vulkan.cc | 2476 +++++---- ashura/gpu/vulkan.h | 582 +- ashura/shaders/blur.ub.glsl | 19 +- ashura/shaders/core.glsl | 4 +- ashura/shaders/ngon.ub.glsl | 4 +- ashura/shaders/pbr.ub.glsl | 50 +- ashura/shaders/rrect.ub.glsl | 24 +- ashura/std/alias_count.h | 26 +- ashura/std/allocator.cc | 12 +- ashura/std/allocator.h | 37 +- ashura/std/allocators.cc | 2 +- ashura/std/allocators.h | 24 +- ashura/std/async.cc | 50 +- ashura/std/async.h | 175 +- ashura/std/bench/hash_map.cc | 2237 ++++---- ashura/std/buffer.h | 2 +- ashura/std/cfg.h | 48 +- ashura/std/dyn.h | 73 +- ashura/std/enum.gen.h | 6695 ++++++++++++++++++++---- ashura/std/enum.h | 2 +- ashura/std/enum.py | 175 +- ashura/std/error.h | 28 +- ashura/std/format.cc | 12 +- ashura/std/format.h | 9 +- ashura/std/fs.cc | 2 +- ashura/std/fs.h | 5 +- ashura/std/hash.cc | 2 +- ashura/std/hash.h | 2 +- ashura/std/image.h | 137 +- ashura/std/index_pack.gen.h | 128 +- ashura/std/index_pack.h | 4 +- ashura/std/index_pack.py | 12 +- ashura/std/lambda.h | 12 +- ashura/std/list.h | 12 +- ashura/std/locale.h | 4 +- ashura/std/log.cc | 2 +- ashura/std/log.h | 6 +- ashura/std/map.h | 75 +- ashura/std/math.h | 455 +- ashura/std/mem.h | 12 +- ashura/std/obj.h | 4 +- ashura/std/option.h | 291 +- ashura/std/panic.cc | 6 +- ashura/std/panic.h | 2 +- ashura/std/range.h | 42 +- ashura/std/rc.h | 143 +- ashura/std/result.h | 68 +- ashura/std/simd.h | 38 - ashura/std/str.h | 4 +- ashura/std/super.h | 34 +- ashura/std/tests/async.cc | 48 +- ashura/std/tests/option.cc | 340 +- ashura/std/text.h | 6 +- ashura/std/time.h | 2 +- ashura/std/traits.h | 52 +- ashura/std/tuple.gen.h | 1576 +++++- ashura/std/tuple.h | 4 +- ashura/std/tuple.py | 39 +- ashura/std/types.h | 134 +- ashura/std/v.gen.h | 16 + ashura/std/v.py | 4 +- ashura/std/vec.h | 308 +- 110 files changed, 17215 insertions(+), 10773 deletions(-) create mode 100644 ashura/bench/bench.cc delete mode 100644 ashura/engine/font_atlas.h delete mode 100644 ashura/engine/gpu_context.cc delete mode 100644 ashura/engine/gpu_context.h create mode 100644 ashura/engine/gpu_system.cc create mode 100644 ashura/engine/gpu_system.h delete mode 100644 ashura/std/simd.h diff --git a/.clang-format b/.clang-format index f6b8d98ec..e0422e5d3 100644 --- a/.clang-format +++ b/.clang-format @@ -62,8 +62,8 @@ ColumnLimit: 80 CommentPragmas: "^ IWYU pragma:" CompactNamespaces: false ConstructorInitializerAllOnOneLineOrOnePerLine: true -ConstructorInitializerIndentWidth: 4 -ContinuationIndentWidth: 4 +ConstructorInitializerIndentWidth: 2 +ContinuationIndentWidth: 2 Cpp11BracedListStyle: true DerivePointerAlignment: false DisableFormat: false @@ -124,7 +124,7 @@ SpaceBeforeParensOptions: BeforeNonEmptyParentheses: false SpaceBeforeRangeBasedForLoopColon: true SpaceBeforeSquareBrackets: false -SpacesBeforeTrailingComments: 8 +SpacesBeforeTrailingComments: 4 SpacesInAngles: Never SpacesInContainerLiterals: false SpacesInLineCommentPrefix: @@ -138,4 +138,4 @@ SpacesInParensOptions: InEmptyParentheses: false SpacesInSquareBrackets: false Standard: c++20 -TabWidth: 4 \ No newline at end of file +TabWidth: 2 \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index e4c525f27..c4504c5f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,7 +38,6 @@ set(SDL_HAPTIC OFF CACHE BOOL "") - include(FetchContent) set(ASH_VULKAN_VERSION "1.3.296.0") @@ -215,7 +214,7 @@ add_library( ashura/engine/font_impl.cc ashura/engine/text.cc ashura/engine/canvas.cc - ashura/engine/gpu_context.cc + ashura/engine/gpu_system.cc ashura/engine/image_decoder.cc ashura/engine/render_text.cc ashura/engine/shader.cc diff --git a/ashura/bench/bench.cc b/ashura/bench/bench.cc new file mode 100644 index 000000000..67f626e7d --- /dev/null +++ b/ashura/bench/bench.cc @@ -0,0 +1 @@ +// use cpu perf counters \ No newline at end of file diff --git a/ashura/config.json b/ashura/config.json index 92ac56006..b61dd4e6c 100644 --- a/ashura/config.json +++ b/ashura/config.json @@ -1,26 +1,25 @@ { "$schema": "./config.schema.json", "version": "0.0.1", - "gpu": { - "validation": false, - "vsync": true, - "preferences": [ - "dgpu", - "vgpu", - "igpu", - "other", - "cpu" - ], - "hdr": true, - "buffering": 2 - }, - "window": { - "resizable": true, - "full_screen": false, - "maximized": false, - "width": 1920, - "height": 1080 - }, + "gpu.validation": false, + "gpu.vsync": false, + "gpu.preferences": [ + "dgpu", + "vgpu", + "igpu", + "other", + "cpu" + ], + "gpu.hdr": true, + "gpu.buffering": 2, + "gpu.antialiasing": "msaa", + "gpu.msaa.level": 4, + "window.resizable": true, + "window.full_screen": false, + "window.maximized": false, + "window.borderless": false, + "window.width": 1500, + "window.height": 960, "shaders": { "Blur/DownSample:VS": "shaders/blur_downsample.vert.spv", "Blur/DownSample:FS": "shaders/blur_downsample.frag.spv", @@ -39,6 +38,6 @@ "Roboto": "fonts/Roboto/Roboto-Regular.ttf", "RobotoMono": "fonts/RobotoMono/RobotoMono-Regular.ttf" }, - "default_font": "RobotoMono", + "fonts.default": "RobotoMono", "images": {} } \ No newline at end of file diff --git a/ashura/config.schema.json b/ashura/config.schema.json index ff9fa3a32..f86bb5761 100644 --- a/ashura/config.schema.json +++ b/ashura/config.schema.json @@ -8,105 +8,102 @@ "description": "Version of the engine this config file is made for. This will be used in inter-engine version conversion", "type": "string" }, - "gpu": { - "description": "GPU Configuration", - "type": "object", - "properties": { - "validation": { - "description": "Enable GPU validation layers", - "type": "boolean", - "default": false - }, - "vsync": { - "description": "Enable GPU VSync", - "type": "boolean", - "default": false - }, - "preferences": { - "description": "Device preferences, selected in the specified order", - "type": "array", - "items": { - "enum": [ - "dgpu", - "vgpu", - "igpu", - "other", - "cpu" - ] - }, - "minItems": 1, - "maxItems": 5, - "default": [ - "dgpu", - "vgpu", - "igpu", - "other", - "cpu" - ] - }, - "hdr": { - "description": "Use HDR", - "type": "boolean", - "default": true - }, - "buffering": { - "description": "GPU Swapchain Buffering", - "type": "integer", - "minimum": 0, - "maximum": 4, - "default": 2 - } + "gpu.validation": { + "description": "Enable GPU validation layers", + "type": "boolean", + "default": false + }, + "gpu.vsync": { + "description": "Enable GPU VSync", + "type": "boolean", + "default": false + }, + "gpu.preferences": { + "description": "Device preferences, selected in the specified order", + "type": "array", + "items": { + "enum": [ + "dgpu", + "vgpu", + "igpu", + "other", + "cpu" + ] }, - "additionalProperties": false, - "required": [ - "validation", - "vsync", - "preferences", - "hdr", - "buffering" + "minItems": 1, + "maxItems": 5, + "default": [ + "dgpu", + "vgpu", + "igpu", + "other", + "cpu" ] }, - "window": { - "description": "Window configuration", - "type": "object", - "properties": { - "resizable": { - "description": "Enable window resizing", - "type": "boolean", - "default": true - }, - "maximized": { - "description": "Maximize window during startup", - "type": "boolean", - "default": false - }, - "full_screen": { - "description": "Set window to full-screen during startup", - "type": "boolean", - "default": false - }, - "width": { - "description": "Initial width of the window", - "type": "integer", - "default": 1920, - "minimum": 0, - "maximum": 8192 - }, - "height": { - "description": "Initial height of the window", - "type": "integer", - "default": 1080, - "maximum": 8192 - } - }, - "additionalProperties": false, - "required": [ - "resizable", - "maximized", - "width", - "height" + "gpu.hdr": { + "description": "Use HDR", + "type": "boolean", + "default": true + }, + "gpu.buffering": { + "description": "GPU Swapchain Buffering", + "type": "integer", + "minimum": 0, + "maximum": 4, + "default": 2 + }, + "gpu.antialiasing": { + "description": "Antialiasing Technique to Use", + "type": "string", + "enum": [ + "msaa" ] }, + "gpu.msaa.level": { + "description": "MSAA Sample Count", + "type": "number", + "enum": [ + 1, + 2, + 4, + 8, + 16 + ], + "default": 4 + }, + "window.resizable": { + "description": "Enable window resizing", + "type": "boolean", + "default": true + }, + "window.maximized": { + "description": "Maximize window during startup", + "type": "boolean", + "default": false + }, + "window.full_screen": { + "description": "Set window to full-screen during startup", + "type": "boolean", + "default": false + }, + "window.borderless": { + "description": "Make window borderless", + "type": "boolean", + "default": false + }, + "window.width": { + "description": "Initial width of the window", + "type": "integer", + "default": 1920, + "minimum": 0, + "maximum": 8192 + }, + "window.height": { + "description": "Initial height of the window", + "type": "integer", + "default": 1080, + "maximum": 8192 + }, "shaders": { "description": "Locations of the compiled shaders to be loaded at startup", "type": "object" @@ -115,7 +112,7 @@ "description": "Locations of the fonts to be loaded at startup", "type": "object" }, - "default_font": { + "fonts.default": { "description": "The default font for the Engine", "type": "string" }, @@ -127,11 +124,20 @@ "additionalProperties": false, "required": [ "version", - "gpu", - "window", + "gpu.validation", + "gpu.vsync", + "gpu.preferences", + "gpu.hdr", + "gpu.buffering", + "gpu.msaa.level", + "window.resizable", + "window.maximized", + "window.borderless", + "window.width", + "window.height", "shaders", "fonts", - "default_font", + "fonts.default", "images" ] } \ No newline at end of file diff --git a/ashura/editor/editor.cc b/ashura/editor/editor.cc index 389de7564..01733b20c 100644 --- a/ashura/editor/editor.cc +++ b/ashura/editor/editor.cc @@ -1,6 +1,7 @@ #include "ashura/engine/animation.h" #include "ashura/engine/engine.h" #include "ashura/engine/views.h" +#include "ashura/std/async.h" struct Animated; @@ -14,9 +15,9 @@ int main() span({2ms, 2ms, 2ms, 2ms})); Engine::init( - default_allocator, nullptr, - R"(C:\Users\rlama\Documents\workspace\oss\ashura\ashura\config.json)"_str, - R"(C:\Users\rlama\Documents\workspace\oss\ashura\assets)"_str); + default_allocator, nullptr, + R"(C:\Users\rlama\Documents\workspace\oss\ashura\ashura\config.json)"_str, + R"(C:\Users\rlama\Documents\workspace\oss\ashura\assets)"_str); // [ ] revamp views // [ ] forward pointer and key events to views @@ -25,8 +26,8 @@ int main() ScrollView view; TextButton btn; - TextButton btn_home; TextButton btn2; + TextButton btn3; Switch sw[4]; Slider slider; ScalarBox scalar; @@ -35,57 +36,60 @@ int main() sw[0].on(); - scalar.frame(250, 100); - - slider.range(0, 100).interp(0).axis(Axis::Y); - - btn.text(U"replay RELOAD"_str) - .style(TextStyle{.foreground = colors::WHITE, .background = colors::BLUE}, - FontStyle{.font = engine->assets.fonts["RobotoMono"_str].get(), - .font_height = 50, - .line_height = 1.2F}) - .style(TextStyle{.foreground = colors::WHITE, .background = colors::BLUE}, - FontStyle{.font = engine->assets.fonts["MaterialIcons"_str].get(), - .font_height = 40, - .line_height = 1}, - 0, 6) - .padding(10, 10); - - btn_home.text(U"home HOME"_str) - .style(TextStyle{.foreground = colors::WHITE, .background = colors::BLUE}, - FontStyle{.font = engine->assets.fonts["RobotoMono"_str].get(), - .font_height = 50, - .line_height = 1.2F}) - .style(TextStyle{.foreground = colors::WHITE, .background = colors::BLUE}, - FontStyle{.font = engine->assets.fonts["MaterialIcons"_str].get(), - .font_height = 40, - .line_height = 1}, - 0, 4) - .frame(200, 200); - - btn2.text(U"بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ"_str) - .style(TextStyle{.foreground = colors::WHITE, .background = colors::NONE}, - FontStyle{.font = engine->assets.fonts["Amiri"_str].get(), - .font_height = 25, - .line_height = 1.2F}) - .frame(200, 200) - .padding(10, 10); + slider.range(0, 100) + .interp(0.5) + .axis(Axis::Y) + .frame(20, 200, false) + .track_frame(20, 200, false); + + btn.text(U"playlist_add ADD"_str) + .run(TextStyle{.color = colors::WHITE}, + FontStyle{.font = engine->assets.fonts["RobotoMono"_str].get(), + .font_height = 50, + .line_height = 1}) + .run(TextStyle{.color = colors::WHITE}, + FontStyle{.font = engine->assets.fonts["MaterialIcons"_str].get(), + .font_height = 40, + .line_height = 1}, + 0, 12) + .padding(5, 5); + + btn2.text(U"shuffle SHUFFLE"_str) + .run(TextStyle{.color = colors::WHITE}, + FontStyle{.font = engine->assets.fonts["RobotoMono"_str].get(), + .font_height = 50, + .line_height = 1}) + .run(TextStyle{.color = colors::WHITE}, + FontStyle{.font = engine->assets.fonts["MaterialIcons"_str].get(), + .font_height = 40, + .line_height = 1}, + 0, 7) + .frame(200, 200); + + btn3.text(U"بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ"_str) + .run(TextStyle{.color = colors::WHITE}, + FontStyle{.font = engine->assets.fonts["Amiri"_str].get(), + .font_height = 25, + .line_height = 1}) + .frame(200, 200) + .padding(5, 5); FlexView flex{}; - flex.items({&btn, &btn2, &btn_home, sw + 0, sw + 1, sw + 2, &slider, &scalar, - &radio, &combo_box}) - .axis(Axis::X) - .cross_align(0) - .main_align(MainAlign::SpaceBetween) - .frame(1'920, 1'080); + flex + .items({&btn, &btn2, &btn3, sw + 0, sw + 1, sw + 2, &slider, &scalar, + &radio, &combo_box}) + .axis(Axis::X) + .cross_align(0) + .main_align(MainAlign::SpaceBetween) + .frame(1'920, 1'080); auto animation = StaggeredAnimation::make(4, 8, RippleStagger{}); animation.timelines().v0.frame(1'920 * 0.25F, 1'920, 400ms, easing::out()); // [ ] do we really need a tick function? - auto loop = [&](time_point, nanoseconds delta) { - animation.tick(delta); + auto loop = [&](ViewContext const & ctx) { + animation.tick(ctx.timedelta); flex.frame(animation.animate(0).v0, 1'080); }; diff --git a/ashura/engine/assets.h b/ashura/engine/assets.h index 233a6d24e..52ab30b19 100644 --- a/ashura/engine/assets.h +++ b/ashura/engine/assets.h @@ -1,32 +1,403 @@ /// SPDX-License-Identifier: MIT #pragma once #include "ashura/engine/font.h" +#include "ashura/engine/image_decoder.h" +#include "ashura/engine/rect_pack.h" +#include "ashura/engine/shader.h" #include "ashura/gpu/gpu.h" +#include "ashura/std/async.h" +#include "ashura/std/fs.h" #include "ashura/std/map.h" #include "ashura/std/vec.h" namespace ash { -typedef StrVecMap ShaderMap; - -typedef StrVecMap> FontMap; +enum WorkerThreadId : u32 +{ + Render = 0, + Audio = 1, + Video = 2 +}; -// [ ] images +// D16_UNORM support and other formatts +// bgr srgb is supported for sampling +// +// srgb color attachment +// +// +// src size +// target size +// +// max size - if set, will not go beyond +// - option<> none if textur not available +// - 2d/3d +// - mipping / coherent (shader requirements) +// +// O(1) navigation +// +// +// budget, updates +// +// have images of size 4096x4096, pack the images into it as they come +// +// [ ] needs separate render thread that only does render work. needed so loading resources and stalling on the main thread will not stall the render thread +// [ ] needs +// +// [ ] images: static read-only. we can pack them, streaming mode. on load, add pre-frame tasks.most images would need to be downsampled first, +// we'll need to categorize or get hints about image usage // [ ] animations // [ ] audio // [ ] ... +// +// + +enum class ImageId : u64 +{ +}; + +enum class FontId : u64 +{ +}; + +enum class AudioId : u64 +{ +}; + +enum class VideoId : u64 +{ +}; + +enum class ShaderId : u64 +{ +}; + +struct Image +{ + ImageId id{}; + TextureId texture{}; + gpu::Rect view_area{}; + Vec2 uv[2]{}; + gpu::ImageInfo info{}; +}; -struct AssetMap +struct FileSystem { - explicit AssetMap(AllocatorImpl allocator) : - shaders{allocator}, - fonts{allocator} + static Future, IoErr>> + load_file(Span path, + AllocatorImpl allocator = default_allocator) { + InplaceVec path_copy; + path_copy.resize(path.size()).expect("Maximum path size exceeded"); + mem::copy(path, path_copy.view()); + + Future fut = future, IoErr>>(allocator).unwrap(); + + async::once( + [allocator, path = path_copy, fut = fut.alias()]() { + Vec data{allocator}; + read_file(path, data) + .match([&](Void) { fut.yield(Ok{std::move(data)}).unwrap(); }, + [&](IoErr err) { fut.yield(Err{err}).unwrap(); }); + }, + Ready{}, TaskSchedule{.target = TaskTarget::Worker, .thread = none}); + + return fut; } +}; + +// Future>; +// +// // submit to end/begin of render frame +// +// +// +// schedule task to load the image from disk. +// +// take the image from memory, resize it if needed. upload to gpu, return handle +// use handle to check if it is ready. +// +// - use gpu-visible staging buffer, then forward that +// - can only be used on the main thread +// +// [ ] mt-safe +// +struct ImageSystem +{ + static constexpr u32 ATLAS_PADDING = 1; + + struct Atlas + { + gpu::ImageInfo image_info = {}; + + gpu::ImageViewInfo image_view_info = {}; + + gpu::Image image = nullptr; + + gpu::ImageView image_view = nullptr; + + bool is_multi = false; + + RectPacker packer{}; + + Vec images{}; + }; + + gpu::Format format_ = gpu::Format::B8G8R8A8_UNORM; + /// @brief The size used for compacting the images + u32 atlas_size_ = 2'048; + Vec atlases_; + InplaceVec staging_buffers_; + InplaceVec encoders_; + u32 ring_index_ = 0; + gpu::Device * device_ = nullptr; + + // void init(gpu::Device * dev, u32 buffering) + // { + // staging_buffers_.resize(buffering).unwrap(); + // } + + void validate_image_info(gpu::ImageInfo const & info) + { + CHECK(info.type == gpu::ImageType::Type2D); + CHECK( + (info.usage & ~(gpu::ImageUsage::Sampled | gpu::ImageUsage::TransferSrc | + gpu::ImageUsage::TransferDst)) == gpu::ImageUsage::None); + CHECK(info.aspects == gpu::ImageAspects::Color || + info.aspects == gpu::ImageAspects::Depth || + info.aspects == + (gpu::ImageAspects::Depth | gpu::ImageAspects::Stencil)); + CHECK(info.extent.z == 1); + CHECK(info.mip_levels == 1); + CHECK(info.array_layers == 1); + CHECK(info.sample_count == gpu::SampleCount::C1); + CHECK(info.format == format_); + } + + Tuple allocate_rect(gpu::Extent extent) + { + if (extent.x <= atlas_size_ && extent.y <= atlas_size_) + { + for (auto [i, atlas] : enumerate(atlases_)) + { + if (atlas.is_multi) + { + PackRect pack_rect[] = { + {.extent = as_vec2i(extent + ATLAS_PADDING * 2)}}; + auto [packed, _] = atlas.packer.pack(pack_rect); + if (packed.is_empty()) + { + continue; + } - ShaderMap shaders; - FontMap fonts; + return { + i, RectU{as_vec2u(pack_rect[0].pos + ATLAS_PADDING), extent} + }; + } + } + + gpu::Image image = + device_ + ->create_image(gpu::ImageInfo{ + .label = "Image Atlas", + .type = gpu::ImageType::Type2D, + .format = format_, + .usage = gpu::ImageUsage::Sampled | gpu::ImageUsage::TransferDst | + gpu::ImageUsage::TransferSrc, + .aspects = gpu::ImageAspects::Color, + .extent{atlas_size_, atlas_size_}, + .mip_levels = 1, + .array_layers = 1, + .sample_count = gpu::SampleCount::C1 + }) + .unwrap(); + + gpu::ImageView view = device_ + ->create_image_view(gpu::ImageViewInfo{ + .label = "Image Atlas View", + .image = image, + .view_type = gpu::ImageViewType::Type2D, + .view_format = format_, + .mapping{}, + .aspects = gpu::ImageAspects::Color, + .first_mip_level = 0, + .num_mip_levels = 1, + .first_array_layer = 0, + .num_array_layers = 1}) + .unwrap(); + + // create new multi-atlas + // create image + // create view + // allocate texture id? immediate? next frame? + // update texture descriptor set to view + // copy buffer data to current frame's staging buffer + // submit pre-frame job to update the texture region + // + // + } + else + { + // create new single atlas with exact size & guaranteed padding + } + } + + static void load_from_memory(gpu::ImageInfo const & info, Vec buffer) + { + validate_image_info(info); + + // + // keep track of images available and slots for them + // + // + // + + // upload data to current frame's staging buffer + // submit to gpu system + // + // + // + // + } + + static Future> + load_from_path(Span label, Span path, + AllocatorImpl allocator = default_allocator) + { + Future fut = future>(allocator).unwrap(); + Future load_fut = FileSystem::load_file(path); + + async::once( + [fut = fut.alias(), load_fut = load_fut.alias(), label, allocator]() { + load_fut.get().match( + [&](Vec & buffer) { + Vec channels{allocator}; + decode_image(buffer, channels) + .match( + [&](DecodedImageInfo const & info) { + Vec bgra_channels{allocator}; + // [ ] abstract this!! + bgra_channels + .resize_uninit((u64) info.extent.x * (u64) info.extent.y * + 4ULL) + .unwrap(); + + ImageSpan bgra_span{.channels = bgra_channels, + .extent = info.extent, + .stride = info.extent.x}; + + switch (info.format) + { + case gpu::Format::R8G8B8A8_UNORM: + copy_RGBA_to_BGRA( + ImageSpan{.channels = channels, + .extent = info.extent, + .stride = info.extent.x}, + bgra_span); + break; + case gpu::Format::R8G8B8_UNORM: + copy_RGB_to_BGRA( + ImageSpan{.channels = channels, + .extent = info.extent, + .stride = info.extent.x}, + bgra_span, U8_MAX); + break; + + default: + CHECK_UNREACHABLE(); + } + + // + // allocate slot on the atlases for the image + // + // schedule for upload to GPU + // + }, + [&](ImageLoadErr err) { fut.yield(Err{err}).unwrap(); }); + }, + [&](IoErr err) { + fut + .yield(Err{err == IoErr::InvalidFileOrDir ? + ImageLoadErr::InvalidPath : + ImageLoadErr::IoErr}) + .unwrap(); + }); + }, + AwaitFutures{load_fut.alias()}, + TaskSchedule{.target = TaskTarget::Worker}); + + return fut; + } + + static void get(ImageId id); + + static void get(Span label); + + static void unload(ImageId); +}; + +// [ ] SPIRV compilation +// [ ] mt +// gpu::Shader +struct ShaderSystem +{ + Result<> compile(ShaderCompileInfo const & info, Vec out); + + Result load_from_memory(Vec spirv); + + void get(ShaderId id); + + void get(Span label); + + void unload(ShaderId); +}; + +// [ ] usual font store and atlasing then uploading to GPU +// [ ] mt +// Dyn +struct FontSystem +{ + void load_from_memory(); + void load_from_path(); + void get(FontId id); + void get(Span label); + void unload(FontId); +}; + +// [ ] load audio, convert into PCM16 using FFMPEG, load into SDL +// [ ] use single audio thread +// [ ] mt +struct AudioSystem +{ + void load_from_memory(); + void load_from_path(); + void get(AudioId id); + void get(Span label); + void unload(AudioId); +}; + +// [ ] load video frame using FFMPEG, upload to Vulkan +// [ ] use single video thread +// [ ] mt +struct VideoSystem +{ + void load_from_memory(); + void load_from_path(); + void get(VideoId id); + void get(Span label); + void unload(VideoId); + + // add pre-frame task to send the new update to the GPU. use for VIDEO frames. + void create_image(); +}; + +struct AssetSystem +{ + ImageSystem image; + FontSystem font; + AudioSystem audio; + VideoSystem video; + ShaderSystem shader; }; -} // namespace ash +} // namespace ash diff --git a/ashura/engine/canvas.cc b/ashura/engine/canvas.cc index 9905aa49e..1a77f9939 100644 --- a/ashura/engine/canvas.cc +++ b/ashura/engine/canvas.cc @@ -1,6 +1,6 @@ /// SPDX-License-Identifier: MIT #include "ashura/engine/canvas.h" -#include "ashura/engine/font_atlas.h" +#include "ashura/engine/font.h" #include "ashura/std/math.h" namespace ash @@ -8,11 +8,11 @@ namespace ash void path::rect(Vec & vtx) { - Vec2 const coords[] = { - {-1, -1}, - {1, -1}, - {1, 1 }, - {-1, 1 } + static constexpr Vec2 coords[] = { + {-0.5, -0.5}, + {0.5, -0.5}, + {0.5, 0.5 }, + {-0.5, 0.5 } }; vtx.extend(coords).unwrap(); @@ -33,7 +33,7 @@ void path::arc(Vec & vtx, f32 start, f32 stop, u32 segments) for (u32 i = 0; i < segments; i++) { - vtx[first + i] = rotor(i * step) * 2 - 1; + vtx[first + i] = rotor(i * step) - 0.5F; } } @@ -52,27 +52,29 @@ void path::circle(Vec & vtx, u32 segments) for (u32 i = 0; i < segments; i++) { - vtx[first + i] = rotor(i * step); + vtx[first + i] = rotor(i * step) - 0.5F; } } void path::squircle(Vec & vtx, f32 elasticity, u32 segments) { - if (segments < 128) + if (segments < 32) { return; } - elasticity = clamp(elasticity, 0.0F, 1.0F); + u32 const n = segments >> 2; - path::cubic_bezier(vtx, {0, -1}, {elasticity, -1}, {1, -1}, {1, 0}, - segments >> 2); - path::cubic_bezier(vtx, {1, 0}, {1, elasticity}, {1, 1}, {0, 1}, - segments >> 2); - path::cubic_bezier(vtx, {0, 1}, {-elasticity, 1}, {-1, 1}, {-1, 0}, - segments >> 2); - path::cubic_bezier(vtx, {-1, 0}, {-1, -elasticity}, {-1, -1}, {0, -1}, - segments >> 2); + elasticity = clamp(elasticity * 0.5F, 0.0F, 0.5F); + + path::cubic_bezier(vtx, {0, -0.5F}, {elasticity, -0.5F}, {0.5F, -0.5F}, + {0.5F, 0}, n); + path::cubic_bezier(vtx, {0.5F, 0}, {0.5F, elasticity}, {0.5F, 0.5F}, + {0, 0.5F}, n); + path::cubic_bezier(vtx, {0, 0.5F}, {-elasticity, 0.5F}, {-0.5F, 0.5F}, + {-0.5F, 0}, n); + path::cubic_bezier(vtx, {-0.5F, 0}, {-0.5F, -elasticity}, {-0.5F, -0.5F}, + {0, -0.5F}, n); } void path::rrect(Vec & vtx, Vec4 radii, u32 segments) @@ -82,89 +84,88 @@ void path::rrect(Vec & vtx, Vec4 radii, u32 segments) return; } - radii = radii * 2; - radii.x = min(radii.x, 2.0F); - radii.y = min(radii.y, 2.0F); - radii.z = min(radii.z, 2.0F); - radii.w = min(radii.w, 2.0F); + radii.x = clamp(radii.x, 0.0F, 1.0F); + radii.y = clamp(radii.y, 0.0F, 1.0F); + radii.z = clamp(radii.z, 0.0F, 1.0F); + radii.w = clamp(radii.w, 0.0F, 1.0F); /// clipping - radii.y = min(radii.y, 2.0F - radii.x); - f32 max_radius_z = min(2.0F - radii.x, 1.0F - radii.y); + radii.y = min(radii.y, 1.0F - radii.x); + f32 max_radius_z = min(1.0F - radii.x, 0.5F - radii.y); radii.z = min(radii.z, max_radius_z); - f32 max_radius_w = min(max_radius_z, 1.0F - radii.z); + f32 max_radius_w = min(max_radius_z, 0.5F - radii.z); radii.w = min(radii.w, max_radius_w); - u32 const curve_segments = (segments - 8) / 4; - f32 const step = (curve_segments == 0) ? 0.0F : ((PI / 2) / curve_segments); + u32 const curve_segments = (segments - 8) >> 2; + f32 const step = + (curve_segments == 0) ? 0.0F : ((PI * 0.5F) / curve_segments); u32 const first = vtx.size32(); vtx.extend_uninit(segments).unwrap(); u32 i = 0; - vtx[first + i++] = Vec2{1, 1 - radii.z}; + vtx[first + i++] = Vec2{0.5F, 0.5F - radii.z}; for (u32 s = 0; s < curve_segments; s++) { - vtx[first + i++] = (1 - radii.z) + radii.z * rotor(s * step); + vtx[first + i++] = (0.5F - radii.z) + radii.z * rotor(s * step); } - vtx[first + i++] = Vec2{1 - radii.z, 1}; + vtx[first + i++] = Vec2{0.5F - radii.z, 0.5F}; - vtx[first + i++] = Vec2{-1 + radii.w, 1}; + vtx[first + i++] = Vec2{-0.5F + radii.w, 0.5F}; for (u32 s = 0; s < curve_segments; s++) { - vtx[first + i++] = - Vec2{-1 + radii.w, 1 - radii.w} + radii.w * rotor(PI / 2 + s * step); + vtx[first + i++] = Vec2{-0.5F + radii.w, 0.5F - radii.w} + + radii.w * rotor(PI * 0.5F + s * step); } - vtx[first + i++] = Vec2{-1, 1 - radii.w}; + vtx[first + i++] = Vec2{-0.5F, 0.5F - radii.w}; - vtx[first + i++] = Vec2{-1, -1 + radii.x}; + vtx[first + i++] = Vec2{-0.5F, -0.5F + radii.x}; for (u32 s = 0; s < curve_segments; s++) { - vtx[first + i++] = (-1 + radii.x) + radii.x * rotor(PI + s * step); + vtx[first + i++] = (-0.5F + radii.x) + radii.x * rotor(PI + s * step); } - vtx[first + i++] = Vec2{-1 + radii.x, -1}; + vtx[first + i++] = Vec2{-0.5F + radii.x, -0.5F}; - vtx[first + i++] = Vec2{1 - radii.y, -1}; + vtx[first + i++] = Vec2{0.5F - radii.y, -0.5F}; for (u32 s = 0; s < curve_segments; s++) { - vtx[first + i++] = Vec2{1 - radii.y, (-1 + radii.y)} + - radii.y * rotor(PI * 3.0F / 2.0F + s * step); + vtx[first + i++] = Vec2{0.5F - radii.y, (-0.5F + radii.y)} + + radii.y * rotor(PI * 1.5F + s * step); } - vtx[first + i++] = Vec2{1, -1 + radii.y}; + vtx[first + i++] = Vec2{0.5F, -0.5F + radii.y}; } void path::brect(Vec & vtx, Vec4 slant) { - slant = slant * 2.0F; - slant.x = min(slant.x, 2.0F); - slant.y = min(slant.y, 2.0F); - slant.z = min(slant.z, 2.0F); - slant.w = min(slant.w, 2.0F); + slant.x = clamp(slant.x, 0.0F, 1.0F); + slant.y = clamp(slant.y, 0.0F, 1.0F); + slant.z = clamp(slant.z, 0.0F, 1.0F); + slant.w = clamp(slant.w, 0.0F, 1.0F); - slant.y = min(slant.y, 2.0F - slant.x); - f32 max_radius_z = min(2.0F - slant.x, 2.0F - slant.y); + slant.y = min(slant.y, 1 - slant.x); + f32 max_radius_z = min(1 - slant.x, 1 - slant.y); slant.z = min(slant.z, max_radius_z); - f32 max_radius_w = min(max_radius_z, 2.0F - slant.z); + f32 max_radius_w = min(max_radius_z, 1 - slant.z); slant.w = min(slant.w, max_radius_w); Vec2 const vertices[] = { - {-1 + slant.x, -1 }, - {1 - slant.y, -1 }, - {1, -1 + slant.y}, - {1, 1 - slant.z }, - {1 - slant.z, 1 }, - {-1 + slant.w, 1 }, - {-1, 1 - slant.w }, - {-1, -1 + slant.x} + {-0.5F + slant.x, -0.5F }, + {0.5F - slant.y, -0.5F }, + {0.5F, -0.5F + slant.y}, + {0.5F, 0.5F - slant.z }, + {0.5F - slant.z, 0.5F }, + {-0.5F + slant.w, 0.5F }, + {-0.5F, 0.5F - slant.w }, + {-0.5F, -0.5F + slant.x} }; vtx.extend(vertices).unwrap(); @@ -207,8 +208,8 @@ void path::cubic_bezier(Vec & vtx, Vec2 cp0, Vec2 cp1, Vec2 cp2, Vec2 cp3, for (u32 i = 0; i < segments; i++) { vtx[first + i] = - Vec2{ash::cubic_bezier(cp0.x, cp1.x, cp2.x, cp3.x, step * i), - ash::cubic_bezier(cp0.y, cp1.y, cp2.y, cp3.y, step * i)}; + Vec2{ash::cubic_bezier(cp0.x, cp1.x, cp2.x, cp3.x, step * i), + ash::cubic_bezier(cp0.y, cp1.y, cp2.y, cp3.y, step * i)}; } } @@ -229,8 +230,8 @@ void path::catmull_rom(Vec & vtx, Vec2 cp0, Vec2 cp1, Vec2 cp2, Vec2 cp3, for (u32 i = 0; i < segments; i++) { vtx[beg + i] = - Vec2{ash::cubic_bezier(cp0.x, cp1.x, cp2.x, cp3.x, step * i), - ash::cubic_bezier(cp0.y, cp1.y, cp2.y, cp3.y, step * i)}; + Vec2{ash::cubic_bezier(cp0.x, cp1.x, cp2.x, cp3.x, step * i), + ash::cubic_bezier(cp0.y, cp1.y, cp2.y, cp3.y, step * i)}; } } @@ -262,7 +263,7 @@ void path::triangulate_stroke(Span points, Vec & vertices, f32 const alpha = atanf(d.y / d.x); // parallel angle - Vec2 const down = (thickness / 2) * rotor(alpha + PI / 2); + Vec2 const down = (thickness * 0.5F) * rotor(alpha + PI * 0.5F); // perpendicular angle Vec2 const up = -down; @@ -356,7 +357,7 @@ Canvas & Canvas::begin_recording(Vec2 new_viewport_extent, viewport_extent = new_viewport_extent; surface_extent = new_surface_extent; - if (new_viewport_extent.y == 0 || new_viewport_extent.x == 0) + if (new_viewport_extent.y == 0 | new_viewport_extent.x == 0) { viewport_aspect_ratio = 1; } @@ -366,7 +367,7 @@ Canvas & Canvas::begin_recording(Vec2 new_viewport_extent, } world_to_view = - translate3d(Vec3{-1, -1, 0}) * scale3d(vec3(2 / viewport_extent, 1)); + translate3d(Vec3{-1, -1, 0}) * scale3d(vec3(2 / viewport_extent, 1)); return *this; } @@ -374,6 +375,7 @@ Canvas & Canvas::begin_recording(Vec2 new_viewport_extent, constexpr RectU clip_to_scissor(gpu::Viewport const & viewport, CRect const & clip) { + // [ ] this should probably also use the framebuffer extent // clips only apply translations. no scaling Rect s{viewport.offset + clip.begin(), clip.extent}; @@ -382,14 +384,14 @@ constexpr RectU clip_to_scissor(gpu::Viewport const & viewport, s.offset.y = clamp(s.offset.y, 0.0F, viewport.extent.y); s.extent.x = - clamp(s.offset.x + s.extent.x, 0.0F, viewport.extent.x) - s.offset.x; + clamp(s.offset.x + s.extent.x, 0.0F, viewport.extent.x) - s.offset.x; s.extent.y = - clamp(s.offset.y + s.extent.y, 0.0F, viewport.extent.y) - s.offset.y; + clamp(s.offset.y + s.extent.y, 0.0F, viewport.extent.y) - s.offset.y; - return RectU{ - .offset{(u32) s.offset.x, (u32) s.offset.y}, - .extent{(u32) s.extent.x, (u32) s.extent.y} + return { + .offset{(u32) s.offset.x, (u32) s.offset.y}, + .extent{(u32) s.extent.x, (u32) s.extent.y} }; } @@ -402,16 +404,16 @@ static inline void flush_batch(Canvas & c) { case Canvas::BatchType::RRect: c.add_pass("RRect"_str, [batch, world_to_view = c.world_to_view]( - Canvas::RenderContext const & ctx) { - RRectPassParams params{.rendering_info = ctx.rt.info, - .scissor = - clip_to_scissor(ctx.rt.viewport, batch.clip), - .viewport = ctx.rt.viewport, - .world_to_view = world_to_view, - .params_ssbo = ctx.rrects.descriptor, - .textures = ctx.gpu.texture_views, - .first_instance = batch.objects.offset, - .num_instances = batch.objects.span}; + Canvas::RenderContext const & ctx) { + RRectPassParams params{ + .framebuffer = ctx.framebuffer, + .scissor = clip_to_scissor(ctx.framebuffer.viewport(), batch.clip), + .viewport = ctx.framebuffer.viewport(), + .world_to_view = world_to_view, + .params_ssbo = ctx.rrects.descriptor, + .textures = ctx.gpu.texture_views, + .first_instance = batch.run.offset, + .num_instances = batch.run.span}; ctx.passes.rrect->encode(ctx.gpu, ctx.enc, params); }); @@ -419,18 +421,19 @@ static inline void flush_batch(Canvas & c) case Canvas::BatchType::Ngon: c.add_pass("Ngon"_str, [batch, world_to_view = c.world_to_view]( - Canvas::RenderContext const & ctx) { + Canvas::RenderContext const & ctx) { NgonPassParams params{ - .rendering_info = ctx.rt.info, - .scissor = clip_to_scissor(ctx.rt.viewport, batch.clip), - .viewport = ctx.rt.viewport, - .world_to_view = world_to_view, - .vertices_ssbo = ctx.ngon_vertices.descriptor, - .indices_ssbo = ctx.ngon_indices.descriptor, - .params_ssbo = ctx.ngons.descriptor, - .textures = ctx.gpu.texture_views, - .index_counts = - ctx.canvas.ngon_index_counts.view().slice(batch.objects)}; + .framebuffer = ctx.framebuffer, + .scissor = clip_to_scissor(ctx.framebuffer.viewport(), batch.clip), + .viewport = ctx.framebuffer.viewport(), + .world_to_view = world_to_view, + .vertices_ssbo = ctx.ngon_vertices.descriptor, + .indices_ssbo = ctx.ngon_indices.descriptor, + .params_ssbo = ctx.ngons.descriptor, + .textures = ctx.gpu.texture_views, + .first_instance = batch.run.offset, + .index_counts = + ctx.canvas.ngon_index_counts.view().slice(batch.run)}; ctx.passes.ngon->encode(ctx.gpu, ctx.enc, params); }); return; @@ -447,18 +450,18 @@ static inline void add_rrect(Canvas & c, RRectParam const & param, c.rrect_params.push(param).unwrap(); if (c.batch.type != Canvas::BatchType::RRect || c.batch.clip != clip) - [[unlikely]] + [[unlikely]] { flush_batch(c); c.batch = Canvas::Batch{ - .type = Canvas::BatchType::RRect, - .clip = clip, - .objects{.offset = index, .span = 1} + .type = Canvas::BatchType::RRect, + .clip = clip, + .run{.offset = index, .span = 1} }; return; } - c.batch.objects.span++; + c.batch.run.span++; } static inline void add_ngon(Canvas & c, NgonParam const & param, @@ -469,18 +472,18 @@ static inline void add_ngon(Canvas & c, NgonParam const & param, c.ngon_params.push(param).unwrap(); if (c.batch.type != Canvas::BatchType::Ngon || c.batch.clip != clip) - [[unlikely]] + [[unlikely]] { flush_batch(c); c.batch = Canvas::Batch{ - .type = Canvas::BatchType::Ngon, - .clip = clip, - .objects{.offset = index, .span = 1} + .type = Canvas::BatchType::Ngon, + .clip = clip, + .run{.offset = index, .span = 1} }; return; } - c.batch.objects.span++; + c.batch.run.span++; } Canvas & Canvas::end_recording() @@ -497,74 +500,73 @@ Canvas & Canvas::clip(CRect const & c) constexpr Mat4 object_to_world(Mat4 const & transform, Vec2 center, Vec2 extent) { - return transform * translate3d(vec3(center, 0)) * - scale3d(vec3(extent * 0.5F, 1)); + return transform * translate3d(vec3(center, 0)) * scale3d(vec3(extent, 1)); } Canvas & Canvas::circle(ShapeInfo const & info) { + f32 const inv_y = 1 / info.extent.y; add_rrect( - *this, - RRectParam{ - .transform = - object_to_world(info.transform, info.center, info.extent), - .tint = {info.tint[0], info.tint[1], info.tint[2], info.tint[3]}, - .radii = {1, 1, 1, 1}, - .uv = {info.uv[0], info.uv[1]}, - .tiling = info.tiling, - .aspect_ratio = info.extent.x / info.extent.y, - .stroke = info.stroke, - .thickness = info.thickness / info.extent.y, - .edge_smoothness = info.edge_smoothness, - .sampler = info.sampler, - .albedo = info.texture + *this, + RRectParam{ + .transform = object_to_world(info.transform, info.center, info.extent), + .tint = {info.tint[0], info.tint[1], info.tint[2], info.tint[3]}, + .radii = {1, 1, 1, 1}, + .uv = {info.uv[0], info.uv[1]}, + .tiling = info.tiling, + .aspect_ratio = info.extent.x * inv_y, + .stroke = info.stroke, + .thickness = info.thickness * inv_y, + .edge_smoothness = info.edge_smoothness * inv_y, + .sampler = info.sampler, + .albedo = info.texture }, - current_clip); + current_clip); return *this; } Canvas & Canvas::rect(ShapeInfo const & info) { + f32 const inv_y = 1 / info.extent.y; add_rrect( - *this, - RRectParam{ - .transform = - object_to_world(info.transform, info.center, info.extent), - .tint = {info.tint[0], info.tint[1], info.tint[2], info.tint[3]}, - .radii = {0, 0, 0, 0}, - .uv = {info.uv[0], info.uv[1]}, - .tiling = info.tiling, - .aspect_ratio = info.extent.x / info.extent.y, - .stroke = info.stroke, - .thickness = info.thickness / info.extent.y, - .edge_smoothness = info.edge_smoothness, - .sampler = info.sampler, - .albedo = info.texture + *this, + RRectParam{ + .transform = object_to_world(info.transform, info.center, info.extent), + .tint = {info.tint[0], info.tint[1], info.tint[2], info.tint[3]}, + .radii = {0, 0, 0, 0}, + .uv = {info.uv[0], info.uv[1]}, + .tiling = info.tiling, + .aspect_ratio = info.extent.x * inv_y, + .stroke = info.stroke, + .thickness = info.thickness * inv_y, + .edge_smoothness = info.edge_smoothness * inv_y, + .sampler = info.sampler, + .albedo = info.texture }, - current_clip); + current_clip); return *this; } Canvas & Canvas::rrect(ShapeInfo const & info) { + f32 const inv_y = 1 / info.extent.y; add_rrect( - *this, - RRectParam{ - .transform = - object_to_world(info.transform, info.center, info.extent), - .tint = {info.tint[0], info.tint[1], info.tint[2], info.tint[3]}, - .radii = info.corner_radii / info.extent.y, - .uv = {info.uv[0], info.uv[1]}, - .tiling = info.tiling, - .aspect_ratio = info.extent.x / info.extent.y, - .stroke = info.stroke, - .thickness = info.thickness / info.extent.y, - .edge_smoothness = info.edge_smoothness, - .sampler = info.sampler, - .albedo = info.texture + *this, + RRectParam{ + .transform = object_to_world(info.transform, info.center, info.extent), + .tint = {info.tint[0], info.tint[1], info.tint[2], info.tint[3]}, + .radii = info.corner_radii * inv_y, + .uv = {info.uv[0], info.uv[1]}, + .tiling = info.tiling, + .aspect_ratio = info.extent.x * inv_y, + .stroke = info.stroke, + .thickness = info.thickness * inv_y, + .edge_smoothness = info.edge_smoothness * inv_y, + .sampler = info.sampler, + .albedo = info.texture }, - current_clip); + current_clip); return *this; } @@ -581,19 +583,19 @@ Canvas & Canvas::brect(ShapeInfo const & info) u32 const num_indices = ngon_indices.size32() - first_index; - add_ngon(*this, - NgonParam{ - .transform = - object_to_world(info.transform, info.center, info.extent), - .tint = {info.tint[0], info.tint[1], info.tint[2], info.tint[3]}, - .uv = {info.uv[0], info.uv[1]}, - .tiling = info.tiling, - .sampler = info.sampler, - .albedo = info.texture, - .first_index = first_index, - .first_vertex = first_vertex + add_ngon( + *this, + NgonParam{ + .transform = object_to_world(info.transform, info.center, info.extent), + .tint = {info.tint[0], info.tint[1], info.tint[2], info.tint[3]}, + .uv = {info.uv[0], info.uv[1]}, + .tiling = info.tiling, + .sampler = info.sampler, + .albedo = info.texture, + .first_index = first_index, + .first_vertex = first_vertex }, - current_clip, num_indices); + current_clip, num_indices); return *this; } @@ -611,19 +613,19 @@ Canvas & Canvas::squircle(ShapeInfo const & info, f32 elasticity, u32 segments) u32 const num_indices = ngon_indices.size32() - first_index; - add_ngon(*this, - NgonParam{ - .transform = - object_to_world(info.transform, info.center, info.extent), - .tint = {info.tint[0], info.tint[1], info.tint[2], info.tint[3]}, - .uv = {info.uv[0], info.uv[1]}, - .tiling = info.tiling, - .sampler = info.sampler, - .albedo = info.texture, - .first_index = first_index, - .first_vertex = first_vertex + add_ngon( + *this, + NgonParam{ + .transform = object_to_world(info.transform, info.center, info.extent), + .tint = {info.tint[0], info.tint[1], info.tint[2], info.tint[3]}, + .uv = {info.uv[0], info.uv[1]}, + .tiling = info.tiling, + .sampler = info.sampler, + .albedo = info.texture, + .first_index = first_index, + .first_vertex = first_vertex }, - current_clip, num_indices); + current_clip, num_indices); return *this; } @@ -638,12 +640,12 @@ Canvas & Canvas::text(ShapeInfo const & info, TextBlock const & block, f32 const block_width = max(layout.extent.x, style.align_width); Vec2 const block_extent{block_width, layout.extent.y}; - static constexpr u8 PASS_BACKGROUND = 0; - static constexpr u8 PASS_GLYPH_SHADOWS = 1; - static constexpr u8 PASS_GLYPHS = 2; - static constexpr u8 PASS_UNDERLINE = 3; - static constexpr u8 PASS_STRIKETHROUGH = 4; - static constexpr u8 NUM_PASSES = 5; + static constexpr u32 PASS_BACKGROUND = 0; + static constexpr u32 PASS_GLYPH_SHADOWS = 1; + static constexpr u32 PASS_GLYPHS = 2; + static constexpr u32 PASS_UNDERLINE = 3; + static constexpr u32 PASS_STRIKETHROUGH = 4; + static constexpr u32 NUM_PASSES = 5; for (u8 pass = 0; pass < NUM_PASSES; pass++) { @@ -651,8 +653,8 @@ Canvas & Canvas::text(ShapeInfo const & info, TextBlock const & block, for (Line const & ln : layout.lines) { if (!overlaps(clip, CRect{ - .center = info.center + line_y, - .extent = {block_width, ln.metrics.height} + .center = info.center + line_y, + .extent = {block_width, ln.metrics.height} })) { continue; @@ -662,8 +664,7 @@ Canvas & Canvas::text(ShapeInfo const & info, TextBlock const & block, TextDirection const direction = level_to_direction(ln.metrics.level); // flip the alignment axis direction if it is an RTL line f32 const alignment = - style.alignment * - ((direction == TextDirection::LeftToRight) ? 1 : -1); + style.alignment * ((direction == TextDirection::LeftToRight) ? 1 : -1); f32 cursor = space_align(block_width, ln.metrics.width, alignment) - ln.metrics.width * 0.5F; for (TextRun const & run : @@ -672,7 +673,7 @@ Canvas & Canvas::text(ShapeInfo const & info, TextBlock const & block, FontStyle const & font_style = block.fonts[run.style]; TextStyle const & run_style = style.runs[run.style]; FontInfo const font = font_style.font->info(); - GpuFontAtlas const * atlas = font.gpu_atlas.value(); + GpuFontAtlas const & atlas = font.gpu_atlas.value(); f32 const run_width = au_to_px(run.metrics.advance, run.font_height); if (pass == PASS_BACKGROUND && !run_style.background.is_transparent()) @@ -680,9 +681,9 @@ Canvas & Canvas::text(ShapeInfo const & info, TextBlock const & block, Vec2 extent{run_width, au_to_px(run.metrics.height(), run.font_height)}; Vec2 center = - Vec2{cursor + extent.x * 0.5F, - baseline - au_to_px(run.metrics.ascent, run.font_height) + - extent.y * 0.5F}; + Vec2{cursor + extent.x * 0.5F, + baseline - au_to_px(run.metrics.ascent, run.font_height) + + extent.y * 0.5F}; rect({.center = info.center, .extent = extent, .transform = info.transform * translate3d(vec3(center, 0)), @@ -692,12 +693,12 @@ Canvas & Canvas::text(ShapeInfo const & info, TextBlock const & block, f32 glyph_cursor = cursor; for (u32 g = 0; g < run.num_glyphs; g++) { - GlyphShape const & sh = layout.glyphs[run.first_glyph + g]; - Glyph const & gl = font.glyphs[sh.glyph]; - AtlasGlyph const & agl = atlas->glyphs[sh.glyph]; - Vec2 const extent = au_to_px(gl.metrics.extent, run.font_height); - Vec2 const center = Vec2{glyph_cursor, baseline} + - au_to_px(gl.metrics.bearing, run.font_height) + + GlyphShape const & sh = layout.glyphs[run.first_glyph + g]; + GlyphMetrics const & m = font.glyphs[sh.glyph]; + AtlasGlyph const & agl = atlas.glyphs[sh.glyph]; + Vec2 const extent = au_to_px(m.extent, run.font_height); + Vec2 const center = Vec2{glyph_cursor, baseline} + + au_to_px(m.bearing, run.font_height) + au_to_px(sh.offset, run.font_height) + extent * 0.5F; @@ -707,31 +708,30 @@ Canvas & Canvas::text(ShapeInfo const & info, TextBlock const & block, Vec2 shadow_extent = extent * run_style.shadow_scale; Vec2 shadow_center = center + run_style.shadow_offset; rect({ - .center = info.center, - .extent = shadow_extent, - .transform = - info.transform * translate3d(vec3(shadow_center, 0)), - .tint = run_style.shadow, - .sampler = info.sampler, - .texture = atlas->textures[agl.layer], - .uv = {agl.uv[0], agl.uv[1]}, - .tiling = info.tiling, - .edge_smoothness = info.edge_smoothness + .center = info.center, + .extent = shadow_extent, + .transform = info.transform * translate3d(vec3(shadow_center, 0)), + .tint = run_style.shadow, + .sampler = info.sampler, + .texture = atlas.textures[agl.layer], + .uv = {agl.uv[0], agl.uv[1]}, + .tiling = info.tiling, + .edge_smoothness = info.edge_smoothness }); } - if (pass == PASS_GLYPHS && !run_style.foreground.is_transparent()) + if (pass == PASS_GLYPHS && !run_style.color.is_transparent()) { rect({ - .center = info.center, - .extent = extent, - .transform = info.transform * translate3d(vec3(center, 0)), - .tint = run_style.foreground, - .sampler = info.sampler, - .texture = atlas->textures[agl.layer], - .uv = {agl.uv[0], agl.uv[1]}, - .tiling = info.tiling, - .edge_smoothness = info.edge_smoothness + .center = info.center, + .extent = extent, + .transform = info.transform * translate3d(vec3(center, 0)), + .tint = run_style.color, + .sampler = info.sampler, + .texture = atlas.textures[agl.layer], + .uv = {agl.uv[0], agl.uv[1]}, + .tiling = info.tiling, + .edge_smoothness = info.edge_smoothness }); } @@ -742,14 +742,14 @@ Canvas & Canvas::text(ShapeInfo const & info, TextBlock const & block, run_style.strikethrough_thickness != 0) { Vec2 extent{run_width, run_style.strikethrough_thickness}; - Vec2 center = Vec2{cursor, baseline - run.metrics.ascent * 0.5F} + - extent * 0.5F; + Vec2 center = + Vec2{cursor, baseline - run.metrics.ascent * 0.5F} + extent * 0.5F; rect({.center = info.center, .extent = extent, .transform = info.transform * translate3d(vec3(center, 0)), .tint = run_style.strikethrough, .sampler = info.sampler, - .texture = 0, + .texture = TextureId::White, .uv = {}, .tiling = info.tiling, .edge_smoothness = info.edge_smoothness}); @@ -764,7 +764,7 @@ Canvas & Canvas::text(ShapeInfo const & info, TextBlock const & block, .transform = info.transform * translate3d(vec3(center, 0)), .tint = run_style.underline, .sampler = info.sampler, - .texture = 0, + .texture = TextureId::White, .uv = {}, .tiling = info.tiling, .edge_smoothness = info.edge_smoothness}); @@ -792,19 +792,19 @@ Canvas & Canvas::triangles(ShapeInfo const & info, Span points) u32 const num_indices = ngon_vertices.size32() - first_vertex; - add_ngon(*this, - NgonParam{ - .transform = - object_to_world(info.transform, info.center, info.extent), - .tint = {info.tint[0], info.tint[1], info.tint[2], info.tint[3]}, - .uv = {info.uv[0], info.uv[1]}, - .tiling = info.tiling, - .sampler = info.sampler, - .albedo = info.texture, - .first_index = first_index, - .first_vertex = first_vertex + add_ngon( + *this, + NgonParam{ + .transform = object_to_world(info.transform, info.center, info.extent), + .tint = {info.tint[0], info.tint[1], info.tint[2], info.tint[3]}, + .uv = {info.uv[0], info.uv[1]}, + .tiling = info.tiling, + .sampler = info.sampler, + .albedo = info.texture, + .first_index = first_index, + .first_vertex = first_vertex }, - current_clip, num_indices); + current_clip, num_indices); return *this; } @@ -828,19 +828,19 @@ Canvas & Canvas::triangles(ShapeInfo const & info, Span points, v += first_vertex; } - add_ngon(*this, - NgonParam{ - .transform = - object_to_world(info.transform, info.center, info.extent), - .tint = {info.tint[0], info.tint[1], info.tint[2], info.tint[3]}, - .uv = {info.uv[0], info.uv[1]}, - .tiling = info.tiling, - .sampler = info.sampler, - .albedo = info.texture, - .first_index = first_index, - .first_vertex = first_vertex + add_ngon( + *this, + NgonParam{ + .transform = object_to_world(info.transform, info.center, info.extent), + .tint = {info.tint[0], info.tint[1], info.tint[2], info.tint[3]}, + .uv = {info.uv[0], info.uv[1]}, + .tiling = info.tiling, + .sampler = info.sampler, + .albedo = info.texture, + .first_index = first_index, + .first_vertex = first_vertex }, - current_clip, idx.size32()); + current_clip, idx.size32()); return *this; } @@ -859,19 +859,19 @@ Canvas & Canvas::line(ShapeInfo const & info, Span points) u32 const num_indices = ngon_indices.size32() - first_index; - add_ngon(*this, - NgonParam{ - .transform = - object_to_world(info.transform, info.center, info.extent), - .tint = {info.tint[0], info.tint[1], info.tint[2], info.tint[3]}, - .uv = {info.uv[0], info.uv[1]}, - .tiling = info.tiling, - .sampler = info.sampler, - .albedo = info.texture, - .first_index = first_index, - .first_vertex = first_vertex + add_ngon( + *this, + NgonParam{ + .transform = object_to_world(info.transform, info.center, info.extent), + .tint = {info.tint[0], info.tint[1], info.tint[2], info.tint[3]}, + .uv = {info.uv[0], info.uv[1]}, + .tiling = info.tiling, + .sampler = info.sampler, + .albedo = info.texture, + .first_index = first_index, + .first_vertex = first_vertex }, - current_clip, num_indices); + current_clip, num_indices); return *this; } @@ -881,12 +881,10 @@ Canvas & Canvas::blur(CRect const & area, u32 num_passes) flush_batch(*this); add_pass("Blur"_str, [num_passes, area](Canvas::RenderContext const & ctx) { - BlurPassParams params{.image_view = ctx.rt.info.color_attachments[0].view, - .extent = ctx.rt.extent, - .texture_view = ctx.rt.color_descriptor, - .texture = 0, - .passes = num_passes, - .area = clip_to_scissor(ctx.rt.viewport, area)}; + BlurPassParams params{.framebuffer = ctx.framebuffer, + .passes = num_passes, + .area = + clip_to_scissor(ctx.framebuffer.viewport(), area)}; ctx.passes.blur->encode(ctx.gpu, ctx.enc, params); }); @@ -900,4 +898,4 @@ Canvas & Canvas::add_pass(Pass && pass) return *this; } -} // namespace ash +} // namespace ash diff --git a/ashura/engine/canvas.h b/ashura/engine/canvas.h index 23a017a27..684c27444 100644 --- a/ashura/engine/canvas.h +++ b/ashura/engine/canvas.h @@ -13,7 +13,7 @@ namespace ash { -/// @brief all points are stored in the [-1, +1] range, all arguments must be +/// @brief all points are stored in the [-0.5, +0.5] range, all arguments must be /// normalized to same range. namespace path { @@ -80,56 +80,56 @@ void triangles(u32 first_vertex, u32 num_vertices, Vec & idx); /// @brief generate vertices for a quadratic bezier curve void triangulate_convex(Vec & idx, u32 first_vertex, u32 num_vertices); -}; // namespace path +}; // namespace path -enum class ScaleMode : u8 +enum class TileMode : u32 { Stretch = 0, Tile = 1 }; /// @brief Canvas Shape Description -/// @param center center of the shape in world-space -/// @param extent extent of the shape in world-space -/// @param transform world-space transform matrix -/// @param corner_radii corner radii of each corner if rrect -/// @param stroke lerp intensity between stroke and fill, 0 to fill, 1 to stroke -/// @param thickness thickness of the stroke -/// @param scissor in surface pixel coordinates -/// @param tint Linear Color gradient to use as tint -/// @param sampler sampler to use in rendering the shape -/// @param texture texture index in the global texture store -/// @param uv uv coordinates of the upper-left and lower-right -/// @param tiling tiling factor -/// @param edge_smoothness edge smoothness to apply if it is a rrect struct ShapeInfo { + /// @brief center of the shape in world-space Vec2 center = {0, 0}; + /// @brief extent of the shape in world-space Vec2 extent = {0, 0}; + /// @brief object-world-space transform matrix Mat4 transform = Mat4::identity(); + /// @brief corner radii of each corner if rrect Vec4 corner_radii = {0, 0, 0, 0}; + /// @brief lerp intensity between stroke and fill, 0 to fill, 1 to stroke f32 stroke = 0.0F; + /// @brief thickness thickness of the stroke f32 thickness = 1.0F; + /// @brief Linear Color gradient to use as tint ColorGradient tint = {}; - u32 sampler = 0; + /// @brief sampler to use in rendering the shape + SamplerId sampler = SamplerId::Linear; - u32 texture = 0; + /// @brief texture to use in rendering the shape + TextureId texture = TextureId::White; + /// @brief uv coordinates of the upper-left and lower-right part of the + /// texture to sample from Vec2 uv[2] = { - {0, 0}, - {1, 1} + {0, 0}, + {1, 1} }; + /// @brief tiling factor f32 tiling = 1; - f32 edge_smoothness = 0.015F; + /// @brief edge smoothness to apply if it is a rrect + f32 edge_smoothness = 1; }; struct Canvas @@ -137,9 +137,9 @@ struct Canvas struct RenderContext { Canvas & canvas; - GpuContext & gpu; + GpuSystem & gpu; PassContext & passes; - RenderTarget const & rt; + Framebuffer const & framebuffer; gpu::CommandEncoder & enc; SSBO const & rrects; SSBO const & ngons; @@ -158,10 +158,10 @@ struct Canvas { BatchType type = BatchType::None; CRect clip{ - .center{F32_MAX / 2, F32_MAX / 2}, - .extent{F32_MAX, F32_MAX } + .center{F32_MAX / 2, F32_MAX / 2}, + .extent{F32_MAX, F32_MAX } }; - Slice32 objects{}; + Slice32 run{}; }; typedef Dyn> PassFn; @@ -181,8 +181,8 @@ struct Canvas Mat4 world_to_view = Mat4::identity(); CRect current_clip{ - .center{F32_MAX / 2, F32_MAX / 2}, - .extent{F32_MAX, F32_MAX } + .center{F32_MAX * 0.5F, F32_MAX * 0.5F}, + .extent{F32_MAX, F32_MAX } }; Vec rrect_params; @@ -204,13 +204,13 @@ struct Canvas ArenaPool frame_arena; explicit Canvas(AllocatorImpl allocator) : - rrect_params{allocator}, - ngon_params{allocator}, - ngon_vertices{allocator}, - ngon_indices{allocator}, - ngon_index_counts{allocator}, - passes{allocator}, - frame_arena{allocator} + rrect_params{allocator}, + ngon_params{allocator}, + ngon_vertices{allocator}, + ngon_indices{allocator}, + ngon_index_counts{allocator}, + passes{allocator}, + frame_arena{allocator} { } @@ -242,7 +242,7 @@ struct Canvas /// @brief render a squircle (triangulation based) /// @param num_segments an upper bound on the number of segments to - /// @param degree + /// @param elasticity elasticity of the squircle [0, 1] Canvas & squircle(ShapeInfo const & info, f32 degree, u32 segments); /// @brief @@ -266,7 +266,7 @@ struct Canvas /// @param info /// @param mode /// @param uvs - Canvas & nine_slice(ShapeInfo const & info, ScaleMode mode, + Canvas & nine_slice(ShapeInfo const & info, TileMode mode, Span uvs); /// @brief Render text using font atlases @@ -281,8 +281,8 @@ struct Canvas Canvas & text(ShapeInfo const & info, TextBlock const & block, TextLayout const & layout, TextBlockStyle const & style, CRect const & clip = { - {F32_MAX / 2, F32_MAX / 2}, - {F32_MAX, F32_MAX } + {F32_MAX / 2, F32_MAX / 2}, + {F32_MAX, F32_MAX } }); /// @brief Render Non-Indexed Triangles @@ -309,15 +309,15 @@ struct Canvas { // relocate lambda to heap Dyn lambda = - dyn(frame_arena.to_allocator(), static_cast(task)).unwrap(); + dyn(frame_arena.to_allocator(), static_cast(task)).unwrap(); // allocator is noop-ed but destructor still runs when the dynamic object is // uninitialized. the memory is freed by at the end of the frame anyway so // no need to free it - lambda.inner.allocator = noop_allocator; + lambda.allocator_ = noop_allocator; return add_pass( - Pass{.name = name, .task = transmute(std::move(lambda), fn(*lambda))}); + Pass{.name = name, .task = transmute(std::move(lambda), fn(*lambda))}); } }; -} // namespace ash +} // namespace ash diff --git a/ashura/engine/color.h b/ashura/engine/color.h index 79ac8a2d3..c45ddc0c2 100644 --- a/ashura/engine/color.h +++ b/ashura/engine/color.h @@ -17,7 +17,7 @@ inline constexpr Vec4U8 GREEN = Vec4U8{0x00, 0xFF, 0x00, 0xFF}; inline constexpr Vec4U8 CYAN = Vec4U8{0x00, 0xFF, 0xFF, 0xFF}; inline constexpr Vec4U8 MAGENTA = Vec4U8{0xFF, 0x00, 0xFF, 0xFF}; inline constexpr Vec4U8 YELLOW = Vec4U8{0xFF, 0xFF, 0x00, 0xFF}; -} // namespace colors +} // namespace colors // ios default system colors namespace ios @@ -100,9 +100,9 @@ inline constexpr Vec4U8 LIGHT_GRAY_5 = Vec4U8{216, 216, 220, 0xFF}; inline constexpr Vec4U8 DARK_GRAY_5 = Vec4U8{54, 54, 56, 0xFF}; inline constexpr Vec4U8 LIGHT_GRAY_6 = Vec4U8{235, 235, 240, 0xFF}; inline constexpr Vec4U8 DARK_GRAY_6 = Vec4U8{36, 36, 36, 0xFF}; -} // namespace accessible +} // namespace accessible -} // namespace ios +} // namespace ios /// @brief Material Design Colors namespace mdc @@ -383,7 +383,7 @@ inline constexpr Vec4U8 BLUE_GRAY_900 = Vec4U8{0x26, 0x32, 0x38, 0xFF}; inline constexpr Vec4U8 WHITE = Vec4U8{0xFF, 0xFF, 0xFF, 0xFF}; inline constexpr Vec4U8 BLACK = Vec4U8{0x00, 0x00, 0x00, 0xFF}; -} // namespace mdc +} // namespace mdc struct ColorGradient { @@ -398,20 +398,20 @@ struct ColorGradient { } - constexpr ColorGradient(Vec4U8 c) : ColorGradient{c.norm()} + constexpr ColorGradient(Vec4U8 c) : ColorGradient{norm(c)} { } constexpr ColorGradient(Vec4 tl, Vec4 tr, Vec4 bl, Vec4 br) : - tl{tl}, - tr{tr}, - bl{bl}, - br{br} + tl{tl}, + tr{tr}, + bl{bl}, + br{br} { } constexpr ColorGradient(Vec4U8 tl, Vec4U8 tr, Vec4U8 bl, Vec4U8 br) : - ColorGradient{tl.norm(), tr.norm(), bl.norm(), br.norm()} + ColorGradient{norm(tl), norm(tr), norm(bl), norm(br)} { } @@ -422,7 +422,7 @@ struct ColorGradient static constexpr ColorGradient x(Vec4U8 x0, Vec4U8 x1) { - return x(x0.norm(), x1.norm()); + return x(norm(x0), norm(x1)); } static constexpr ColorGradient y(Vec4 y0, Vec4 y1) @@ -432,7 +432,7 @@ struct ColorGradient static constexpr ColorGradient y(Vec4U8 y0, Vec4U8 y1) { - return y(y0.norm(), y1.norm()); + return y(norm(y0), norm(y1)); } constexpr Vec4 const & operator[](usize i) const @@ -451,4 +451,4 @@ struct ColorGradient } }; -} // namespace ash +} // namespace ash diff --git a/ashura/engine/engine.cc b/ashura/engine/engine.cc index 20c4e4786..91d5709be 100644 --- a/ashura/engine/engine.cc +++ b/ashura/engine/engine.cc @@ -1,5 +1,6 @@ /// SPDX-License-Identifier: MIT #include "ashura/engine/engine.h" +#include "ashura/std/async.h" #include "ashura/std/fs.h" #include "simdjson.h" @@ -15,16 +16,15 @@ EngineCfg EngineCfg::parse(AllocatorImpl allocator, Span json) simdjson::padded_string str{json.as_char().data(), json.size()}; simdjson::ondemand::document doc = parser.iterate(str); - auto config = doc.get_object().value(); + auto cfg = doc.get_object().value(); - std::string_view version = config["version"].get_string().value(); + std::string_view version = cfg["version"].get_string().value(); CHECK(version == "0.0.1"); - auto gpu = config["gpu"].get_object().value(); - out.gpu.validation = gpu["validation"].get_bool().value(); - out.gpu.vsync = gpu["vsync"].get_bool().value(); + out.gpu.validation = cfg["gpu.validation"].get_bool().value(); + out.gpu.vsync = cfg["gpu.vsync"].get_bool().value(); - auto gpu_prefs = gpu["preferences"].get_array().value(); + auto gpu_prefs = cfg["gpu.preferences"].get_array().value(); CHECK(gpu_prefs.count_elements().value() <= 5); for (auto pref : gpu_prefs) @@ -56,57 +56,78 @@ EngineCfg EngineCfg::parse(AllocatorImpl allocator, Span json) } } - out.gpu.hdr = gpu["hdr"].get_bool().value(); + out.gpu.hdr = cfg["gpu.hdr"].get_bool().value(); out.gpu.buffering = - (u32) clamp(gpu["buffering"].get_int64().value(), (i64) 0, (i64) 4); - - auto window = config["window"].get_object().value(); - out.window.resizable = window["resizable"].get_bool().value(); - out.window.maximized = window["maximized"].get_bool().value(); - out.window.full_screen = window["full_screen"].get_bool().value(); - out.window.width = - (u32) clamp(window["width"].get_int64().value(), (i64) 0, (i64) U32_MAX); - out.window.height = - (u32) clamp(window["height"].get_int64().value(), (i64) 0, (i64) U32_MAX); - - auto shaders = config["shaders"].get_object().value(); + (u32) clamp(cfg["gpu.buffering"].get_int64().value(), (i64) 0, (i64) 4); + + switch (cfg["gpu.msaa.level"].get_int64().value()) + { + case 1: + out.gpu.msaa_level = gpu::SampleCount::C1; + break; + case 2: + out.gpu.msaa_level = gpu::SampleCount::C2; + break; + case 4: + out.gpu.msaa_level = gpu::SampleCount::C4; + break; + case 8: + out.gpu.msaa_level = gpu::SampleCount::C8; + break; + case 16: + out.gpu.msaa_level = gpu::SampleCount::C16; + break; + default: + out.gpu.msaa_level = gpu::SampleCount::C4; + break; + } + + out.window.resizable = cfg["window.resizable"].get_bool().value(); + out.window.maximized = cfg["window.maximized"].get_bool().value(); + out.window.full_screen = cfg["window.full_screen"].get_bool().value(); + out.window.borderless = cfg["window.borderless"].get_bool().value(); + out.window.width = (u32) clamp(cfg["window.width"].get_int64().value(), + (i64) 0, (i64) U32_MAX); + out.window.height = (u32) clamp(cfg["window.height"].get_int64().value(), + (i64) 0, (i64) U32_MAX); + + auto shaders = cfg["shaders"].get_object().value(); for (auto entry : shaders) { std::string_view id = entry.escaped_key().value(); std::string_view path = entry.value().get_string().value(); out.shaders - .insert(vec(span(id), allocator).unwrap(), - vec(span(path), allocator).unwrap()) - .unwrap(); + .insert(vec(span(id), allocator).unwrap(), + vec(span(path), allocator).unwrap()) + .unwrap(); } - auto fonts = config["fonts"].get_object().value(); + auto fonts = cfg["fonts"].get_object().value(); for (auto entry : fonts) { std::string_view id = entry.escaped_key().value(); std::string_view path = entry.value().get_string().value(); out.fonts - .insert(vec(span(id), allocator).unwrap(), - vec(span(path), allocator).unwrap()) - .unwrap(); + .insert(vec(span(id), allocator).unwrap(), + vec(span(path), allocator).unwrap()) + .unwrap(); } - std::string_view default_font_sv = - config["default_font"].get_string().value(); - out.default_font = vec(default_font_sv, allocator).unwrap(); + std::string_view default_font = cfg["fonts.default"].get_string().value(); + out.default_font = vec(default_font, allocator).unwrap(); // check that it is a valid entry - fonts[default_font_sv].get_string().value(); + fonts[default_font].get_string().value(); - auto images = config["images"].get_object().value(); + auto images = cfg["images"].get_object().value(); for (auto entry : images) { std::string_view id = entry.escaped_key().value(); std::string_view path = entry.value().get_string().value(); out.images - .insert(vec(span(id), allocator).unwrap(), - vec(span(path), allocator).unwrap()) - .unwrap(); + .insert(vec(span(id), allocator).unwrap(), + vec(span(path), allocator).unwrap()) + .unwrap(); } return out; @@ -137,14 +158,14 @@ void Engine::init(AllocatorImpl allocator, void * app, logger->trace("Initializing Engine"); Dyn instance = - gpu::create_vulkan_instance(allocator, cfg.gpu.validation).unwrap(); + gpu::create_vulkan_instance(allocator, cfg.gpu.validation).unwrap(); gpu::Device * device = - instance->create_device(allocator, cfg.gpu.preferences, 2).unwrap(); + instance->create_device(allocator, cfg.gpu.preferences, 2).unwrap(); - GpuContext gpu_ctx = - GpuContext::create(allocator, device, cfg.gpu.hdr, cfg.gpu.buffering, - Vec2U{cfg.window.width, cfg.window.height}); + GpuSystem gpu = GpuSystem::create(allocator, device, cfg.gpu.hdr, + cfg.gpu.buffering, cfg.gpu.msaa_level, + Vec2U{cfg.window.width, cfg.window.height}); logger->trace("Initializing Renderer"); @@ -154,12 +175,10 @@ void Engine::init(AllocatorImpl allocator, void * app, ViewSystem view_system{allocator}; - ViewContext view_ctx{app, window_system->get_clipboard()}; - logger->trace("Creating Root Window"); Window window = - window_system->create_window(*instance, "Ashura"_str).unwrap(); + window_system->create_window(*instance, "Ashura"_str).unwrap(); if (cfg.window.maximized) { @@ -179,6 +198,15 @@ void Engine::init(AllocatorImpl allocator, void * app, window_system->make_windowed(window); } + if (cfg.window.borderless) + { + window_system->make_borderless(window); + } + else + { + window_system->make_bordered(window); + } + if (cfg.window.resizable) { window_system->make_resizable(window); @@ -194,172 +222,171 @@ void Engine::init(AllocatorImpl allocator, void * app, alignas(Engine) static u8 storage[sizeof(Engine)] = {}; - engine = new (storage) Engine{allocator, - app, - std::move(instance), - device, - window, - surface, - cfg.gpu.vsync ? gpu::PresentMode::Fifo : - gpu::PresentMode::Immediate, - std::move(gpu_ctx), - std::move(renderer), - std::move(canvas), - std::move(view_system), - std::move(view_ctx)}; + engine = new (storage) + Engine{allocator, + app, + std::move(instance), + device, + window, + window_system->get_clipboard(), + surface, + cfg.gpu.vsync ? gpu::PresentMode::Fifo : gpu::PresentMode::Immediate, + std::move(gpu), + std::move(renderer), + std::move(canvas), + std::move(view_system)}; window_system->listen(fn( - engine, +[](Engine * engine, SystemEvent const & event) { - event.match( - [&](SystemTheme theme) { - FrameState & state = engine->view_ctx.state_buffer; - state.theme = theme; - }, - [](SystemEventType) {}); - })); + engine, +[](Engine * engine, SystemEvent const & event) { + event.match( + [&](SystemTheme theme) { + InputState & f = engine->input_buffer; + f.theme = theme; + }, + [](SystemEventType) {}); + })); window_system->listen( - window, fn( - engine, +[](Engine * engine, WindowEvent const & event) { - FrameState & state = engine->view_ctx.state_buffer; - - event.match( - [&](KeyEvent e) { - switch (e.action) - { - case KeyAction::Press: - { - state.key.any_down = true; - set_bit(state.key.downs, (u32) e.key_code); - set_bit(state.key.scan_downs, (u32) e.scan_code); - state.key.modifiers |= e.modifiers; - } + window, fn( + engine, +[](Engine * engine, WindowEvent const & event) { + InputState & f = engine->input_buffer; + + event.match( + [&](KeyEvent e) { + switch (e.action) + { + case KeyAction::Press: + { + f.key.any_down = true; + set_bit(f.key.downs, (u32) e.key_code); + set_bit(f.key.scan_downs, (u32) e.scan_code); + f.key.modifiers |= e.modifiers; + } + break; + case KeyAction::Release: + { + f.key.any_up = true; + set_bit(f.key.ups, (u32) e.key_code); + set_bit(f.key.scan_ups, (u32) e.scan_code); + f.key.modifiers |= e.modifiers; + } + break; + default: + break; + } + }, + [&](MouseMotionEvent e) { + f.mouse.moved = true; + f.mouse.position = e.position; + f.mouse.translation += e.translation; + }, + [&](MouseClickEvent e) { + f.mouse.num_clicks[(u32) e.button] = e.clicks; + f.mouse.position = e.position; + switch (e.action) + { + case KeyAction::Press: + set_bit(f.mouse.downs, (u32) e.button); + f.mouse.any_down = true; + break; + case KeyAction::Release: + set_bit(f.mouse.ups, (u32) e.button); + f.mouse.any_up = true; + break; + default: + break; + } + }, + [&](MouseWheelEvent e) { + f.mouse.wheel_scrolled = true; + f.mouse.position = e.position; + f.mouse.translation += e.translation; + }, + [&](TextInputEvent e) { + f.text_input = true; + f.text.extend(e.text).unwrap(); + }, + [&](WindowEventType e) { + switch (e) + { + case WindowEventType::Shown: + case WindowEventType::Hidden: + case WindowEventType::Exposed: + case WindowEventType::Moved: + break; + case WindowEventType::Resized: + f.resized = true; + break; + case WindowEventType::SurfaceResized: + f.surface_resized = true; + break; + case WindowEventType::Minimized: + case WindowEventType::Maximized: + case WindowEventType::Restored: + break; + case WindowEventType::MouseEnter: + f.mouse.in = true; + f.mouse_focused = true; + break; + case WindowEventType::MouseLeave: + f.mouse.out = true; + f.mouse_focused = false; + break; + case WindowEventType::KeyboardFocusIn: + f.key.in = true; + f.key_focused = true; + break; + case WindowEventType::KeyboardFocusOut: + f.key.out = true; + f.key_focused = false; + break; + case WindowEventType::CloseRequested: + f.close_requested = true; + break; + case WindowEventType::Occluded: + case WindowEventType::EnterFullScreen: + case WindowEventType::LeaveFullScreen: + case WindowEventType::Destroyed: + break; + default: + break; + } + }, + [&](DropEvent const & e) { + e.match( + [&](DropEventType e) { + switch (e) + { + case DropEventType::DropBegin: break; - case KeyAction::Release: - { - state.key.any_up = true; - set_bit(state.key.ups, (u32) e.key_code); - set_bit(state.key.scan_ups, (u32) e.scan_code); - state.key.modifiers |= e.modifiers; - } + case DropEventType::DropComplete: + f.dropped = true; break; - default: - break; - } - }, - [&](MouseMotionEvent e) { - state.mouse.moved = true; - state.mouse.position = e.position; - state.mouse.translation += e.translation; - }, - [&](MouseClickEvent e) { - state.mouse.num_clicks[(u32) e.button] = e.clicks; - state.mouse.position = e.position; - switch (e.action) - { - case KeyAction::Press: - set_bit(state.mouse.downs, (u32) e.button); - state.mouse.any_down = true; - break; - case KeyAction::Release: - set_bit(state.mouse.ups, (u32) e.button); - state.mouse.any_up = true; - break; - default: - break; - } - }, - [&](MouseWheelEvent e) { - state.mouse.wheel_scrolled = true; - state.mouse.position = e.position; - state.mouse.translation += e.translation; - }, - [&](TextInputEvent e) { - state.text_input = true; - state.text.extend(e.text).unwrap(); - }, - [&](WindowEventType e) { - switch (e) - { - case WindowEventType::Shown: - case WindowEventType::Hidden: - case WindowEventType::Exposed: - case WindowEventType::Moved: - break; - case WindowEventType::Resized: - state.resized = true; - break; - case WindowEventType::SurfaceResized: - state.surface_resized = true; - break; - case WindowEventType::Minimized: - case WindowEventType::Maximized: - case WindowEventType::Restored: - break; - case WindowEventType::MouseEnter: - state.mouse.in = true; - state.mouse_focused = true; - break; - case WindowEventType::MouseLeave: - state.mouse.out = true; - state.mouse_focused = false; - break; - case WindowEventType::KeyboardFocusIn: - state.key.in = true; - state.key_focused = true; - break; - case WindowEventType::KeyboardFocusOut: - state.key.out = true; - state.key_focused = false; - break; - case WindowEventType::CloseRequested: - state.close_requested = true; - break; - case WindowEventType::Occluded: - case WindowEventType::EnterFullScreen: - case WindowEventType::LeaveFullScreen: - case WindowEventType::Destroyed: - break; - default: - break; - } - }, - [&](DropEvent const & e) { - e.match( - [&](DropEventType e) { - switch (e) - { - case DropEventType::DropBegin: - break; - case DropEventType::DropComplete: - state.dropped = true; - break; - default: - break; - } - }, - [&](DropPositionEvent e) { - state.drop_hovering = true; - state.mouse.position = e.pos; - }, - [&](DropFileEvent e) { - state.drop_data.clear(); - state.drop_data.extend(e.path.as_u8()).unwrap(); - state.drop_type = DropType::FilePath; - }, - [&](DropTextEvent e) { - state.drop_data.clear(); - state.drop_data.extend(e.text.as_u8()).unwrap(); - state.drop_type = DropType::Bytes; - }); - }); - })); + default: + break; + } + }, + [&](DropPositionEvent e) { + f.drop_hovering = true; + f.mouse.position = e.pos; + }, + [&](DropFileEvent e) { + f.drop_data.clear(); + f.drop_data.extend(e.path.as_u8()).unwrap(); + f.drop_type = DropType::FilePath; + }, + [&](DropTextEvent e) { + f.drop_data.clear(); + f.drop_data.extend(e.text.as_u8()).unwrap(); + f.drop_type = DropType::Bytes; + }); + }); + })); engine->device->begin_frame(nullptr).unwrap(); Semaphore sem = - create_semaphore(allocator, cfg.shaders.size() + cfg.fonts.size()) - .unwrap(); + create_semaphore(allocator, cfg.shaders.size() + cfg.fonts.size()).unwrap(); for (auto const & [id, path] : cfg.shaders) { @@ -385,6 +412,7 @@ void Engine::init(AllocatorImpl allocator, void * app, static_assert(std::endian::native == std::endian::little); +// [ ] always aligned!"" Vec data_u32{allocator}; data_u32.resize_uninit(data.size() >> 2).unwrap(); @@ -394,22 +422,21 @@ void Engine::init(AllocatorImpl allocator, void * app, logger->trace("Loaded shader ", shader_id, " from file"); async::once( - [shader_id = std::move(shader_id), sem = std::move(sem), - data_u32 = std::move(data_u32)]() mutable { - logger->trace("Sending shader ", shader_id, " to GPU"); - - gpu::Shader shader = - engine->device - ->create_shader(gpu::ShaderInfo{.label = "Shader"_str, - .spirv_code = data_u32}) - .unwrap(); - - engine->assets.shaders.insert(std::move(shader_id), shader) - .unwrap(); - - sem->increment(1); - }, - async::Ready{}, TaskSchedule{.target = TaskTarget::Main}); + [shader_id = std::move(shader_id), sem = std::move(sem), + data_u32 = std::move(data_u32)]() mutable { + logger->trace("Sending shader ", shader_id, " to GPU"); + + gpu::Shader shader = + engine->device + ->create_shader( + gpu::ShaderInfo{.label = "Shader"_str, .spirv_code = data_u32}) + .unwrap(); + + engine->assets.shaders.insert(std::move(shader_id), shader).unwrap(); + + sem->increment(1); + }, + async::Ready{}, TaskSchedule{.target = TaskTarget::Main}); }); } @@ -458,18 +485,18 @@ void Engine::init(AllocatorImpl allocator, void * app, logger->trace("Rasterized font ", font_id); async::once( - [font_id = std::move(font_id), sem = std::move(sem), - font = std::move(font), allocator]() mutable { - logger->trace("Uploading font ", font_id, " to GPU"); + [font_id = std::move(font_id), sem = std::move(sem), + font = std::move(font), allocator]() mutable { + logger->trace("Uploading font ", font_id, " to GPU"); - font->upload_to_device(engine->gpu_ctx, allocator); + font->upload_to_device(engine->gpu, allocator); - engine->assets.fonts.insert(std::move(font_id), std::move(font)) - .unwrap(); + engine->assets.fonts.insert(std::move(font_id), std::move(font)) + .unwrap(); - sem->increment(1); - }, - async::Ready{}, TaskSchedule{.target = TaskTarget::Main}); + sem->increment(1); + }, + async::Ready{}, TaskSchedule{.target = TaskTarget::Main}); }); } @@ -481,7 +508,7 @@ void Engine::init(AllocatorImpl allocator, void * app, engine->default_font_name = vec(cfg.default_font, allocator).unwrap(); engine->default_font = engine->assets.fonts[engine->default_font_name].get(); - engine->renderer.acquire(engine->gpu_ctx, engine->assets); + engine->renderer.acquire(engine->gpu, engine->assets); engine->device->submit_frame(nullptr).unwrap(); @@ -504,35 +531,34 @@ Engine::~Engine() for (auto const & [_, shader] : assets.shaders) { - device->uninit_shader(shader); + device->uninit(shader); } assets.shaders.clear(); for (auto const & [_, font] : assets.fonts) { - font->unload_from_device(gpu_ctx); + font->unload_from_device(gpu); } assets.fonts.clear(); - renderer.release(gpu_ctx, assets); + renderer.release(gpu, assets); - gpu_ctx.uninit(); + gpu.uninit(); - device->uninit_swapchain(swapchain); + device->uninit(swapchain); window_system->uninit_window(window); logger->trace("Uninitializing Window System"); WindowSystem::uninit(); - instance->uninit_device(device); + instance->uninit(device); } void Engine::recreate_swapchain_() { gpu::SurfaceCapabilities capabilities = - device->get_surface_capabilities(surface).unwrap(); - CHECK( - has_bits(capabilities.image_usage, gpu::ImageUsage::TransferDst | + device->get_surface_capabilities(surface).unwrap(); + CHECK(has_bits(capabilities.image_usage, gpu::ImageUsage::TransferDst | gpu::ImageUsage::ColorAttachment)); Vec formats{allocator}; @@ -546,26 +572,26 @@ void Engine::recreate_swapchain_() surface_extent.y = max(surface_extent.y, 1U); gpu::ColorSpace preferred_color_spaces[] = { - gpu::ColorSpace::DCI_P3_NONLINEAR, - gpu::ColorSpace::DISPLAY_P3_NONLINEAR, - gpu::ColorSpace::DISPLAY_P3_LINEAR, - gpu::ColorSpace::ADOBERGB_LINEAR, - gpu::ColorSpace::ADOBERGB_NONLINEAR, - gpu::ColorSpace::SRGB_NONLINEAR, - gpu::ColorSpace::EXTENDED_SRGB_LINEAR, - gpu::ColorSpace::EXTENDED_SRGB_NONLINEAR, - gpu::ColorSpace::DOLBYVISION, - gpu::ColorSpace::HDR10_ST2084, - gpu::ColorSpace::HDR10_HLG, - gpu::ColorSpace::BT709_LINEAR, - gpu::ColorSpace::BT709_NONLINEAR, - gpu::ColorSpace::BT2020_LINEAR, - gpu::ColorSpace::PASS_THROUGH}; + gpu::ColorSpace::DCI_P3_NONLINEAR, + gpu::ColorSpace::DISPLAY_P3_NONLINEAR, + gpu::ColorSpace::DISPLAY_P3_LINEAR, + gpu::ColorSpace::ADOBERGB_LINEAR, + gpu::ColorSpace::ADOBERGB_NONLINEAR, + gpu::ColorSpace::SRGB_NONLINEAR, + gpu::ColorSpace::EXTENDED_SRGB_LINEAR, + gpu::ColorSpace::EXTENDED_SRGB_NONLINEAR, + gpu::ColorSpace::DOLBYVISION, + gpu::ColorSpace::HDR10_ST2084, + gpu::ColorSpace::HDR10_HLG, + gpu::ColorSpace::BT709_LINEAR, + gpu::ColorSpace::BT709_NONLINEAR, + gpu::ColorSpace::BT2020_LINEAR, + gpu::ColorSpace::PASS_THROUGH}; gpu::PresentMode preferred_present_modes[] = { - present_mode_preference, gpu::PresentMode::Immediate, - gpu::PresentMode::Mailbox, gpu::PresentMode::Fifo, - gpu::PresentMode::FifoRelaxed}; + present_mode_preference, gpu::PresentMode::Immediate, + gpu::PresentMode::Mailbox, gpu::PresentMode::Fifo, + gpu::PresentMode::FifoRelaxed}; bool found_format = false; gpu::SurfaceFormat format; @@ -621,7 +647,7 @@ void Engine::recreate_swapchain_() .format = format, .usage = gpu::ImageUsage::TransferDst | gpu::ImageUsage::ColorAttachment, - .preferred_buffering = gpu_ctx.buffering, + .preferred_buffering = gpu.buffering, .present_mode = present_mode, .preferred_extent = surface_extent, .composite_alpha = alpha}; @@ -636,7 +662,7 @@ void Engine::recreate_swapchain_() } } -void Engine::run(View & view, Fn loop) +void Engine::run(View & view, Fn loop) { logger->trace("Starting Engine Run Loop"); @@ -647,83 +673,53 @@ void Engine::run(View & view, Fn loop) time_point timestamp = steady_clock::now(); - // [ ] remove !view_ctx.close_requested once we have proper cooperative shutdown - while (!should_shutdown && !view_ctx.close_requested) + while (true) { time_point const current_time = steady_clock::now(); - nanoseconds const time_delta = current_time - timestamp; + nanoseconds const timedelta = current_time - timestamp; timestamp = current_time; - view_ctx.swap_frame_state_(); - auto & state = view_ctx.state_buffer; + input_buffer.clear(); + input_buffer.stamp(timestamp, timedelta); - state.clear_frame_(); + input_buffer.mouse.position = + window_system->get_mouse_state(input_buffer.mouse.states); + window_system->get_keyboard_state(input_buffer.key.states); window_system->poll_events(); - // [ ] EVENTS NEED TO BE UPDATED 1 FRAME LATER, NOT IMMEDIATELY AS RECEIVED - - // [ ] get mouse position? - - window_system->get_mouse_state(state.mouse.states); - window_system->get_keyboard_state(state.key.states); - - gpu_ctx.begin_frame(swapchain); + // [ ] proper cooperative shutdown is needed + if (input_buffer.close_requested) + { + break; + } - if (view_ctx.resized || view_ctx.surface_resized) + if (input_buffer.resized || input_buffer.surface_resized) { Vec2U const surface_size = window_system->get_surface_size(window); - gpu_ctx.recreate_framebuffers(surface_size); + gpu.recreate_framebuffers(surface_size); } - view_ctx.viewport_extent = as_vec2(gpu_ctx.screen_fb.extent); - - gpu::RenderingAttachment attachments[] = { - {.view = gpu_ctx.screen_fb.color.view, - .resolve = nullptr, - .resolve_mode = gpu::ResolveModes::None, - .load_op = gpu::LoadOp::Load, - .store_op = gpu::StoreOp::Store, - .clear = {}} - }; - - RenderTarget rt{ - .info = - gpu::RenderingInfo{ - .render_area = {.offset = {}, - .extent = gpu_ctx.screen_fb.extent}, - .num_layers = 1, - .color_attachments = attachments, - .depth_attachment = {}, - .stencil_attachment = {}}, - .viewport = gpu::Viewport{.offset = {0, 0}, - .extent = as_vec2(gpu_ctx.screen_fb.extent), - .min_depth = 0, - .max_depth = 1}, - .extent = gpu_ctx.screen_fb.extent, - .color_descriptor = gpu_ctx.screen_fb.color_texture, - .depth_descriptor = nullptr, - .stencil_descriptor = nullptr - }; - - view_ctx.begin_frame_(timestamp, time_delta); - - canvas.begin_recording(Vec2{(f32) rt.extent.x, (f32) rt.extent.y}, - rt.extent); - - loop(timestamp, time_delta); - - view_system.tick(view_ctx, view, canvas); + gpu.begin_frame(swapchain); + + // [ ] wrong! use window extent? + // [ ] propert use of window extent and surface size. then store sizing. + // [ ] this will be needed for high density displays, i,e. apple devices + input_buffer.viewport_extent = as_vec2(gpu.fb.extent()); + + canvas.begin_recording(gpu.fb.viewport().extent, gpu.fb.extent()); + + view_system.tick(input_buffer, view, canvas, loop); canvas.end_recording(); - renderer.begin_frame(gpu_ctx, rt, canvas); - renderer.render_frame(gpu_ctx, rt, canvas); - renderer.end_frame(gpu_ctx, rt, canvas); - gpu_ctx.submit_frame(swapchain); + renderer.begin_frame(gpu, gpu.fb, canvas); + renderer.render_frame(gpu, gpu.fb, canvas); + renderer.end_frame(gpu, gpu.fb, canvas); + gpu.submit_frame(swapchain); } logger->trace("Ended Engine Run Loop"); } -} // namespace ash +} // namespace ash diff --git a/ashura/engine/engine.h b/ashura/engine/engine.h index b1f8d65f0..b90076f59 100644 --- a/ashura/engine/engine.h +++ b/ashura/engine/engine.h @@ -1,11 +1,9 @@ /// SPDX-License-Identifier: MIT #pragma once #include "ashura/engine/font.h" -#include "ashura/engine/gpu_context.h" -#include "ashura/engine/image_decoder.h" +#include "ashura/engine/gpu_system.h" #include "ashura/engine/view_system.h" #include "ashura/engine/window.h" -#include "ashura/std/async.h" #include "ashura/std/cfg.h" namespace ash @@ -18,8 +16,9 @@ struct EngineCfg bool validation = false; bool vsync = true; InplaceVec preferences{}; - bool hdr = true; - u32 buffering = 2; + bool hdr = true; + u32 buffering = 2; + gpu::SampleCount msaa_level = gpu::SampleCount::C4; }; struct Window @@ -27,6 +26,7 @@ struct EngineCfg bool resizable = true; bool maximized = false; bool full_screen = false; + bool borderless = false; u32 width = 1'920; u32 height = 1'080; }; @@ -46,11 +46,6 @@ struct EngineCfg static EngineCfg parse(AllocatorImpl allocator, Span json); }; -// [ ] Image Manager -// [ ] Audio Device/Manager (FFMPEG, SDL) -// [ ] Video Manager (Vulkan, FFMPEG) -// [ ] Custom Subsystems - struct Engine { AllocatorImpl allocator; @@ -63,13 +58,15 @@ struct Engine Window window; + ClipBoard * clipboard; + gpu::Surface surface; gpu::Swapchain swapchain = nullptr; gpu::PresentMode present_mode_preference; - GpuContext gpu_ctx; + GpuSystem gpu; Renderer renderer; @@ -77,38 +74,41 @@ struct Engine ViewSystem view_system; - ViewContext view_ctx; - AssetMap assets; Vec default_font_name; Font * default_font = nullptr; - bool should_shutdown = false; + InputState input_buffer; Engine(AllocatorImpl allocator, void * app, Dyn instance, - gpu::Device * device, Window window, gpu::Surface surface, - gpu::PresentMode present_mode_preference, GpuContext gpu_ctx, - Renderer renderer, Canvas canvas, ViewSystem view_system, - ViewContext view_ctx) : - allocator{allocator}, - app{app}, - instance{std::move(instance)}, - device{device}, - window{window}, - surface{surface}, - present_mode_preference{present_mode_preference}, - gpu_ctx{std::move(gpu_ctx)}, - renderer{std::move(renderer)}, - canvas{std::move(canvas)}, - view_system{std::move(view_system)}, - view_ctx{std::move(view_ctx)}, - assets{allocator}, - default_font_name{allocator} + gpu::Device * device, Window window, ClipBoard & clipboard, + gpu::Surface surface, gpu::PresentMode present_mode_preference, + GpuSystem gpu, Renderer renderer, Canvas canvas, + ViewSystem view_system) : + allocator{allocator}, + app{app}, + instance{std::move(instance)}, + device{device}, + window{window}, + clipboard{&clipboard}, + surface{surface}, + present_mode_preference{present_mode_preference}, + gpu{std::move(gpu)}, + renderer{std::move(renderer)}, + canvas{std::move(canvas)}, + view_system{std::move(view_system)}, + assets{allocator}, + default_font_name{allocator}, + input_buffer{allocator} { } + Engine(Engine const &) = delete; + Engine & operator=(Engine const &) = delete; + Engine(Engine &&) = default; + Engine & operator=(Engine &&) = default; ~Engine(); static void init(AllocatorImpl allocator, void * app, @@ -118,11 +118,11 @@ struct Engine void recreate_swapchain_(); - void run(View & view, Fn loop = noop); + void run(View & view, Fn loop = noop); }; /// Global Engine Pointer. Can be hooked at runtime for dynamically loaded /// executables. ASH_C_LINKAGE ASH_DLL_EXPORT Engine * engine; -} // namespace ash +} // namespace ash diff --git a/ashura/engine/font.h b/ashura/engine/font.h index 993c9a684..d59a2ca62 100644 --- a/ashura/engine/font.h +++ b/ashura/engine/font.h @@ -1,7 +1,8 @@ /// SPDX-License-Identifier: MIT #pragma once -#include "ashura/engine/gpu_context.h" +#include "ashura/engine/font.h" +#include "ashura/engine/gpu_system.h" #include "ashura/gpu/gpu.h" #include "ashura/std/dyn.h" #include "ashura/std/image.h" @@ -13,6 +14,8 @@ namespace ash // App Unit (AU) inline constexpr i32 AU_UNIT = 128 * 64; +inline constexpr f32 AU_SCALE = 1 / (f32) AU_UNIT; + static_assert((AU_UNIT % 64) == 0, "App Unit needs to be in 26.6 Fractional Unit"); @@ -21,7 +24,7 @@ static_assert((AU_UNIT / 64) >= 64, constexpr f32 au_to_px(i32 au, f32 base) { - return (au / (f32) AU_UNIT) * base; + return au * AU_SCALE * base; } constexpr Vec2 au_to_px(Vec2I au, f32 base) @@ -37,7 +40,7 @@ enum class FontErr : u8 OutOfMemory = 3 }; -constexpr Span to_string(FontErr err) +constexpr Span to_str(FontErr err) { switch (err) { @@ -59,10 +62,10 @@ namespace fmt inline bool push(Context const & ctx, Spec const & spec, FontErr const & err) { - return push(ctx, spec, to_string(err)); + return push(ctx, spec, to_str(err)); } -} // namespace fmt +} // namespace fmt /// @brief Glyph Metrics. expressed on an AU_UNIT scale /// @param bearing offset from cursor baseline to start drawing glyph from (au) @@ -76,6 +79,7 @@ struct GlyphMetrics Vec2I extent = {}; }; +/// @brief normalized font metrics /// @param ascent maximum ascent of the font's glyphs (au) /// @param descent maximum descent of the font's glyphs (au) /// @param advance maximum advance of the font's glyphs (au) @@ -86,20 +90,9 @@ struct FontMetrics i32 advance = 0; }; -/// @param is_valid if the glyph was found in the font and loaded -// successfully -/// @param metrics normalized font metrics -/// @param bin atlas layer this glyph belongs to -/// @param offset, extent: area in the atlas this glyph's cache data is placed -/// @param uv0, uv1: normalized texture coordinates of this -/// glyph in the atlas bin -struct Glyph -{ - bool is_valid = false; - GlyphMetrics metrics = {}; -}; - -/// @param rasterized if the glyph was rasterized +/// @param later atlas layer this glyph belongs to +/// @param area area in the atlas this glyph's cache data is placed +/// @param uv normalized texture coordinates of this glyph in the layer struct AtlasGlyph { u32 layer = 0; @@ -107,8 +100,34 @@ struct AtlasGlyph Vec2 uv[2] = {}; }; -typedef struct CpuFontAtlas CpuFontAtlas; -typedef struct GpuFontAtlas GpuFontAtlas; +struct CpuFontAtlas +{ + i32 font_height = 0; + Vec2U extent = {}; + u32 num_layers = 0; + Vec glyphs = {}; + Vec channels = {}; + + ImageLayerSpan span() const + { + return ImageLayerSpan{.channels = channels, + .width = extent.x, + .height = extent.y, + .layers = num_layers}; + } +}; + +struct GpuFontAtlas +{ + gpu::Image image = nullptr; + Vec views = {}; + Vec textures = {}; + i32 font_height = 0; + u32 num_layers = 0; + Vec2U extent = {}; + Vec glyphs = {}; + gpu::Format format = gpu::Format::Undefined; +}; /// @param postscript_name ASCII. i.e. RobotoBold /// @param family_name ASCII. i.e. Roboto @@ -121,23 +140,22 @@ typedef struct GpuFontAtlas GpuFontAtlas; /// @param gpu_atlas gpu font atlas if loaded struct FontInfo { - Span postscript_name = {}; - Span family_name = {}; - Span style_name = {}; - Span glyphs = {}; - u32 replacement_glyph = 0; - u32 space_glyph = 0; - u32 ellipsis_glyph = 0; - FontMetrics metrics = {}; - Option cpu_atlas = None; - Option gpu_atlas = None; + Span postscript_name = {}; + Span family_name = {}; + Span style_name = {}; + Span glyphs = {}; + u32 replacement_glyph = 0; + u32 space_glyph = 0; + u32 ellipsis_glyph = 0; + FontMetrics metrics = {}; + OptionRef cpu_atlas = none; + OptionRef gpu_atlas = none; }; struct Font { - static Result, FontErr> decode(Span encoded, - u32 face = 0, - AllocatorImpl allocator = {}); + static Result, FontErr> + decode(Span encoded, u32 face = 0, AllocatorImpl allocator = {}); /// @brief rasterize the font at the specified font height. Note: raster is /// stored as alpha values. @@ -149,11 +167,11 @@ struct Font virtual FontInfo info() = 0; - virtual void upload_to_device(GpuContext & c, AllocatorImpl allocator) = 0; + virtual void upload_to_device(GpuSystem & c, AllocatorImpl allocator) = 0; - virtual void unload_from_device(GpuContext & c) = 0; + virtual void unload_from_device(GpuSystem & c) = 0; virtual ~Font() = default; }; -} // namespace ash +} // namespace ash diff --git a/ashura/engine/font_atlas.h b/ashura/engine/font_atlas.h deleted file mode 100644 index 285416c5b..000000000 --- a/ashura/engine/font_atlas.h +++ /dev/null @@ -1,42 +0,0 @@ -/// SPDX-License-Identifier: MIT -#pragma once - -#include "ashura/engine/font.h" -#include "ashura/engine/gpu_context.h" -#include "ashura/gpu/gpu.h" -#include "ashura/std/image.h" -#include "ashura/std/types.h" - -namespace ash -{ - -struct CpuFontAtlas -{ - i32 font_height = 0; - Vec2U extent = {}; - u32 num_layers = 0; - Vec glyphs = {}; - Vec channels = {}; - - ImageLayerSpan span() const - { - return ImageLayerSpan{.channels = channels, - .width = extent.x, - .height = extent.y, - .layers = num_layers}; - } -}; - -struct GpuFontAtlas -{ - gpu::Image image = nullptr; - Vec views = {}; - Vec textures = {}; - i32 font_height = 0; - u32 num_layers = 0; - Vec2U extent = {}; - Vec glyphs = {}; - gpu::Format format = gpu::Format::Undefined; -}; - -} // namespace ash diff --git a/ashura/engine/font_impl.cc b/ashura/engine/font_impl.cc index dab737ba1..a82b5e190 100644 --- a/ashura/engine/font_impl.cc +++ b/ashura/engine/font_impl.cc @@ -14,8 +14,8 @@ Result, FontErr> Font::decode(Span encoded, u32 face, } hb_blob_t * hb_blob = - hb_blob_create(font_data.data(), font_data.size(), - HB_MEMORY_MODE_READONLY, nullptr, nullptr); + hb_blob_create(font_data.data(), font_data.size(), HB_MEMORY_MODE_READONLY, + nullptr, nullptr); if (hb_blob == nullptr) { @@ -148,41 +148,34 @@ Result, FontErr> Font::decode(Span encoded, u32 face, i32 const descent = -ft_face->size->metrics.descender; i32 const advance = ft_face->size->metrics.max_advance; - Vec glyphs{allocator}; + Vec glyphs{allocator}; - if (!glyphs.resize_uninit(num_glyphs)) + if (!glyphs.resize(num_glyphs)) { return Err{FontErr::OutOfMemory}; } - for (u32 i = 0; i < num_glyphs; i++) + for (auto [i, metric] : enumerate(glyphs)) { if (FT_Load_Glyph(ft_face, i, FT_LOAD_DEFAULT) == 0) { FT_GlyphSlot s = ft_face->glyph; - GlyphMetrics m{ - .bearing{(i32) s->metrics.horiBearingX, - (i32) -s->metrics.horiBearingY }, - .advance = (i32) s->metrics.horiAdvance, - .extent{(i32) s->metrics.width, (i32) s->metrics.height} - }; - // bin offsets are determined after binning and during rect packing - glyphs[i] = Glyph{.is_valid = true, .metrics = m}; - } - else - { - glyphs[i] = Glyph{.is_valid = false, .metrics = {}}; + metric = GlyphMetrics{ + .bearing{(i32) s->metrics.horiBearingX, (i32) -s->metrics.horiBearingY}, + .advance = (i32) s->metrics.horiAdvance, + .extent{(i32) s->metrics.width, (i32) s->metrics.height } + }; } } Result font = dyn_inplace( - allocator, std::move(font_data), std::move(postscript_name), - std::move(family_name), std::move(style_name), hb_blob, hb_face, hb_font, - ft_lib, ft_face, face, std::move(glyphs), replacement_glyph, - ellipsis_glyph, space_glyph, - FontMetrics{.ascent = ascent, .descent = descent, .advance = advance}); + allocator, std::move(font_data), std::move(postscript_name), + std::move(family_name), std::move(style_name), hb_blob, hb_face, hb_font, + ft_lib, ft_face, face, std::move(glyphs), replacement_glyph, ellipsis_glyph, + space_glyph, + FontMetrics{.ascent = ascent, .descent = descent, .advance = advance}); if (!font) { @@ -200,4 +193,4 @@ Result, FontErr> Font::decode(Span encoded, u32 face, return Ok{transmute(std::move(font.value()), font_base)}; } -} // namespace ash +} // namespace ash diff --git a/ashura/engine/font_impl.h b/ashura/engine/font_impl.h index 6efc103ef..c2e7fe71f 100644 --- a/ashura/engine/font_impl.h +++ b/ashura/engine/font_impl.h @@ -1,14 +1,9 @@ /// SPDX-License-Identifier: MIT #pragma once -#define STBIRDEF extern "C" inline - #include "ashura/engine/font.h" -#include "ashura/engine/font_atlas.h" #include "ashura/engine/rect_pack.h" -#include "ashura/gpu/image.h" #include "ashura/std/allocator.h" -#include "ashura/std/allocators.h" #include "ashura/std/error.h" #include "ashura/std/range.h" #include "ashura/std/types.h" @@ -49,7 +44,7 @@ struct FontImpl : Font u32 face; - Vec glyphs; + Vec glyphs; u32 replacement_glyph; @@ -59,30 +54,31 @@ struct FontImpl : Font FontMetrics metrics; - Option cpu_atlas = None; + Option cpu_atlas = none; - Option gpu_atlas = None; + Option gpu_atlas = none; FontImpl(Vec font_data, Vec postscript_name, Vec family_name, Vec style_name, hb_blob_t * hb_blob, hb_face_t * hb_face, hb_font_t * hb_font, FT_Library ft_lib, - FT_Face ft_face, u32 face, Vec glyphs, u32 replacement_glyph, - u32 ellipsis_glyph, u32 space_glyph, FontMetrics metrics) : - font_data{std::move(font_data)}, - postscript_name{std::move(postscript_name)}, - family_name{std::move(family_name)}, - style_name{std::move(style_name)}, - hb_blob{hb_blob}, - hb_face{hb_face}, - hb_font{hb_font}, - ft_lib{ft_lib}, - ft_face{ft_face}, - face{face}, - glyphs{std::move(glyphs)}, - replacement_glyph{replacement_glyph}, - ellipsis_glyph{ellipsis_glyph}, - space_glyph{space_glyph}, - metrics{metrics} + FT_Face ft_face, u32 face, Vec glyphs, + u32 replacement_glyph, u32 ellipsis_glyph, u32 space_glyph, + FontMetrics metrics) : + font_data{std::move(font_data)}, + postscript_name{std::move(postscript_name)}, + family_name{std::move(family_name)}, + style_name{std::move(style_name)}, + hb_blob{hb_blob}, + hb_face{hb_face}, + hb_font{hb_font}, + ft_lib{ft_lib}, + ft_face{ft_face}, + face{face}, + glyphs{std::move(glyphs)}, + replacement_glyph{replacement_glyph}, + ellipsis_glyph{ellipsis_glyph}, + space_glyph{space_glyph}, + metrics{metrics} { } @@ -117,12 +113,12 @@ struct FontImpl : Font if (cpu_atlas.is_some()) { - info.cpu_atlas = Some{&cpu_atlas.value()}; + info.cpu_atlas = cpu_atlas.value(); } if (gpu_atlas.is_some()) { - info.gpu_atlas = Some{&gpu_atlas.value()}; + info.gpu_atlas = gpu_atlas.value(); } return info; @@ -147,7 +143,9 @@ struct FontImpl : Font CpuFontAtlas atlas; - if (!atlas.glyphs.resize(glyphs.size32())) + u32 const num_glyphs = glyphs.size32(); + + if (!atlas.glyphs.resize(num_glyphs)) { return Err{}; } @@ -157,106 +155,92 @@ struct FontImpl : Font return Err{}; } - u32 num_rasterized_glyphs = 0; - - for (u32 i = 0; i < glyphs.size32(); i++) + for (auto [i, g] : enumerate(atlas.glyphs)) { - if (glyphs[i].is_valid) + FT_Error ft_error = FT_Load_Glyph(ft_face, i, FT_LOAD_DEFAULT); + if (ft_error != 0) { - FT_Error ft_error = FT_Load_Glyph(ft_face, i, FT_LOAD_DEFAULT); - if (ft_error != 0) - { - continue; - } - - Vec2U extent{ft_face->glyph->bitmap.width, ft_face->glyph->bitmap.rows}; - - if (extent.x == 0 || extent.y == 0) - { - continue; - } - - atlas.glyphs[i].area.extent = extent; - - num_rasterized_glyphs++; + continue; } + + g.area.extent = + Vec2U{ft_face->glyph->bitmap.width, ft_face->glyph->bitmap.rows}; } + static constexpr u16 GLYPH_PADDING = 1; + u32 num_layers = 0; { - Vec rects{allocator}; + Vec rects{allocator}; + RectPacker packer = RectPacker::make(as_vec2i(atlas_extent), allocator); - if (!rects.resize_uninit(num_rasterized_glyphs)) + if (!rects.resize_uninit(num_glyphs)) { return Err{}; } - for (u32 g = 0, irect = 0; g < glyphs.size32(); g++) + for (auto [i, gl, ag, rect] : + zip(range(size32(glyphs)), glyphs, atlas.glyphs, rects)) { - Glyph const & gl = glyphs[g]; - AtlasGlyph const & agl = atlas.glyphs[g]; - // only assign packing rects to the valid glyphs - if (gl.is_valid && agl.area.extent.x != 0 && agl.area.extent.y != 0) + // added padding to avoid texture spilling due to accumulated + // floating-point uv interpolation errors + Vec2U padded_extent{}; + + if (ag.area.extent.x != 0 && ag.area.extent.y != 0) { - rect_pack::rect & r = rects[irect]; - r.glyph_index = g; - r.x = 0; - r.y = 0; - // added padding to avoid texture spilling due to accumulated - // floating-point uv interpolation errors - r.w = (i32) (agl.area.extent.x + 2); - r.h = (i32) (agl.area.extent.y + 2); - atlas_extent.x = max(atlas_extent.x, agl.area.extent.x + 2); - atlas_extent.y = max(atlas_extent.y, agl.area.extent.y + 2); - irect++; + padded_extent = ag.area.extent + GLYPH_PADDING * 2; } + + rect = PackRect{.pos = {}, + .extent = as_vec2i(padded_extent), + .packed = false, + .id = i}; + + atlas_extent.x = max(atlas_extent.x, padded_extent.x); + atlas_extent.y = max(atlas_extent.y, padded_extent.y); } CHECK(atlas_extent.x <= gpu::MAX_IMAGE_EXTENT_2D); CHECK(atlas_extent.y <= gpu::MAX_IMAGE_EXTENT_2D); - Vec nodes{allocator}; - - if (!nodes.resize_uninit(atlas_extent.x)) - { - return Err{}; - } + Vec2 const atlas_scale = 1 / as_vec2(atlas_extent); - u32 num_packed = 0; - bool all_packed = false; + u32 num_packed = 0; - while (!all_packed) + while (num_packed < num_glyphs) { // tries to pack all the glyph rects into the provided extent - rect_pack::Context pack_context = rect_pack::init( - atlas_extent.x, atlas_extent.y, nodes.data(), atlas_extent.x, true); - all_packed = - rect_pack::pack_rects(pack_context, rects.data() + num_packed, - num_rasterized_glyphs - num_packed); - auto [just_packed, unpacked] = - partition(rects.view().slice(num_packed), - [](rect_pack::rect const & r) { return r.was_packed; }); - CHECK(!just_packed.is_empty()); - for (auto & rect : rects.view().slice(num_packed, just_packed.size32())) + packer.reset(as_vec2i(atlas_extent)); + auto [packed, unpacked] = packer.pack(rects.view().slice(num_packed)); + CHECK(!packed.is_empty()); + for (PackRect & rect : rects.view().slice(num_packed)) { - rect.layer = num_layers; + atlas.glyphs[rect.id].layer = num_layers; } - num_packed += just_packed.size32(); + num_packed += packed.size32(); num_layers++; } // sanity check. ideally all should have been packed - CHECK(all_packed); + CHECK(num_packed == num_glyphs); - for (u32 i = 0; i < num_rasterized_glyphs; i++) + for (u32 i = 0; i < num_glyphs; i++) { - rect_pack::rect r = rects[i]; - AtlasGlyph & g = atlas.glyphs[r.glyph_index]; - g.area.offset.x = (u32) r.x + 1; - g.area.offset.y = (u32) r.y + 1; - g.layer = r.layer; - g.uv[0] = as_vec2(g.area.offset) / as_vec2(atlas_extent); - g.uv[1] = as_vec2(g.area.end()) / as_vec2(atlas_extent); + PackRect const & r = rects[i]; + AtlasGlyph & g = atlas.glyphs[r.id]; + + if (g.area.extent.x == 0 | g.area.extent.y == 0) + { + // adjust back to original position from the padded position + g.area.offset = as_vec2u(r.pos + GLYPH_PADDING); + } + else + { + g.area.offset = Vec2U{}; + } + + g.uv[0] = as_vec2(g.area.offset) * atlas_scale; + g.uv[1] = as_vec2(g.area.end()) * atlas_scale; } } @@ -274,48 +258,42 @@ struct FontImpl : Font .height = atlas_extent.y, .layers = num_layers}; - for (u32 i = 0; i < glyphs.size32(); i++) + for (auto [i, ag] : enumerate(atlas.glyphs)) { - Glyph const & g = glyphs[i]; - AtlasGlyph const & ag = atlas.glyphs[i]; - if (g.is_valid) + FT_GlyphSlot slot = ft_face->glyph; + FT_Error ft_error = FT_Load_Glyph( + ft_face, i, FT_LOAD_DEFAULT | FT_LOAD_RENDER | FT_LOAD_NO_HINTING); + if (ft_error != 0) { - FT_GlyphSlot slot = ft_face->glyph; - FT_Error ft_error = FT_Load_Glyph( - ft_face, i, FT_LOAD_DEFAULT | FT_LOAD_RENDER | FT_LOAD_NO_HINTING); - if (ft_error != 0) - { - continue; - } + continue; + } - CHECK(slot->bitmap.pixel_mode == FT_PIXEL_MODE_GRAY); - /// we don't want to handle negative pitches - CHECK(slot->bitmap.pitch >= 0); + CHECK(slot->bitmap.pixel_mode == FT_PIXEL_MODE_GRAY); + /// we don't want to handle negative pitches + CHECK(slot->bitmap.pitch >= 0); - ImageSpan src{ - .channels = {slot->bitmap.buffer, - slot->bitmap.rows * (u32) slot->bitmap.pitch}, - .width = slot->bitmap.width, - .height = slot->bitmap.rows, - .stride = (u32) slot->bitmap.pitch - }; + ImageSpan src{ + .channels{slot->bitmap.buffer, + slot->bitmap.rows * (u32) slot->bitmap.pitch}, + .width = slot->bitmap.width, + .height = slot->bitmap.rows, + .stride = (u32) slot->bitmap.pitch + }; - copy_image(src, atlas_span.get_layer(ag.layer).slice(ag.area.offset, - ag.area.extent)); - } + copy_image(src, atlas_span.get_layer(ag.layer).slice(ag.area.offset, + ag.area.extent)); } atlas.font_height = font_height; atlas.extent = atlas_extent; atlas.num_layers = num_layers; - cpu_atlas = Some{std::move(atlas)}; + cpu_atlas = std::move(atlas); return Ok{}; } - virtual void upload_to_device(GpuContext & c, - AllocatorImpl allocator) override + virtual void upload_to_device(GpuSystem & c, AllocatorImpl allocator) override { gpu::CommandEncoder & enc = c.encoder(); gpu::Device & dev = *c.device; @@ -330,22 +308,21 @@ struct FontImpl : Font CHECK(atlas.extent.y > 0); gpu::Image image = - dev.create_image(gpu::ImageInfo{ - .label = "Font Atlas Image"_str, - .type = gpu::ImageType::Type2D, - .format = gpu::Format::B8G8R8A8_UNORM, - .usage = gpu::ImageUsage::Sampled | - gpu::ImageUsage::InputAttachment | - gpu::ImageUsage::Storage | - gpu::ImageUsage::TransferSrc | - gpu::ImageUsage::TransferDst, - .aspects = gpu::ImageAspects::Color, - .extent = {atlas.extent.x, atlas.extent.y, 1}, - .mip_levels = 1, - .array_layers = atlas.num_layers, - .sample_count = gpu::SampleCount::Count1 + dev + .create_image(gpu::ImageInfo{ + .label = "Font Atlas Image"_str, + .type = gpu::ImageType::Type2D, + .format = gpu::Format::B8G8R8A8_UNORM, + .usage = gpu::ImageUsage::Sampled | gpu::ImageUsage::InputAttachment | + gpu::ImageUsage::Storage | gpu::ImageUsage::TransferSrc | + gpu::ImageUsage::TransferDst, + .aspects = gpu::ImageAspects::Color, + .extent{atlas.extent.x, atlas.extent.y, 1}, + .mip_levels = 1, + .array_layers = atlas.num_layers, + .sample_count = gpu::SampleCount::C1 }) - .unwrap(); + .unwrap(); Vec views; @@ -353,38 +330,38 @@ struct FontImpl : Font for (u32 i = 0; i < atlas.num_layers; i++) { - views[i] = - dev.create_image_view( - gpu::ImageViewInfo{.label = "Font Atlas Image View"_str, - .image = image, - .view_type = gpu::ImageViewType::Type2D, - .view_format = gpu::Format::B8G8R8A8_UNORM, - .mapping = {}, - .aspects = gpu::ImageAspects::Color, - .first_mip_level = 0, - .num_mip_levels = 1, - .first_array_layer = i, - .num_array_layers = 1}) - .unwrap(); + views[i] = dev + .create_image_view(gpu::ImageViewInfo{ + .label = "Font Atlas Image View"_str, + .image = image, + .view_type = gpu::ImageViewType::Type2D, + .view_format = gpu::Format::B8G8R8A8_UNORM, + .mapping = {}, + .aspects = gpu::ImageAspects::Color, + .first_mip_level = 0, + .num_mip_levels = 1, + .first_array_layer = i, + .num_array_layers = 1}) + .unwrap(); } u64 const atlas_size = atlas.channels.size() * (u64) 4; gpu::Buffer staging_buffer = - dev.create_buffer( - gpu::BufferInfo{.label = "Font Atlas Staging Buffer"_str, - .size = atlas_size, - .host_mapped = true, - .usage = gpu::BufferUsage::TransferSrc | - gpu::BufferUsage::TransferDst}) - .unwrap(); + dev + .create_buffer(gpu::BufferInfo{.label = "Font Atlas Staging Buffer"_str, + .size = atlas_size, + .host_mapped = true, + .usage = gpu::BufferUsage::TransferSrc | + gpu::BufferUsage::TransferDst}) + .unwrap(); u8 * map = (u8 *) dev.map_buffer_memory(staging_buffer).unwrap(); ImageLayerSpan dst{ - .channels = {map, atlas_size}, - .width = atlas.extent.x, - .height = atlas.extent.y, - .layers = atlas.num_layers + .channels{map, atlas_size}, + .width = atlas.extent.x, + .height = atlas.extent.y, + .layers = atlas.num_layers }; for (u32 i = 0; i < atlas.num_layers; i++) @@ -393,9 +370,10 @@ struct FontImpl : Font dst.get_layer(i), U8_MAX, U8_MAX, U8_MAX); } - dev.flush_mapped_buffer_memory(staging_buffer, - {.offset = 0, .size = gpu::WHOLE_SIZE}) - .unwrap(); + dev + .flush_mapped_buffer_memory(staging_buffer, + {.offset = 0, .size = gpu::WHOLE_SIZE}) + .unwrap(); dev.unmap_buffer_memory(staging_buffer); Vec copies{allocator}; @@ -405,17 +383,18 @@ struct FontImpl : Font for (u32 layer = 0; layer < atlas.num_layers; layer++) { u64 offset = - (u64) atlas.extent.x * (u64) atlas.extent.y * 4 * (u64) layer; + (u64) atlas.extent.x * (u64) atlas.extent.y * 4 * (u64) layer; + copies[layer] = gpu::BufferImageCopy{ - .buffer_offset = offset, - .buffer_row_length = atlas.extent.x, - .buffer_image_height = atlas.extent.y, - .image_layers = {.aspects = gpu::ImageAspects::Color, - .mip_level = 0, - .first_array_layer = layer, - .num_array_layers = 1}, - .image_offset = {0, 0, 0}, - .image_extent = {atlas.extent.x, atlas.extent.y, 1} + .buffer_offset = offset, + .buffer_row_length = atlas.extent.x, + .buffer_image_height = atlas.extent.y, + .image_layers{.aspects = gpu::ImageAspects::Color, + .mip_level = 0, + .first_array_layer = layer, + .num_array_layers = 1}, + .image_offset{0, 0, 0}, + .image_extent{atlas.extent.x, atlas.extent.y, 1} }; } @@ -424,7 +403,7 @@ struct FontImpl : Font gpu::Format format = gpu::Format::B8G8R8A8_UNORM; c.release(staging_buffer); - Vec textures; + Vec textures; Vec glyphs; textures.resize(atlas.num_layers).unwrap(); @@ -432,43 +411,40 @@ struct FontImpl : Font for (u32 i = 0; i < atlas.num_layers; i++) { - textures[i] = c.alloc_texture_slot(); - dev.update_descriptor_set(gpu::DescriptorSetUpdate{ - .set = c.texture_views, - .binding = 0, - .element = textures[i], - .images = span({gpu::ImageBinding{.image_view = views[i]}})}); + textures[i] = c.alloc_texture_id(views[i]); } - gpu_atlas = Some{ - GpuFontAtlas{.image = image, - .views = std::move(views), - .textures = std::move(textures), - .font_height = atlas.font_height, - .num_layers = atlas.num_layers, - .extent = atlas.extent, - .glyphs = std::move(glyphs), - .format = format} - }; + gpu_atlas = GpuFontAtlas{.image = image, + .views = std::move(views), + .textures = std::move(textures), + .font_height = atlas.font_height, + .num_layers = atlas.num_layers, + .extent = atlas.extent, + .glyphs = std::move(glyphs), + .format = format}; } - virtual void unload_from_device(GpuContext & c) override + virtual void unload_from_device(GpuSystem & c) override { CHECK_DESC(gpu_atlas.is_some(), "Requested font to be unloaded from GPU with no GPU atlas"); - for (u32 slot : gpu_atlas.value().textures) + + GpuFontAtlas & atlas = gpu_atlas.value(); + + for (TextureId id : atlas.textures) { - c.release_texture_slot(slot); + c.release_texture_id(id); } - for (gpu::ImageView view : gpu_atlas.value().views) + for (gpu::ImageView view : atlas.views) { c.release(view); } - c.release(gpu_atlas.value().image); - gpu_atlas = None; + c.release(atlas.image); + + gpu_atlas = none; } }; -} // namespace ash +} // namespace ash diff --git a/ashura/engine/gpu_context.cc b/ashura/engine/gpu_context.cc deleted file mode 100644 index 34df7f5c3..000000000 --- a/ashura/engine/gpu_context.cc +++ /dev/null @@ -1,820 +0,0 @@ -/// SPDX-License-Identifier: MIT -#include "ashura/engine/gpu_context.h" - -namespace ash -{ - -GpuContext GpuContext::create(AllocatorImpl allocator, gpu::Device * device, - bool use_hdr, u32 buffering, - gpu::Extent initial_extent) -{ - CHECK(buffering <= gpu::MAX_FRAME_BUFFERING); - CHECK(initial_extent.x > 0 && initial_extent.y > 0); - - u32 sel_hdr_color_format = 0; - u32 sel_sdr_color_format = 0; - u32 sel_depth_stencil_format = 0; - - if (use_hdr) - { - for (; sel_hdr_color_format < size(HDR_COLOR_FORMATS); - sel_hdr_color_format++) - { - gpu::FormatProperties props = - device->get_format_properties(HDR_COLOR_FORMATS[sel_hdr_color_format]) - .unwrap(); - if (has_bits(props.optimal_tiling_features, COLOR_FEATURES)) - { - break; - } - } - - if (sel_hdr_color_format >= size(HDR_COLOR_FORMATS)) - { - logger->warn("HDR mode requested but Device does not support " - "HDR render target, trying UNORM color"); - } - } - - if (!use_hdr || sel_hdr_color_format >= size(HDR_COLOR_FORMATS)) - { - for (; sel_sdr_color_format < size(SDR_COLOR_FORMATS); - sel_sdr_color_format++) - { - gpu::FormatProperties props = - device->get_format_properties(SDR_COLOR_FORMATS[sel_sdr_color_format]) - .unwrap(); - if (has_bits(props.optimal_tiling_features, COLOR_FEATURES)) - { - break; - } - } - } - - for (; sel_depth_stencil_format < size(DEPTH_STENCIL_FORMATS); - sel_depth_stencil_format++) - { - gpu::FormatProperties props = - device - ->get_format_properties( - DEPTH_STENCIL_FORMATS[sel_depth_stencil_format]) - .unwrap(); - if (has_bits(props.optimal_tiling_features, DEPTH_STENCIL_FEATURES)) - { - break; - } - } - - gpu::Format color_format = gpu::Format::Undefined; - - if (use_hdr) - { - CHECK_DESC(sel_sdr_color_format != size(SDR_COLOR_FORMATS) || - sel_hdr_color_format != size(HDR_COLOR_FORMATS), - "Device doesn't support any known color format"); - if (sel_hdr_color_format != size(HDR_COLOR_FORMATS)) - { - color_format = HDR_COLOR_FORMATS[sel_hdr_color_format]; - } - else - { - color_format = SDR_COLOR_FORMATS[sel_sdr_color_format]; - } - } - else - { - CHECK_DESC(sel_sdr_color_format != size(SDR_COLOR_FORMATS), - "Device doesn't support any known color format"); - color_format = SDR_COLOR_FORMATS[sel_sdr_color_format]; - } - - CHECK_DESC(sel_depth_stencil_format != size(DEPTH_STENCIL_FORMATS), - "Device doesn't support any known depth stencil format"); - gpu::Format depth_stencil_format = - DEPTH_STENCIL_FORMATS[sel_depth_stencil_format]; - - logger->trace("Selected color format: ", color_format); - - logger->trace("Selected depth stencil format: ", depth_stencil_format); - - gpu::PipelineCache pipeline_cache = nullptr; - - gpu::DescriptorSetLayout ubo_layout = - device - ->create_descriptor_set_layout( - - gpu::DescriptorSetLayoutInfo{ - .label = "UBO Layout"_str, - .bindings = span({gpu::DescriptorBindingInfo{ - .type = gpu::DescriptorType::DynamicUniformBuffer, - .count = 1, - .is_variable_length = false}} - ) - }) - .unwrap(); - - gpu::DescriptorSetLayout ssbo_layout = - device - ->create_descriptor_set_layout( - - gpu::DescriptorSetLayoutInfo{ - .label = "SSBO Layout"_str, - .bindings = span({gpu::DescriptorBindingInfo{ - .type = gpu::DescriptorType::DynamicStorageBuffer, - .count = 1, - .is_variable_length = false}} - ) - }) - .unwrap(); - - gpu::DescriptorSetLayout textures_layout = - device - ->create_descriptor_set_layout( - - gpu::DescriptorSetLayoutInfo{ - .label = "Textures Layout"_str, - .bindings = span({gpu::DescriptorBindingInfo{ - .type = gpu::DescriptorType::SampledImage, - .count = NUM_TEXTURE_SLOTS, - .is_variable_length = true}} - ) - }) - .unwrap(); - - gpu::DescriptorSetLayout samplers_layout = - device - ->create_descriptor_set_layout( - - gpu::DescriptorSetLayoutInfo{ - .label = "Samplers Layout"_str, - .bindings = span({gpu::DescriptorBindingInfo{ - .type = gpu::DescriptorType::Sampler, - .count = NUM_SAMPLER_SLOTS, - .is_variable_length = true}} - ) - }) - .unwrap(); - - gpu::DescriptorSet texture_views = - device - ->create_descriptor_set(textures_layout, - span({NUM_TEXTURE_SLOTS})) - .unwrap(); - - gpu::DescriptorSet samplers = - device - ->create_descriptor_set(samplers_layout, - span({NUM_SAMPLER_SLOTS})) - .unwrap(); - - gpu::Image default_image = - device - ->create_image(gpu::ImageInfo{ - .label = "Default Texture Image"_str, - .type = gpu::ImageType::Type2D, - .format = gpu::Format::B8G8R8A8_UNORM, - .usage = gpu::ImageUsage::Sampled | gpu::ImageUsage::TransferDst | - gpu::ImageUsage::Storage | gpu::ImageUsage::Storage, - .aspects = gpu::ImageAspects::Color, - .extent = {1, 1, 1}, - .mip_levels = 1, - .array_layers = 1, - .sample_count = gpu::SampleCount::Count1 - }) - .unwrap(); - - Array default_image_views; - - { - gpu::ComponentMapping mappings[NUM_DEFAULT_TEXTURES] = {}; - mappings[TEXTURE_WHITE] = {.r = gpu::ComponentSwizzle::One, - .g = gpu::ComponentSwizzle::One, - .b = gpu::ComponentSwizzle::One, - .a = gpu::ComponentSwizzle::One}; - mappings[TEXTURE_BLACK] = {.r = gpu::ComponentSwizzle::Zero, - .g = gpu::ComponentSwizzle::Zero, - .b = gpu::ComponentSwizzle::Zero, - .a = gpu::ComponentSwizzle::One}; - mappings[TEXTURE_TRANSPARENT] = {.r = gpu::ComponentSwizzle::Zero, - .g = gpu::ComponentSwizzle::Zero, - .b = gpu::ComponentSwizzle::Zero, - .a = gpu::ComponentSwizzle::Zero}; - mappings[TEXTURE_RED] = {.r = gpu::ComponentSwizzle::One, - .g = gpu::ComponentSwizzle::Zero, - .b = gpu::ComponentSwizzle::Zero, - .a = gpu::ComponentSwizzle::One}; - mappings[TEXTURE_GREEN] = {.r = gpu::ComponentSwizzle::Zero, - .g = gpu::ComponentSwizzle::One, - .b = gpu::ComponentSwizzle::Zero, - .a = gpu::ComponentSwizzle::One}; - mappings[TEXTURE_BLUE] = {.r = gpu::ComponentSwizzle::Zero, - .g = gpu::ComponentSwizzle::Zero, - .b = gpu::ComponentSwizzle::One, - .a = gpu::ComponentSwizzle::One}; - - for (u32 i = 0; i < NUM_DEFAULT_TEXTURES; i++) - { - default_image_views[i] = - device - ->create_image_view( - - gpu::ImageViewInfo{.label = "Default Texture Image View"_str, - .image = default_image, - .view_type = gpu::ImageViewType::Type2D, - .view_format = gpu::Format::B8G8R8A8_UNORM, - .mapping = mappings[i], - .aspects = gpu::ImageAspects::Color, - .first_mip_level = 0, - .num_mip_levels = 1, - .first_array_layer = 0, - .num_array_layers = 1}) - .unwrap(); - - device->update_descriptor_set(gpu::DescriptorSetUpdate{ - .set = texture_views, - .binding = 0, - .element = i, - .images = - span({gpu::ImageBinding{.image_view = default_image_views[i]}})}); - } - } - - InplaceVec, gpu::MAX_FRAME_BUFFERING> released_objects; - - for (u32 i = 0; i < buffering; i++) - { - released_objects.push(Vec{allocator}).unwrap(); - } - - GpuContext ctx{allocator, - device, - pipeline_cache, - buffering, - color_format, - depth_stencil_format, - ubo_layout, - ssbo_layout, - textures_layout, - samplers_layout, - texture_views, - samplers, - default_image, - default_image_views, - std::move(released_objects)}; - - for (u32 i = 0; i < NUM_DEFAULT_TEXTURES; i++) - { - u32 slot = ctx.alloc_texture_slot(); - CHECK(slot == i); - } - - CachedSampler sampler = ctx.create_sampler( - gpu::SamplerInfo{.label = "Linear+Repeat Sampler"_str, - .mag_filter = gpu::Filter::Linear, - .min_filter = gpu::Filter::Linear, - .mip_map_mode = gpu::SamplerMipMapMode::Linear, - .address_mode_u = gpu::SamplerAddressMode::Repeat, - .address_mode_v = gpu::SamplerAddressMode::Repeat, - .address_mode_w = gpu::SamplerAddressMode::Repeat, - .mip_lod_bias = 0, - .anisotropy_enable = false, - .max_anisotropy = 1.0, - .compare_enable = false, - .compare_op = gpu::CompareOp::Never, - .min_lod = 0, - .max_lod = 0, - .border_color = gpu::BorderColor::FloatTransparentBlack, - .unnormalized_coordinates = false}); - - CHECK(sampler.slot == SAMPLER_LINEAR); - - sampler = ctx.create_sampler( - gpu::SamplerInfo{.label = "Nearest+Repeat Sampler"_str, - .mag_filter = gpu::Filter::Nearest, - .min_filter = gpu::Filter::Nearest, - .mip_map_mode = gpu::SamplerMipMapMode::Nearest, - .address_mode_u = gpu::SamplerAddressMode::Repeat, - .address_mode_v = gpu::SamplerAddressMode::Repeat, - .address_mode_w = gpu::SamplerAddressMode::Repeat, - .mip_lod_bias = 0, - .anisotropy_enable = false, - .max_anisotropy = 1.0, - .compare_enable = false, - .compare_op = gpu::CompareOp::Never, - .min_lod = 0, - .max_lod = 0, - .border_color = gpu::BorderColor::FloatTransparentBlack, - .unnormalized_coordinates = false}); - - CHECK(sampler.slot == SAMPLER_NEAREST); - - sampler = ctx.create_sampler( - gpu::SamplerInfo{.label = "Linear+EdgeClamped Sampler"_str, - .mag_filter = gpu::Filter::Linear, - .min_filter = gpu::Filter::Linear, - .mip_map_mode = gpu::SamplerMipMapMode::Linear, - .address_mode_u = gpu::SamplerAddressMode::ClampToEdge, - .address_mode_v = gpu::SamplerAddressMode::ClampToEdge, - .address_mode_w = gpu::SamplerAddressMode::ClampToEdge, - .mip_lod_bias = 0, - .anisotropy_enable = false, - .max_anisotropy = 1.0, - .compare_enable = false, - .compare_op = gpu::CompareOp::Never, - .min_lod = 0, - .max_lod = 0, - .border_color = gpu::BorderColor::FloatTransparentBlack, - .unnormalized_coordinates = false}); - - CHECK(sampler.slot == SAMPLER_LINEAR_CLAMPED); - - sampler = ctx.create_sampler( - gpu::SamplerInfo{.label = "Nearest+EdgeClamped Sampler"_str, - .mag_filter = gpu::Filter::Nearest, - .min_filter = gpu::Filter::Nearest, - .mip_map_mode = gpu::SamplerMipMapMode::Nearest, - .address_mode_u = gpu::SamplerAddressMode::ClampToEdge, - .address_mode_v = gpu::SamplerAddressMode::ClampToEdge, - .address_mode_w = gpu::SamplerAddressMode::ClampToEdge, - .mip_lod_bias = 0, - .anisotropy_enable = false, - .max_anisotropy = 1.0, - .compare_enable = false, - .compare_op = gpu::CompareOp::Never, - .min_lod = 0, - .max_lod = 0, - .border_color = gpu::BorderColor::FloatTransparentBlack, - .unnormalized_coordinates = false}); - - CHECK(sampler.slot == SAMPLER_NEAREST_CLAMPED); - - ctx.recreate_framebuffers(initial_extent); - - return ctx; -} - -static void recreate_framebuffer(GpuContext & ctx, Framebuffer & fb, - gpu::Extent new_extent) -{ - ctx.release(fb); - - fb.color.info = gpu::ImageInfo{ - .label = "Framebuffer Color Image"_str, - .type = gpu::ImageType::Type2D, - .format = ctx.color_format, - .usage = gpu::ImageUsage::ColorAttachment | gpu::ImageUsage::Sampled | - gpu::ImageUsage::Storage | gpu::ImageUsage::TransferDst | - gpu::ImageUsage::TransferSrc, - .aspects = gpu::ImageAspects::Color, - .extent = gpu::Extent3D{new_extent.x, new_extent.y, 1}, - .mip_levels = 1, - .array_layers = 1, - .sample_count = gpu::SampleCount::Count1 - }; - - fb.color.image = ctx.device->create_image(fb.color.info).unwrap(); - - fb.color.view_info = - gpu::ImageViewInfo{.label = "Framebuffer Color Image View"_str, - .image = fb.color.image, - .view_type = gpu::ImageViewType::Type2D, - .view_format = fb.color.info.format, - .mapping = {}, - .aspects = gpu::ImageAspects::Color, - .first_mip_level = 0, - .num_mip_levels = 1, - .first_array_layer = 0, - .num_array_layers = 1}; - fb.color.view = ctx.device->create_image_view(fb.color.view_info).unwrap(); - - fb.depth_stencil.info = gpu::ImageInfo{ - .label = "Framebuffer Depth Stencil Image"_str, - .type = gpu::ImageType::Type2D, - .format = ctx.depth_stencil_format, - .usage = gpu::ImageUsage::DepthStencilAttachment | - gpu::ImageUsage::Sampled | gpu::ImageUsage::TransferDst | - gpu::ImageUsage::TransferSrc, - .aspects = gpu::ImageAspects::Depth | gpu::ImageAspects::Stencil, - .extent = gpu::Extent3D{new_extent.x, new_extent.y, 1}, - .mip_levels = 1, - .array_layers = 1, - .sample_count = gpu::SampleCount::Count1 - }; - - fb.depth_stencil.image = - ctx.device->create_image(fb.depth_stencil.info).unwrap(); - - fb.depth_stencil.view_info = gpu::ImageViewInfo{ - .label = "Framebuffer Depth Stencil Image View"_str, - .image = fb.depth_stencil.image, - .view_type = gpu::ImageViewType::Type2D, - .view_format = fb.depth_stencil.info.format, - .mapping = {}, - .aspects = gpu::ImageAspects::Depth | gpu::ImageAspects::Stencil, - .first_mip_level = 0, - .num_mip_levels = 1, - .first_array_layer = 0, - .num_array_layers = 1}; - - fb.depth_stencil.view = - ctx.device->create_image_view(fb.depth_stencil.view_info).unwrap(); - - fb.color_texture = - ctx.device->create_descriptor_set(ctx.textures_layout, span({1})) - .unwrap(); - - ctx.device->update_descriptor_set( - - gpu::DescriptorSetUpdate{ - .set = fb.color_texture, - .binding = 0, - .element = 0, - .images = span({gpu::ImageBinding{.image_view = fb.color.view}})}); - - fb.extent = new_extent; -} - -void GpuContext::recreate_framebuffers(gpu::Extent new_extent) -{ - recreate_framebuffer(*this, screen_fb, new_extent); - for (Framebuffer & f : scratch_fbs) - { - recreate_framebuffer(*this, f, new_extent); - } -} - -gpu::CommandEncoder & GpuContext::encoder() -{ - gpu::FrameContext ctx = device->get_frame_context(); - return *ctx.encoders[ctx.ring_index]; -} - -u32 GpuContext::ring_index() -{ - gpu::FrameContext ctx = device->get_frame_context(); - return ctx.ring_index; -} - -gpu::FrameId GpuContext::frame_id() -{ - gpu::FrameContext ctx = device->get_frame_context(); - return ctx.current; -} - -gpu::FrameId GpuContext::tail_frame_id() -{ - gpu::FrameContext ctx = device->get_frame_context(); - return ctx.tail; -} - -CachedSampler GpuContext::create_sampler(gpu::SamplerInfo const & info) -{ - CachedSampler * cached = sampler_cache.try_get(info); - if (cached != nullptr) - { - return *cached; - } - - CachedSampler sampler{.sampler = device->create_sampler(info).unwrap(), - .slot = alloc_sampler_slot()}; - - device->update_descriptor_set( - - gpu::DescriptorSetUpdate{ - .set = samplers, - .binding = 0, - .element = sampler.slot, - .images = span({gpu::ImageBinding{.sampler = sampler.sampler}})}); - - sampler_cache.insert(info, sampler).unwrap(); - - return sampler; -} - -u32 GpuContext::alloc_texture_slot() -{ - usize i = find_clear_bit(texture_slots); - CHECK_DESC(i < size_bits(texture_slots), "Out of Texture Slots"); - set_bit(texture_slots, i); - return (u32) i; -} - -void GpuContext::release_texture_slot(u32 slot) -{ - clear_bit(texture_slots, slot); -} - -u32 GpuContext::alloc_sampler_slot() -{ - usize i = find_clear_bit(sampler_slots); - CHECK_DESC(i < size_bits(sampler_slots), "Out of Sampler Slots"); - set_bit(sampler_slots, i); - return (u32) i; -} - -void GpuContext::release_sampler_slot(u32 slot) -{ - clear_bit(sampler_slots, slot); -} - -void GpuContext::release(gpu::Image image) -{ - if (image == nullptr) - { - return; - } - released_objects[ring_index()] - .push(gpu::Object{.image = image, .type = gpu::ObjectType::Image}) - .unwrap(); -} - -void GpuContext::release(gpu::ImageView view) -{ - if (view == nullptr) - { - return; - } - released_objects[ring_index()] - .push(gpu::Object{.image_view = view, .type = gpu::ObjectType::ImageView}) - .unwrap(); -} - -void GpuContext::release(gpu::Buffer buffer) -{ - if (buffer == nullptr) - { - return; - } - released_objects[ring_index()] - .push(gpu::Object{.buffer = buffer, .type = gpu::ObjectType::Buffer}) - .unwrap(); -} - -void GpuContext::release(gpu::BufferView view) -{ - if (view == nullptr) - { - return; - } - released_objects[ring_index()] - .push( - gpu::Object{.buffer_view = view, .type = gpu::ObjectType::BufferView}) - .unwrap(); -} - -void GpuContext::release(gpu::DescriptorSetLayout layout) -{ - if (layout == nullptr) - { - return; - } - released_objects[ring_index()] - .push(gpu::Object{.descriptor_set_layout = layout, - .type = gpu::ObjectType::DescriptorSetLayout}) - .unwrap(); -} - -void GpuContext::release(gpu::DescriptorSet set) -{ - if (set == nullptr) - { - return; - } - released_objects[ring_index()] - .push(gpu::Object{.descriptor_set = set, - .type = gpu::ObjectType::DescriptorSet}) - .unwrap(); -} - -void GpuContext::release(gpu::Sampler sampler) -{ - if (sampler == nullptr) - { - return; - } - released_objects[ring_index()] - .push(gpu::Object{.sampler = sampler, .type = gpu::ObjectType::Sampler}) - .unwrap(); -} - -static void uninit_objects(gpu::Device * d, Span objects) -{ - for (gpu::Object obj : objects) - { - switch (obj.type) - { - case gpu::ObjectType::Image: - d->uninit_image(obj.image); - break; - case gpu::ObjectType::ImageView: - d->uninit_image_view(obj.image_view); - break; - case gpu::ObjectType::Buffer: - d->uninit_buffer(obj.buffer); - break; - case gpu::ObjectType::BufferView: - d->uninit_buffer_view(obj.buffer_view); - break; - case gpu::ObjectType::Sampler: - d->uninit_sampler(obj.sampler); - break; - case gpu::ObjectType::DescriptorSet: - d->uninit_descriptor_set(obj.descriptor_set); - break; - case gpu::ObjectType::DescriptorSetLayout: - d->uninit_descriptor_set_layout(obj.descriptor_set_layout); - break; - default: - CHECK_UNREACHABLE(); - } - } -} - -void GpuContext::idle_reclaim() -{ - device->wait_idle().unwrap(); - for (auto & objects : released_objects) - { - uninit_objects(device, objects); - objects.clear(); - } -} - -void GpuContext::begin_frame(gpu::Swapchain swapchain) -{ - device->begin_frame(swapchain).unwrap(); - uninit_objects(device, released_objects[ring_index()]); - released_objects[ring_index()].clear(); - - gpu::CommandEncoder & enc = encoder(); - - enc.clear_color_image( - screen_fb.color.image, - gpu::Color{ - .float32 = {0, 0, 0, 0} - }, - span({gpu::ImageSubresourceRange{.aspects = gpu::ImageAspects::Color, - .first_mip_level = 0, - .num_mip_levels = 1, - .first_array_layer = 0, - .num_array_layers = 1}})); - - for (Framebuffer const & f : scratch_fbs) - { - enc.clear_color_image( - f.color.image, - gpu::Color{ - .float32 = {0, 0, 0, 0} - }, - span({gpu::ImageSubresourceRange{.aspects = gpu::ImageAspects::Color, - .first_mip_level = 0, - .num_mip_levels = 1, - .first_array_layer = 0, - .num_array_layers = 1}})); - } - - enc.clear_depth_stencil_image( - screen_fb.depth_stencil.image, - gpu::DepthStencil{ - .depth = 0, .stencil = 0 - }, - span({gpu::ImageSubresourceRange{.aspects = gpu::ImageAspects::Depth | - gpu::ImageAspects::Stencil, - .first_mip_level = 0, - .num_mip_levels = 1, - .first_array_layer = 0, - .num_array_layers = 1}})); - - for (Framebuffer const & f : scratch_fbs) - { - enc.clear_depth_stencil_image( - f.depth_stencil.image, - gpu::DepthStencil{ - .depth = 0, .stencil = 0 - }, - span({gpu::ImageSubresourceRange{.aspects = gpu::ImageAspects::Depth | - gpu::ImageAspects::Stencil, - .first_mip_level = 0, - .num_mip_levels = 1, - .first_array_layer = 0, - .num_array_layers = 1}})); - } -} - -void GpuContext::submit_frame(gpu::Swapchain swapchain) -{ - gpu::CommandEncoder & enc = encoder(); - if (swapchain != nullptr) - { - gpu::SwapchainState swapchain_state = - device->get_swapchain_state(swapchain).unwrap(); - - if (swapchain_state.current_image.is_some()) - { - enc.blit_image( - screen_fb.color.image, - swapchain_state.images[swapchain_state.current_image.unwrap()], - span({ - gpu::ImageBlit{ - .src_layers = {.aspects = gpu::ImageAspects::Color, - .mip_level = 0, - .first_array_layer = 0, - .num_array_layers = 1}, - .src_offsets = {{0, 0, 0}, - {screen_fb.extent.x, screen_fb.extent.y, 1}}, - .dst_layers = {.aspects = gpu::ImageAspects::Color, - .mip_level = 0, - .first_array_layer = 0, - .num_array_layers = 1}, - .dst_offsets = {{0, 0, 0}, - {swapchain_state.extent.x, - swapchain_state.extent.y, 1}}} - }), - gpu::Filter::Linear); - } - } - device->submit_frame(swapchain).unwrap(); -} - -void SSBO::uninit(GpuContext & ctx) -{ - ctx.device->uninit_descriptor_set(descriptor); - ctx.device->uninit_buffer(buffer); -} - -void SSBO::reserve(GpuContext & ctx, u64 p_size) -{ - p_size = max(p_size, (u64) 1); - if (buffer != nullptr && size >= p_size) - { - return; - } - - ctx.device->uninit_buffer(buffer); - - buffer = ctx.device - ->create_buffer( - - gpu::BufferInfo{.label = label, - .size = p_size, - .host_mapped = true, - .usage = gpu::BufferUsage::TransferSrc | - gpu::BufferUsage::TransferDst | - gpu::BufferUsage::UniformBuffer | - gpu::BufferUsage::StorageBuffer}) - .unwrap(); - - if (descriptor == nullptr) - { - descriptor = - ctx.device->create_descriptor_set(ctx.ssbo_layout, {}).unwrap(); - } - - ctx.device->update_descriptor_set(gpu::DescriptorSetUpdate{ - .set = descriptor, - .binding = 0, - .element = 0, - .buffers = span({gpu::BufferBinding{ - .buffer = buffer, .offset = 0, .size = p_size}} - ) - }); - - size = p_size; -} - -void SSBO::copy(GpuContext & ctx, Span src) -{ - reserve(ctx, (u64) src.size()); - u8 * data = (u8 *) map(ctx); - mem::copy(src, data); - flush(ctx); - unmap(ctx); -} - -void * SSBO::map(GpuContext & ctx) -{ - return ctx.device->map_buffer_memory(buffer).unwrap(); -} - -void SSBO::unmap(GpuContext & ctx) -{ - ctx.device->unmap_buffer_memory(buffer); -} - -void SSBO::flush(GpuContext & ctx) -{ - ctx.device - ->flush_mapped_buffer_memory(buffer, gpu::MemoryRange{0, gpu::WHOLE_SIZE}) - .unwrap(); -} - -void SSBO::release(GpuContext & ctx) -{ - ctx.release(buffer); - ctx.release(descriptor); - buffer = nullptr; - size = 0; - descriptor = nullptr; -} - -} // namespace ash diff --git a/ashura/engine/gpu_context.h b/ashura/engine/gpu_context.h deleted file mode 100644 index e14c254a3..000000000 --- a/ashura/engine/gpu_context.h +++ /dev/null @@ -1,312 +0,0 @@ -/// SPDX-License-Identifier: MIT -#pragma once -#include "ashura/gpu/gpu.h" -#include "ashura/std/error.h" -#include "ashura/std/map.h" -#include "ashura/std/option.h" -#include "ashura/std/result.h" -#include "ashura/std/types.h" -#include "ashura/std/vec.h" - -namespace ash -{ - -inline constexpr u32 TEXTURE_WHITE = 0; -inline constexpr u32 TEXTURE_BLACK = 1; -inline constexpr u32 TEXTURE_TRANSPARENT = 2; -inline constexpr u32 TEXTURE_RED = 3; -inline constexpr u32 TEXTURE_GREEN = 4; -inline constexpr u32 TEXTURE_BLUE = 5; -inline constexpr u32 NUM_DEFAULT_TEXTURES = TEXTURE_BLUE + 1; - -inline constexpr u32 SAMPLER_LINEAR = 0; -inline constexpr u32 SAMPLER_NEAREST = 1; -inline constexpr u32 SAMPLER_LINEAR_CLAMPED = 2; -inline constexpr u32 SAMPLER_NEAREST_CLAMPED = 3; -inline constexpr u32 NUM_DEFAULT_SAMPLERS = SAMPLER_NEAREST_CLAMPED + 1; - -struct FramebufferAttachment -{ - gpu::ImageInfo info = {}; - gpu::ImageViewInfo view_info = {}; - gpu::Image image = nullptr; - gpu::ImageView view = nullptr; -}; - -/// created with sampled, storage, color attachment, and transfer flags -struct Framebuffer -{ - FramebufferAttachment color = {}; - FramebufferAttachment depth_stencil = {}; - gpu::DescriptorSet color_texture = nullptr; - gpu::Extent extent = {}; -}; - -struct SamplerHasher -{ - constexpr hash64 operator()(gpu::SamplerInfo const & info) const - { - return hash_combine_n( - (hash64) info.mag_filter, (hash64) info.min_filter, - (hash64) info.mip_map_mode, (hash64) info.address_mode_u, - (hash64) info.address_mode_v, (hash64) info.address_mode_w, - (hash64) bit_cast(info.mip_lod_bias), - (hash64) info.anisotropy_enable, - (hash64) bit_cast(info.max_anisotropy), - (hash64) info.compare_enable, (hash64) info.compare_op, - (hash64) bit_cast(info.min_lod), - (hash64) bit_cast(info.max_lod), (hash64) info.border_color, - (hash64) info.unnormalized_coordinates); - } -}; - -struct SamplerEq -{ - constexpr hash64 operator()(gpu::SamplerInfo const & a, - gpu::SamplerInfo const & b) const - { - return a.mag_filter == b.mag_filter && a.mip_map_mode == b.mip_map_mode && - a.address_mode_u == b.address_mode_u && - a.address_mode_v == b.address_mode_v && - a.address_mode_w == b.address_mode_w && - a.mip_lod_bias == b.mip_lod_bias && - a.anisotropy_enable == b.anisotropy_enable && - a.max_anisotropy == b.max_anisotropy && - a.compare_enable == b.compare_enable && - a.compare_op == b.compare_op && a.min_lod == b.min_lod && - a.max_lod == b.max_lod && a.border_color == b.border_color && - a.unnormalized_coordinates == b.unnormalized_coordinates; - } -}; - -struct CachedSampler -{ - gpu::Sampler sampler = nullptr; - u32 slot = 0; -}; - -typedef Map - SamplerCache; - -/// @param color_format hdr if hdr supported and required. -/// -/// scratch images resized when swapchain extents changes -/// -struct GpuContext -{ - static constexpr gpu::FormatFeatures COLOR_FEATURES = - gpu::FormatFeatures::ColorAttachment | - gpu::FormatFeatures::ColorAttachmentBlend | - gpu::FormatFeatures::StorageImage | gpu::FormatFeatures::SampledImage; - - static constexpr gpu::FormatFeatures DEPTH_STENCIL_FEATURES = - gpu::FormatFeatures::DepthStencilAttachment | - gpu::FormatFeatures::SampledImage; - - static constexpr gpu::BufferUsage SSBO_USAGE = - gpu::BufferUsage::UniformBuffer | gpu::BufferUsage::StorageBuffer | - gpu::BufferUsage::UniformTexelBuffer | - gpu::BufferUsage::StorageTexelBuffer | gpu::BufferUsage::IndirectBuffer | - gpu::BufferUsage::TransferSrc | gpu::BufferUsage::TransferDst; - - static constexpr gpu::Format HDR_COLOR_FORMATS[] = { - gpu::Format::R16G16B16A16_SFLOAT}; - - static constexpr gpu::Format SDR_COLOR_FORMATS[] = { - gpu::Format::B8G8R8A8_UNORM, gpu::Format::R8G8B8A8_UNORM}; - - static constexpr gpu::Format DEPTH_STENCIL_FORMATS[] = { - gpu::Format::D16_UNORM_S8_UINT, gpu::Format::D24_UNORM_S8_UINT, - gpu::Format::D32_SFLOAT_S8_UINT}; - - static constexpr u16 NUM_TEXTURE_SLOTS = 1'024; - static constexpr u16 NUM_SAMPLER_SLOTS = 64; - static constexpr u16 NUM_SCRATCH_FRAMEBUFFERS = 2; - - gpu::Device * device; - - gpu::PipelineCache pipeline_cache; - - u32 buffering; - - gpu::Format color_format; - - gpu::Format depth_stencil_format; - - gpu::DescriptorSetLayout ubo_layout; - - gpu::DescriptorSetLayout ssbo_layout; - - gpu::DescriptorSetLayout textures_layout; - - gpu::DescriptorSetLayout samplers_layout; - - gpu::DescriptorSet texture_views; - - gpu::DescriptorSet samplers; - - SamplerCache sampler_cache; - - Framebuffer screen_fb; - - Array scratch_fbs; - - gpu::Image default_image; - - Array default_image_views; - - InplaceVec, gpu::MAX_FRAME_BUFFERING> released_objects; - - Bits texture_slots; - - Bits sampler_slots; - - static GpuContext create(AllocatorImpl allocator, gpu::Device * device, - bool use_hdr, u32 buffering, - gpu::Extent initial_extent); - - GpuContext( - AllocatorImpl allocator, gpu::Device * device, - gpu::PipelineCache pipeline_cache, u32 buffering, - gpu::Format color_format, gpu::Format depth_stencil_format, - gpu::DescriptorSetLayout ubo_layout, gpu::DescriptorSetLayout ssbo_layout, - gpu::DescriptorSetLayout textures_layout, - gpu::DescriptorSetLayout samplers_layout, - gpu::DescriptorSet texture_views, gpu::DescriptorSet samplers, - gpu::Image default_image, - Array default_image_views, - InplaceVec, gpu::MAX_FRAME_BUFFERING> released_objects) : - device{device}, - pipeline_cache{pipeline_cache}, - buffering{buffering}, - color_format{color_format}, - depth_stencil_format{depth_stencil_format}, - ubo_layout{ubo_layout}, - ssbo_layout{ssbo_layout}, - textures_layout{textures_layout}, - samplers_layout{samplers_layout}, - texture_views{texture_views}, - samplers{samplers}, - sampler_cache{allocator}, - screen_fb{}, - scratch_fbs{}, - default_image{default_image}, - default_image_views{default_image_views}, - released_objects{std::move(released_objects)}, - texture_slots{}, - sampler_slots{} - { - } - - GpuContext(GpuContext const &) = delete; - GpuContext(GpuContext &&) = default; - GpuContext & operator=(GpuContext const &) = delete; - GpuContext & operator=(GpuContext &&) = default; - ~GpuContext() = default; - - void uninit() - { - release(default_image); - for (gpu::ImageView v : default_image_views) - { - release(v); - } - release(texture_views); - release(samplers); - release(ubo_layout); - release(ssbo_layout); - release(textures_layout); - release(samplers_layout); - release(screen_fb); - for (Framebuffer & f : scratch_fbs) - { - release(f); - } - for (auto const & [_, sampler] : sampler_cache) - { - release(sampler.sampler); - } - idle_reclaim(); - device->uninit_pipeline_cache(pipeline_cache); - } - - void recreate_framebuffers(gpu::Extent new_extent); - - gpu::CommandEncoder & encoder(); - - u32 ring_index(); - - gpu::FrameId frame_id(); - - gpu::FrameId tail_frame_id(); - - CachedSampler create_sampler(gpu::SamplerInfo const & info); - - u32 alloc_texture_slot(); - - void release_texture_slot(u32 slot); - - u32 alloc_sampler_slot(); - - void release_sampler_slot(u32 slot); - - void release(gpu::Image image); - - void release(gpu::ImageView view); - - void release(gpu::Buffer view); - - void release(gpu::BufferView view); - - void release(gpu::DescriptorSetLayout layout); - - void release(gpu::DescriptorSet set); - - void release(gpu::Sampler sampler); - - void release(FramebufferAttachment fb) - { - release(fb.image); - release(fb.view); - } - - void release(Framebuffer fb) - { - release(fb.color); - release(fb.depth_stencil); - release(fb.color_texture); - } - - void idle_reclaim(); - - void begin_frame(gpu::Swapchain swapchain); - - void submit_frame(gpu::Swapchain swapchain); -}; - -struct SSBO -{ - gpu::Buffer buffer = nullptr; - - u64 size = 0; - - gpu::DescriptorSet descriptor = nullptr; - - Span label = "SSBO"_str; - - void uninit(GpuContext & ctx); - - void reserve(GpuContext & ctx, u64 size); - - void copy(GpuContext & ctx, Span src); - - void * map(GpuContext & ctx); - - void unmap(GpuContext & ctx); - - void flush(GpuContext & ctx); - - void release(GpuContext & ctx); -}; - -} // namespace ash diff --git a/ashura/engine/gpu_system.cc b/ashura/engine/gpu_system.cc new file mode 100644 index 000000000..9defdc069 --- /dev/null +++ b/ashura/engine/gpu_system.cc @@ -0,0 +1,845 @@ +/// SPDX-License-Identifier: MIT +#include "ashura/engine/gpu_system.h" + +namespace ash +{ + +GpuSystem GpuSystem::create(AllocatorImpl allocator, gpu::Device * device, + bool use_hdr, u32 buffering, + gpu::SampleCount sample_count, + gpu::Extent initial_extent) +{ + CHECK(buffering <= gpu::MAX_FRAME_BUFFERING); + CHECK(initial_extent.x > 0 && initial_extent.y > 0); + + u32 sel_hdr_color_format = 0; + u32 sel_sdr_color_format = 0; + u32 sel_depth_stencil_format = 0; + + if (use_hdr) + { + for (; sel_hdr_color_format < size(HDR_COLOR_FORMATS); + sel_hdr_color_format++) + { + gpu::FormatProperties props = + device->get_format_properties(HDR_COLOR_FORMATS[sel_hdr_color_format]) + .unwrap(); + if (has_bits(props.optimal_tiling_features, COLOR_FEATURES)) + { + break; + } + } + + if (sel_hdr_color_format >= size(HDR_COLOR_FORMATS)) + { + logger->warn("HDR mode requested but Device does not support " + "HDR render target, trying UNORM color"); + } + } + + if (!use_hdr || sel_hdr_color_format >= size(HDR_COLOR_FORMATS)) + { + for (; sel_sdr_color_format < size(SDR_COLOR_FORMATS); + sel_sdr_color_format++) + { + gpu::FormatProperties props = + device->get_format_properties(SDR_COLOR_FORMATS[sel_sdr_color_format]) + .unwrap(); + if (has_bits(props.optimal_tiling_features, COLOR_FEATURES)) + { + break; + } + } + } + + for (; sel_depth_stencil_format < size(DEPTH_STENCIL_FORMATS); + sel_depth_stencil_format++) + { + gpu::FormatProperties props = + device + ->get_format_properties(DEPTH_STENCIL_FORMATS[sel_depth_stencil_format]) + .unwrap(); + if (has_bits(props.optimal_tiling_features, DEPTH_STENCIL_FEATURES)) + { + break; + } + } + + gpu::Format color_format = gpu::Format::Undefined; + + if (use_hdr) + { + CHECK_DESC(sel_sdr_color_format != size(SDR_COLOR_FORMATS) || + sel_hdr_color_format != size(HDR_COLOR_FORMATS), + "Device doesn't support any known color format"); + if (sel_hdr_color_format != size(HDR_COLOR_FORMATS)) + { + color_format = HDR_COLOR_FORMATS[sel_hdr_color_format]; + } + else + { + color_format = SDR_COLOR_FORMATS[sel_sdr_color_format]; + } + } + else + { + CHECK_DESC(sel_sdr_color_format != size(SDR_COLOR_FORMATS), + "Device doesn't support any known color format"); + color_format = SDR_COLOR_FORMATS[sel_sdr_color_format]; + } + + CHECK_DESC(sel_depth_stencil_format != size(DEPTH_STENCIL_FORMATS), + "Device doesn't support any known depth stencil format"); + gpu::Format depth_stencil_format = + DEPTH_STENCIL_FORMATS[sel_depth_stencil_format]; + + logger->trace("Selected color format: ", color_format); + + logger->trace("Selected depth stencil format: ", depth_stencil_format); + + gpu::PipelineCache pipeline_cache = nullptr; + + gpu::DescriptorSetLayout ubo_layout = + device + ->create_descriptor_set_layout(gpu::DescriptorSetLayoutInfo{ + .label = "UBO Layout"_str, + .bindings = span({gpu::DescriptorBindingInfo{ + .type = gpu::DescriptorType::DynamicUniformBuffer, + .count = 1, + .is_variable_length = false}} + ) + }) + .unwrap(); + + gpu::DescriptorSetLayout ssbo_layout = + device + ->create_descriptor_set_layout(gpu::DescriptorSetLayoutInfo{ + .label = "SSBO Layout"_str, + .bindings = span({gpu::DescriptorBindingInfo{ + .type = gpu::DescriptorType::DynamicStorageBuffer, + .count = 1, + .is_variable_length = false}} + ) + }) + .unwrap(); + + gpu::DescriptorSetLayout textures_layout = + device + ->create_descriptor_set_layout(gpu::DescriptorSetLayoutInfo{ + .label = "Textures Layout"_str, + .bindings = span( + {gpu::DescriptorBindingInfo{.type = gpu::DescriptorType::SampledImage, + .count = NUM_TEXTURE_SLOTS, + .is_variable_length = true}} + ) + }) + .unwrap(); + + gpu::DescriptorSetLayout samplers_layout = + device + ->create_descriptor_set_layout(gpu::DescriptorSetLayoutInfo{ + .label = "Samplers Layout"_str, + .bindings = + span({gpu::DescriptorBindingInfo{.type = gpu::DescriptorType::Sampler, + .count = NUM_SAMPLER_SLOTS, + .is_variable_length = true}} + ) + }) + .unwrap(); + + gpu::DescriptorSet texture_views = + device + ->create_descriptor_set(gpu::DescriptorSetInfo{ + .label = "Texture Views"_str, + .layout = textures_layout, + .variable_lengths = span({NUM_TEXTURE_SLOTS})}) + .unwrap(); + + gpu::DescriptorSet samplers = + device + ->create_descriptor_set(gpu::DescriptorSetInfo{ + .label = "Samplers"_str, + .layout = samplers_layout, + .variable_lengths = span({NUM_SAMPLER_SLOTS})}) + .unwrap(); + + gpu::Image default_image = + device + ->create_image(gpu::ImageInfo{ + .label = "Default Image"_str, + .type = gpu::ImageType::Type2D, + .format = gpu::Format::B8G8R8A8_UNORM, + .usage = gpu::ImageUsage::Sampled | gpu::ImageUsage::TransferDst | + gpu::ImageUsage::Storage | gpu::ImageUsage::Storage, + .aspects = gpu::ImageAspects::Color, + .extent = {1, 1, 1}, + .mip_levels = 1, + .array_layers = 1, + .sample_count = gpu::SampleCount::C1 + }) + .unwrap(); + + InplaceVec, gpu::MAX_FRAME_BUFFERING> released_objects; + + for (u32 i = 0; i < buffering; i++) + { + released_objects.push(Vec{allocator}).unwrap(); + } + + GpuSystem gpu{allocator, + device, + pipeline_cache, + buffering, + sample_count, + color_format, + depth_stencil_format, + ubo_layout, + ssbo_layout, + textures_layout, + samplers_layout, + texture_views, + samplers, + default_image, + {}, + std::move(released_objects)}; + + { + static constexpr Tuple, TextureId, gpu::ComponentMapping> + mappings[] = { + {"Default White Texture"_str, + TextureId::White, + {.r = gpu::ComponentSwizzle::One, + .g = gpu::ComponentSwizzle::One, + .b = gpu::ComponentSwizzle::One, + .a = gpu::ComponentSwizzle::One} }, + {"Default Black Texture"_str, + TextureId::Black, + {.r = gpu::ComponentSwizzle::Zero, + .g = gpu::ComponentSwizzle::Zero, + .b = gpu::ComponentSwizzle::Zero, + .a = gpu::ComponentSwizzle::One} }, + {"Default Transparent Texture"_str, + TextureId::Transparent, + {.r = gpu::ComponentSwizzle::Zero, + .g = gpu::ComponentSwizzle::Zero, + .b = gpu::ComponentSwizzle::Zero, + .a = gpu::ComponentSwizzle::Zero}}, + {"Default Alpha Texture"_str, + TextureId::Alpha, + {.r = gpu::ComponentSwizzle::Zero, + .g = gpu::ComponentSwizzle::Zero, + .b = gpu::ComponentSwizzle::Zero, + .a = gpu::ComponentSwizzle::One} }, + {"Default Red Texture"_str, + TextureId::Red, + {.r = gpu::ComponentSwizzle::One, + .g = gpu::ComponentSwizzle::Zero, + .b = gpu::ComponentSwizzle::Zero, + .a = gpu::ComponentSwizzle::One} }, + {"Default Green Texture"_str, + TextureId::Green, + {.r = gpu::ComponentSwizzle::Zero, + .g = gpu::ComponentSwizzle::One, + .b = gpu::ComponentSwizzle::Zero, + .a = gpu::ComponentSwizzle::One} }, + {"Default Blue Texture"_str, + TextureId::Blue, + {.r = gpu::ComponentSwizzle::Zero, + .g = gpu::ComponentSwizzle::Zero, + .b = gpu::ComponentSwizzle::One, + .a = gpu::ComponentSwizzle::One} }, + {"Default Magenta Texture"_str, + TextureId::Magenta, + {.r = gpu::ComponentSwizzle::One, + .g = gpu::ComponentSwizzle::Zero, + .b = gpu::ComponentSwizzle::One, + .a = gpu::ComponentSwizzle::One} }, + {"Default Cyan Texture"_str, + TextureId::Cyan, + {.r = gpu::ComponentSwizzle::Zero, + .g = gpu::ComponentSwizzle::One, + .b = gpu::ComponentSwizzle::One, + .a = gpu::ComponentSwizzle::One} }, + {"Default Yellow Texture"_str, + TextureId::Yellow, + {.r = gpu::ComponentSwizzle::One, + .g = gpu::ComponentSwizzle::One, + .b = gpu::ComponentSwizzle::Zero, + .a = gpu::ComponentSwizzle::One} } + }; + + static_assert(size(mappings) == NUM_DEFAULT_TEXTURES); + + for (auto const [mapping, view] : zip(mappings, gpu.default_image_views)) + { + view = device + ->create_image_view( + gpu::ImageViewInfo{.label = mapping.v0, + .image = default_image, + .view_type = gpu::ImageViewType::Type2D, + .view_format = gpu::Format::B8G8R8A8_UNORM, + .mapping = mapping.v2, + .aspects = gpu::ImageAspects::Color, + .first_mip_level = 0, + .num_mip_levels = 1, + .first_array_layer = 0, + .num_array_layers = 1}) + .unwrap(); + + CHECK(mapping.v1 == gpu.alloc_texture_id(view)); + } + } + + { + static constexpr SamplerId default_ids[NUM_DEFAULT_SAMPLERS] = { + SamplerId::Linear, SamplerId::Nearest, SamplerId::LinearClamped, + SamplerId::NearestClamped}; + + static constexpr gpu::SamplerInfo infos[NUM_DEFAULT_SAMPLERS] = { + {.label = "Linear+Repeat Sampler"_str, + .mag_filter = gpu::Filter::Linear, + .min_filter = gpu::Filter::Linear, + .mip_map_mode = gpu::SamplerMipMapMode::Linear, + .address_mode_u = gpu::SamplerAddressMode::Repeat, + .address_mode_v = gpu::SamplerAddressMode::Repeat, + .address_mode_w = gpu::SamplerAddressMode::Repeat, + .mip_lod_bias = 0, + .anisotropy_enable = false, + .max_anisotropy = 1.0, + .compare_enable = false, + .compare_op = gpu::CompareOp::Never, + .min_lod = 0, + .max_lod = 0, + .border_color = gpu::BorderColor::FloatTransparentBlack, + .unnormalized_coordinates = false}, + {.label = "Nearest+Repeat Sampler"_str, + .mag_filter = gpu::Filter::Nearest, + .min_filter = gpu::Filter::Nearest, + .mip_map_mode = gpu::SamplerMipMapMode::Nearest, + .address_mode_u = gpu::SamplerAddressMode::Repeat, + .address_mode_v = gpu::SamplerAddressMode::Repeat, + .address_mode_w = gpu::SamplerAddressMode::Repeat, + .mip_lod_bias = 0, + .anisotropy_enable = false, + .max_anisotropy = 1.0, + .compare_enable = false, + .compare_op = gpu::CompareOp::Never, + .min_lod = 0, + .max_lod = 0, + .border_color = gpu::BorderColor::FloatTransparentBlack, + .unnormalized_coordinates = false}, + {.label = "Linear+EdgeClamped Sampler"_str, + .mag_filter = gpu::Filter::Linear, + .min_filter = gpu::Filter::Linear, + .mip_map_mode = gpu::SamplerMipMapMode::Linear, + .address_mode_u = gpu::SamplerAddressMode::ClampToEdge, + .address_mode_v = gpu::SamplerAddressMode::ClampToEdge, + .address_mode_w = gpu::SamplerAddressMode::ClampToEdge, + .mip_lod_bias = 0, + .anisotropy_enable = false, + .max_anisotropy = 1.0, + .compare_enable = false, + .compare_op = gpu::CompareOp::Never, + .min_lod = 0, + .max_lod = 0, + .border_color = gpu::BorderColor::FloatTransparentBlack, + .unnormalized_coordinates = false}, + {.label = "Nearest+EdgeClamped Sampler"_str, + .mag_filter = gpu::Filter::Nearest, + .min_filter = gpu::Filter::Nearest, + .mip_map_mode = gpu::SamplerMipMapMode::Nearest, + .address_mode_u = gpu::SamplerAddressMode::ClampToEdge, + .address_mode_v = gpu::SamplerAddressMode::ClampToEdge, + .address_mode_w = gpu::SamplerAddressMode::ClampToEdge, + .mip_lod_bias = 0, + .anisotropy_enable = false, + .max_anisotropy = 1.0, + .compare_enable = false, + .compare_op = gpu::CompareOp::Never, + .min_lod = 0, + .max_lod = 0, + .border_color = gpu::BorderColor::FloatTransparentBlack, + .unnormalized_coordinates = false} + }; + + for (auto const [expected_id, info] : zip(default_ids, infos)) + { + CHECK(gpu.create_sampler(info).id == expected_id); + } + } + + gpu.recreate_framebuffers(initial_extent); + + return gpu; +} + +static void recreate_framebuffer(GpuSystem & gpu, Framebuffer & fb, + gpu::Extent new_extent) +{ + gpu.release(fb); + fb = Framebuffer{}; + gpu::Device & dev = *gpu.device; + + gpu::ImageInfo info{ + .label = "Resolved Framebuffer Color Image"_str, + .type = gpu::ImageType::Type2D, + .format = gpu.color_format, + .usage = gpu::ImageUsage::ColorAttachment | gpu::ImageUsage::Sampled | + gpu::ImageUsage::Storage | gpu::ImageUsage::TransferDst | + gpu::ImageUsage::TransferSrc, + .aspects = gpu::ImageAspects::Color, + .extent = gpu::Extent3D{new_extent.x, new_extent.y, 1}, + .mip_levels = 1, + .array_layers = 1, + .sample_count = gpu::SampleCount::C1 + }; + + gpu::Image image = dev.create_image(info).unwrap(); + + gpu::ImageViewInfo view_info{.label = + "Resolved Framebuffer Color Image View"_str, + .image = image, + .view_type = gpu::ImageViewType::Type2D, + .view_format = info.format, + .mapping = {}, + .aspects = gpu::ImageAspects::Color, + .first_mip_level = 0, + .num_mip_levels = 1, + .first_array_layer = 0, + .num_array_layers = 1}; + + gpu::ImageView view = dev.create_image_view(view_info).unwrap(); + + gpu::DescriptorSet texture = + dev + .create_descriptor_set(gpu::DescriptorSetInfo{ + .label = "Resolved Framebuffer Color Image Descriptor"_str, + .layout = gpu.textures_layout, + .variable_lengths = span({1})}) + .unwrap(); + + dev.update_descriptor_set(gpu::DescriptorSetUpdate{ + .set = texture, + .binding = 0, + .element = 0, + .images = span({gpu::ImageBinding{.image_view = view}})}); + + fb.color = Framebuffer::Color{.info = info, + .view_info = view_info, + .image = image, + .view = view, + .texture = texture}; + + if (gpu.sample_count != gpu::SampleCount::C1) + { + gpu::ImageInfo info{ + .label = "Framebuffer MSAA Color Image"_str, + .type = gpu::ImageType::Type2D, + .format = gpu.color_format, + .usage = gpu::ImageUsage::ColorAttachment | gpu::ImageUsage::TransferSrc | + gpu::ImageUsage::TransferDst, + .aspects = gpu::ImageAspects::Color, + .extent = gpu::Extent3D{new_extent.x, new_extent.y, 1}, + .mip_levels = 1, + .array_layers = 1, + .sample_count = gpu.sample_count + }; + + gpu::Image image = dev.create_image(info).unwrap(); + + gpu::ImageViewInfo view_info{.label = + "Framebuffer MSAA Color Image View"_str, + .image = image, + .view_type = gpu::ImageViewType::Type2D, + .view_format = info.format, + .mapping = {}, + .aspects = gpu::ImageAspects::Color, + .first_mip_level = 0, + .num_mip_levels = 1, + .first_array_layer = 0, + .num_array_layers = 1}; + + gpu::ImageView view = dev.create_image_view(view_info).unwrap(); + + fb.color_msaa = Framebuffer::ColorMsaa{ + .info = info, .view_info = view_info, .image = image, .view = view}; + } + + { + gpu::ImageInfo info{ + .label = "Framebuffer Depth & Stencil Image"_str, + .type = gpu::ImageType::Type2D, + .format = gpu.depth_stencil_format, + .usage = gpu::ImageUsage::DepthStencilAttachment | + gpu::ImageUsage::Sampled | gpu::ImageUsage::TransferDst | + gpu::ImageUsage::TransferSrc, + .aspects = gpu::ImageAspects::Depth | gpu::ImageAspects::Stencil, + .extent = gpu::Extent3D{new_extent.x, new_extent.y, 1}, + .mip_levels = 1, + .array_layers = 1, + .sample_count = gpu::SampleCount::C1 + }; + + gpu::Image image = dev.create_image(info).unwrap(); + + gpu::ImageViewInfo view_info{.label = "Framebuffer Depth Image View"_str, + .image = image, + .view_type = gpu::ImageViewType::Type2D, + .view_format = info.format, + .mapping = {}, + .aspects = gpu::ImageAspects::Depth, + .first_mip_level = 0, + .num_mip_levels = 1, + .first_array_layer = 0, + .num_array_layers = 1}; + + gpu::ImageView view = dev.create_image_view(view_info).unwrap(); + + gpu::ImageViewInfo stencil_view_info{ + .label = "Framebuffer Stencil Image View"_str, + .image = image, + .view_type = gpu::ImageViewType::Type2D, + .view_format = info.format, + .mapping = {}, + .aspects = gpu::ImageAspects::Stencil, + .first_mip_level = 0, + .num_mip_levels = 1, + .first_array_layer = 0, + .num_array_layers = 1}; + + gpu::ImageView stencil_view = + dev.create_image_view(stencil_view_info).unwrap(); + + gpu::DescriptorSet texture = + dev + .create_descriptor_set(gpu::DescriptorSetInfo{ + .label = "Framebuffer Depth Image Descriptor"_str, + .layout = gpu.textures_layout, + .variable_lengths = span({1})}) + .unwrap(); + + dev.update_descriptor_set(gpu::DescriptorSetUpdate{ + .set = texture, + .binding = 0, + .element = 0, + .images = span({gpu::ImageBinding{.image_view = view}})}); + + gpu::DescriptorSet stencil_texture = + dev + .create_descriptor_set(gpu::DescriptorSetInfo{ + .label = "Framebuffer Stencil Image Descriptor"_str, + .layout = gpu.textures_layout, + .variable_lengths = span({1})}) + .unwrap(); + + dev.update_descriptor_set(gpu::DescriptorSetUpdate{ + .set = stencil_texture, + .binding = 0, + .element = 0, + .images = span({gpu::ImageBinding{.image_view = stencil_view}})}); + + fb.depth = Framebuffer::Depth{.info = info, + .view_info = view_info, + .stencil_view_info = stencil_view_info, + .image = image, + .view = view, + .stencil_view = stencil_view, + .texture = texture, + .stencil_texture = stencil_texture}; + } +} + +void GpuSystem::recreate_framebuffers(gpu::Extent new_extent) +{ + idle_reclaim(); + recreate_framebuffer(*this, fb, new_extent); + recreate_framebuffer(*this, scratch_fb, new_extent); +} + +gpu::CommandEncoder & GpuSystem::encoder() +{ + gpu::FrameContext ctx = device->get_frame_context(); + return *ctx.encoders[ctx.ring_index]; +} + +u32 GpuSystem::ring_index() +{ + gpu::FrameContext ctx = device->get_frame_context(); + return ctx.ring_index; +} + +gpu::FrameId GpuSystem::frame_id() +{ + gpu::FrameContext ctx = device->get_frame_context(); + return ctx.current; +} + +gpu::FrameId GpuSystem::tail_frame_id() +{ + gpu::FrameContext ctx = device->get_frame_context(); + return ctx.tail; +} + +CachedSampler GpuSystem::create_sampler(gpu::SamplerInfo const & info) +{ + OptionRef cached = sampler_cache.try_get(info); + if (cached) + { + return cached.value(); + } + + gpu::Sampler sampler = device->create_sampler(info).unwrap(); + SamplerId id = alloc_sampler_id(sampler); + + CachedSampler entry{.sampler = sampler, .id = id}; + + sampler_cache.insert(info, entry).unwrap(); + + return entry; +} + +TextureId GpuSystem::alloc_texture_id(gpu::ImageView view) +{ + usize i = find_clear_bit(texture_slots); + CHECK_DESC(i < size_bits(texture_slots), "Out of Texture Slots"); + set_bit(texture_slots, i); + device->wait_idle().unwrap(); + device->update_descriptor_set(gpu::DescriptorSetUpdate{ + .set = texture_views, + .binding = 0, + .element = (u32) i, + .images = span({gpu::ImageBinding{.image_view = view}})}); + return TextureId{(u32) i}; +} + +void GpuSystem::release_texture_id(TextureId id) +{ + clear_bit(texture_slots, (u32) id); +} + +SamplerId GpuSystem::alloc_sampler_id(gpu::Sampler sampler) +{ + usize i = find_clear_bit(sampler_slots); + CHECK_DESC(i < size_bits(sampler_slots), "Out of Sampler Slots"); + set_bit(sampler_slots, i); + device->wait_idle().unwrap(); + device->update_descriptor_set(gpu::DescriptorSetUpdate{ + .set = samplers, + .binding = 0, + .element = (u32) i, + .images = span({gpu::ImageBinding{.sampler = sampler}})}); + return SamplerId{(u32) i}; +} + +void GpuSystem::release_sampler_id(SamplerId id) +{ + clear_bit(sampler_slots, (u32) id); +} + +void GpuSystem::release(gpu::Object object) +{ + if (object.v18_ == nullptr) + { + return; + } + + released_objects[ring_index()].push(object).unwrap(); +} + +static void uninit_objects(gpu::Device & d, Span objects) +{ + for (gpu::Object obj : objects) + { + obj.match([](gpu::Instance *) { CHECK_UNREACHABLE(); }, + [](gpu::Device *) { CHECK_UNREACHABLE(); }, + [](gpu::CommandEncoder *) { CHECK_UNREACHABLE(); }, + [&](gpu::Buffer r) { d.uninit(r); }, + [&](gpu::BufferView r) { d.uninit(r); }, + [&](gpu::Image r) { d.uninit(r); }, + [&](gpu::ImageView r) { d.uninit(r); }, + [&](gpu::Sampler r) { d.uninit(r); }, + [&](gpu::Shader r) { d.uninit(r); }, + [&](gpu::DescriptorSetLayout r) { d.uninit(r); }, + [&](gpu::DescriptorSet r) { d.uninit(r); }, + [&](gpu::PipelineCache r) { d.uninit(r); }, + [&](gpu::ComputePipeline r) { d.uninit(r); }, + [&](gpu::GraphicsPipeline r) { d.uninit(r); }, + [&](gpu::TimeStampQuery r) { d.uninit(r); }, + [&](gpu::StatisticsQuery r) { d.uninit(r); }, + [&](gpu::Surface) { CHECK_UNREACHABLE(); }, + [&](gpu::Swapchain) { CHECK_UNREACHABLE(); }, + [](void *) { CHECK_UNREACHABLE(); }); + } +} + +void GpuSystem::idle_reclaim() +{ + device->wait_idle().unwrap(); + for (auto & objects : released_objects) + { + uninit_objects(*device, objects); + objects.clear(); + } +} + +void GpuSystem::begin_frame(gpu::Swapchain swapchain) +{ + device->begin_frame(swapchain).unwrap(); + uninit_objects(*device, released_objects[ring_index()]); + released_objects[ring_index()].clear(); + + gpu::CommandEncoder & enc = encoder(); + + auto clear_color = [&](gpu::Image image) { + enc.clear_color_image( + image, + gpu::Color{ + }, + span({gpu::ImageSubresourceRange{.aspects = gpu::ImageAspects::Color, + .first_mip_level = 0, + .num_mip_levels = 1, + .first_array_layer = 0, + .num_array_layers = 1}})); + }; + + auto clear_depth = [&](gpu::Image image) { + enc.clear_depth_stencil_image( + image, + gpu::DepthStencil{ + }, + span({gpu::ImageSubresourceRange{.aspects = gpu::ImageAspects::Depth | + gpu::ImageAspects::Stencil, + .first_mip_level = 0, + .num_mip_levels = 1, + .first_array_layer = 0, + .num_array_layers = 1}})); + }; + + clear_color(fb.color.image); + fb.color_msaa.match( + [&](Framebuffer::ColorMsaa const & c) { clear_color(c.image); }); + clear_depth(fb.depth.image); + + clear_color(scratch_fb.color.image); + scratch_fb.color_msaa.match( + [&](Framebuffer::ColorMsaa const & c) { clear_color(c.image); }); + clear_depth(scratch_fb.depth.image); +} + +void GpuSystem::submit_frame(gpu::Swapchain swapchain) +{ + gpu::CommandEncoder & enc = encoder(); + if (swapchain != nullptr) + { + gpu::SwapchainState swapchain_state = + device->get_swapchain_state(swapchain).unwrap(); + + if (swapchain_state.current_image.is_some()) + { + enc.blit_image( + fb.color.image, + swapchain_state.images[swapchain_state.current_image.unwrap()], + span({ + gpu::ImageBlit{.src_layers = {.aspects = gpu::ImageAspects::Color, + .mip_level = 0, + .first_array_layer = 0, + .num_array_layers = 1}, + .src_offsets = {{0, 0, 0}, fb.extent3()}, + .dst_layers = {.aspects = gpu::ImageAspects::Color, + .mip_level = 0, + .first_array_layer = 0, + .num_array_layers = 1}, + .dst_offsets = {{0, 0, 0}, + {swapchain_state.extent.x, + swapchain_state.extent.y, 1}}} + }), + gpu::Filter::Linear); + } + } + device->submit_frame(swapchain).unwrap(); +} + +void SSBO::uninit(GpuSystem & gpu) +{ + gpu.device->uninit(descriptor); + gpu.device->uninit(buffer); +} + +void SSBO::reserve(GpuSystem & gpu, u64 p_size) +{ + p_size = max(p_size, (u64) 1); + if (buffer != nullptr && size >= p_size) + { + return; + } + + gpu.device->uninit(buffer); + + buffer = + gpu.device + ->create_buffer(gpu::BufferInfo{.label = label, + .size = p_size, + .host_mapped = true, + .usage = gpu::BufferUsage::TransferSrc | + gpu::BufferUsage::TransferDst | + gpu::BufferUsage::UniformBuffer | + gpu::BufferUsage::StorageBuffer}) + .unwrap(); + + if (descriptor == nullptr) + { + descriptor = + gpu.device + ->create_descriptor_set(gpu::DescriptorSetInfo{ + .label = label, .layout = gpu.ssbo_layout, .variable_lengths = {}}) + .unwrap(); + } + + gpu.device->update_descriptor_set(gpu::DescriptorSetUpdate{ + .set = descriptor, + .binding = 0, + .element = 0, + .buffers = span( + {gpu::BufferBinding{.buffer = buffer, .offset = 0, .size = p_size}} + ) + }); + + size = p_size; +} + +void SSBO::copy(GpuSystem & gpu, Span src) +{ + reserve(gpu, (u64) src.size()); + u8 * data = (u8 *) map(gpu); + mem::copy(src, data); + flush(gpu); + unmap(gpu); +} + +void * SSBO::map(GpuSystem & gpu) +{ + return gpu.device->map_buffer_memory(buffer).unwrap(); +} + +void SSBO::unmap(GpuSystem & gpu) +{ + gpu.device->unmap_buffer_memory(buffer); +} + +void SSBO::flush(GpuSystem & gpu) +{ + gpu.device + ->flush_mapped_buffer_memory(buffer, gpu::MemoryRange{0, gpu::WHOLE_SIZE}) + .unwrap(); +} + +void SSBO::release(GpuSystem & gpu) +{ + gpu.release(buffer); + gpu.release(descriptor); + buffer = nullptr; + size = 0; + descriptor = nullptr; +} + +} // namespace ash diff --git a/ashura/engine/gpu_system.h b/ashura/engine/gpu_system.h new file mode 100644 index 000000000..fb2458eb4 --- /dev/null +++ b/ashura/engine/gpu_system.h @@ -0,0 +1,429 @@ +/// SPDX-License-Identifier: MIT +#pragma once +#include "ashura/gpu/gpu.h" +#include "ashura/std/allocators.h" +#include "ashura/std/async.h" +#include "ashura/std/map.h" +#include "ashura/std/option.h" +#include "ashura/std/types.h" +#include "ashura/std/vec.h" + +namespace ash +{ + +enum class TextureId : u32 +{ + Base = 0, + White = 0, + Black = 1, + Transparent = 2, + Alpha = 3, + Red = 4, + Green = 5, + Blue = 6, + Magenta = 7, + Cyan = 8, + Yellow = 9 +}; + +inline constexpr u32 NUM_DEFAULT_TEXTURES = 10; + +enum class SamplerId : u32 +{ + Linear = 0, + Nearest = 1, + LinearClamped = 2, + NearestClamped = 3 +}; + +inline constexpr u32 NUM_DEFAULT_SAMPLERS = 4; + +struct Framebuffer +{ + /// @brief created with sampled, storage, color attachment, and transfer flags + struct Color + { + gpu::ImageInfo info = {}; + + gpu::ImageViewInfo view_info = {}; + + gpu::Image image = nullptr; + + gpu::ImageView view = nullptr; + + gpu::DescriptorSet texture = nullptr; + + static constexpr TextureId texture_id = TextureId::Base; + }; + + /// @brief created with color attachment flag + struct ColorMsaa + { + gpu::ImageInfo info = {}; + + gpu::ImageViewInfo view_info = {}; + + gpu::Image image = nullptr; + + /// @brief to preserve bandwidth (especially for tiled architectures), preferably + /// use `StoreOp::DontCare` and `LoadOp::Clear/LoadOp::DontCare` in the render passes. + gpu::ImageView view = nullptr; + + constexpr gpu::SampleCount sample_count() const + { + return info.sample_count; + } + }; + + struct Depth + { + gpu::ImageInfo info = {}; + + gpu::ImageViewInfo view_info = {}; + + gpu::ImageViewInfo stencil_view_info = {}; + + gpu::Image image = nullptr; + + gpu::ImageView view = nullptr; + + gpu::ImageView stencil_view = {}; + + gpu::DescriptorSet texture = nullptr; + + static constexpr TextureId texture_id = TextureId::Base; + + gpu::DescriptorSet stencil_texture = nullptr; + + static constexpr TextureId stencil_texture_id = TextureId::Base; + }; + + /// @brief color texture + Color color = {}; + + Option color_msaa = none; + + /// @brief combined depth and stencil aspect attachment + Depth depth = {}; + + constexpr gpu::Viewport viewport() const + { + return gpu::Viewport{.offset = {}, + .extent = as_vec2(extent()), + .min_depth = 0, + .max_depth = 1}; + } + + constexpr gpu::Rect scissor() const + { + return gpu::Rect{.offset = {}, .extent = extent()}; + } + + constexpr gpu::Extent3D extent3() const + { + return color.info.extent; + } + + constexpr gpu::Extent extent() const + { + return gpu::Extent{extent3().x, extent3().y}; + } +}; + +struct SamplerHasher +{ + constexpr hash64 operator()(gpu::SamplerInfo const & info) const + { + return hash_combine_n( + (hash64) info.mag_filter, (hash64) info.min_filter, + (hash64) info.mip_map_mode, (hash64) info.address_mode_u, + (hash64) info.address_mode_v, (hash64) info.address_mode_w, + (hash64) bit_cast(info.mip_lod_bias), + (hash64) info.anisotropy_enable, + (hash64) bit_cast(info.max_anisotropy), (hash64) info.compare_enable, + (hash64) info.compare_op, (hash64) bit_cast(info.min_lod), + (hash64) bit_cast(info.max_lod), (hash64) info.border_color, + (hash64) info.unnormalized_coordinates); + } +}; + +struct SamplerEq +{ + constexpr bool operator()(gpu::SamplerInfo const & a, + gpu::SamplerInfo const & b) const + { + return a.mag_filter == b.mag_filter && a.mip_map_mode == b.mip_map_mode && + a.address_mode_u == b.address_mode_u && + a.address_mode_v == b.address_mode_v && + a.address_mode_w == b.address_mode_w && + a.mip_lod_bias == b.mip_lod_bias && + a.anisotropy_enable == b.anisotropy_enable && + a.max_anisotropy == b.max_anisotropy && + a.compare_enable == b.compare_enable && + a.compare_op == b.compare_op && a.min_lod == b.min_lod && + a.max_lod == b.max_lod && a.border_color == b.border_color && + a.unnormalized_coordinates == b.unnormalized_coordinates; + } +}; + +struct CachedSampler +{ + gpu::Sampler sampler = nullptr; + SamplerId id = SamplerId::Linear; +}; + +typedef Map + SamplerCache; + +/// @param color_format hdr if hdr supported and required. +/// +/// scratch images resized when swapchain extents changes +/// +struct GpuSystem +{ + // [ ] for all the formats, get their properties& features. add to gpu api and allow passes to query them + // [ ] create SRGB/SDR ranking of formats + // [ ] allow user to override swapchain/texture format + + struct JobQueue + { + ArenaPool arena; + Vec> jobs; + + void clear() + { + jobs.clear(); + arena.reclaim(); + } + }; + + struct FrameQueue + { + JobQueue pre; + JobQueue main; + JobQueue post; + }; + + static constexpr gpu::FormatFeatures COLOR_FEATURES = + gpu::FormatFeatures::ColorAttachment | + gpu::FormatFeatures::ColorAttachmentBlend | + gpu::FormatFeatures::StorageImage | gpu::FormatFeatures::SampledImage; + + static constexpr gpu::FormatFeatures DEPTH_STENCIL_FEATURES = + gpu::FormatFeatures::DepthStencilAttachment | + gpu::FormatFeatures::SampledImage; + + static constexpr gpu::BufferUsage SSBO_USAGE = + gpu::BufferUsage::UniformBuffer | gpu::BufferUsage::StorageBuffer | + gpu::BufferUsage::UniformTexelBuffer | + gpu::BufferUsage::StorageTexelBuffer | gpu::BufferUsage::IndirectBuffer | + gpu::BufferUsage::TransferSrc | gpu::BufferUsage::TransferDst; + + static constexpr gpu::Format HDR_COLOR_FORMATS[] = { + gpu::Format::R16G16B16A16_SFLOAT}; + + static constexpr gpu::Format SDR_COLOR_FORMATS[] = { + gpu::Format::B8G8R8A8_UNORM, gpu::Format::R8G8B8A8_UNORM}; + + static constexpr gpu::Format DEPTH_STENCIL_FORMATS[] = { + gpu::Format::D16_UNORM_S8_UINT, gpu::Format::D24_UNORM_S8_UINT, + gpu::Format::D32_SFLOAT_S8_UINT}; + + static constexpr u16 NUM_TEXTURE_SLOTS = 1'024; + static constexpr u16 NUM_SAMPLER_SLOTS = 64; + + gpu::Device * device; + + gpu::PipelineCache pipeline_cache; + + u32 buffering; + + gpu::SampleCount sample_count; + + gpu::Format color_format; + + gpu::Format depth_stencil_format; + + gpu::DescriptorSetLayout ubo_layout; + + gpu::DescriptorSetLayout ssbo_layout; + + gpu::DescriptorSetLayout textures_layout; + + gpu::DescriptorSetLayout samplers_layout; + + gpu::DescriptorSet texture_views; + + gpu::DescriptorSet samplers; + + SamplerCache sampler_cache; + + Framebuffer fb; + + Framebuffer scratch_fb; + + gpu::Image default_image; + + Array default_image_views; + + InplaceVec, gpu::MAX_FRAME_BUFFERING> released_objects; + + Bits texture_slots; + + Bits sampler_slots; + + InplaceVec jobs; + + static GpuSystem create(AllocatorImpl allocator, gpu::Device * device, + bool use_hdr, u32 buffering, + gpu::SampleCount sample_count, + gpu::Extent initial_extent); + + GpuSystem( + AllocatorImpl allocator, gpu::Device * device, + gpu::PipelineCache pipeline_cache, u32 buffering, + gpu::SampleCount sample_count, gpu::Format color_format, + gpu::Format depth_stencil_format, gpu::DescriptorSetLayout ubo_layout, + gpu::DescriptorSetLayout ssbo_layout, + gpu::DescriptorSetLayout textures_layout, + gpu::DescriptorSetLayout samplers_layout, gpu::DescriptorSet texture_views, + gpu::DescriptorSet samplers, gpu::Image default_image, + Array default_image_views, + InplaceVec, gpu::MAX_FRAME_BUFFERING> released_objects) : + device{device}, + pipeline_cache{pipeline_cache}, + buffering{buffering}, + sample_count{sample_count}, + color_format{color_format}, + depth_stencil_format{depth_stencil_format}, + ubo_layout{ubo_layout}, + ssbo_layout{ssbo_layout}, + textures_layout{textures_layout}, + samplers_layout{samplers_layout}, + texture_views{texture_views}, + samplers{samplers}, + sampler_cache{allocator}, + fb{}, + scratch_fb{}, + default_image{default_image}, + default_image_views{default_image_views}, + released_objects{std::move(released_objects)}, + texture_slots{}, + sampler_slots{} + { + } + + GpuSystem(GpuSystem const &) = delete; + GpuSystem(GpuSystem &&) = default; + GpuSystem & operator=(GpuSystem const &) = delete; + GpuSystem & operator=(GpuSystem &&) = default; + ~GpuSystem() = default; + + void uninit() + { + release(texture_views); + for (gpu::ImageView v : default_image_views) + { + release(v); + } + release(default_image); + release(samplers); + release(ubo_layout); + release(ssbo_layout); + release(textures_layout); + release(samplers_layout); + release(fb); + release(scratch_fb); + for (auto const & [_, sampler] : sampler_cache) + { + release(sampler.sampler); + } + idle_reclaim(); + device->uninit(pipeline_cache); + } + + void recreate_framebuffers(gpu::Extent new_extent); + + gpu::CommandEncoder & encoder(); + + u32 ring_index(); + + gpu::FrameId frame_id(); + + gpu::FrameId tail_frame_id(); + + CachedSampler create_sampler(gpu::SamplerInfo const & info); + + TextureId alloc_texture_id(gpu::ImageView view); + + void release_texture_id(TextureId id); + + SamplerId alloc_sampler_id(gpu::Sampler sampler); + + void release_sampler_id(SamplerId id); + + /// @brief schedule an object for destruction, the object is destructed on the next frame + void release(gpu::Object object); + + void release(Framebuffer::Color const & fb) + { + release(fb.texture); + release(fb.view); + release(fb.image); + } + + void release(Framebuffer::ColorMsaa const & fb) + { + release(fb.view); + release(fb.image); + } + + void release(Framebuffer::Depth const & fb) + { + release(fb.texture); + release(fb.stencil_texture); + release(fb.view); + release(fb.stencil_view); + release(fb.image); + } + + void release(Framebuffer const & fb) + { + release(fb.color); + fb.color_msaa.match([&](Framebuffer::ColorMsaa const & f) { release(f); }); + release(fb.depth); + } + + void idle_reclaim(); + + void begin_frame(gpu::Swapchain swapchain); + + void submit_frame(gpu::Swapchain swapchain); +}; + +struct SSBO +{ + gpu::Buffer buffer = nullptr; + + u64 size = 0; + + gpu::DescriptorSet descriptor = nullptr; + + Span label = "SSBO"_str; + + void uninit(GpuSystem & gpu); + + void reserve(GpuSystem & gpu, u64 size); + + void copy(GpuSystem & gpu, Span src); + + void * map(GpuSystem & gpu); + + void unmap(GpuSystem & gpu); + + void flush(GpuSystem & gpu); + + void release(GpuSystem & gpu); +}; + +} // namespace ash diff --git a/ashura/engine/image_decoder.cc b/ashura/engine/image_decoder.cc index e9e1bf833..0334d72a1 100644 --- a/ashura/engine/image_decoder.cc +++ b/ashura/engine/image_decoder.cc @@ -18,48 +18,50 @@ constexpr u64 rgba8_size(bool has_alpha, u32 width, u32 height) return ((u64) width) * ((u64) height) * (has_alpha ? 4ULL : 3ULL); } -ImageDecodeError decode_webp(Span bytes, DecodedImage & image) +Result decode_webp(Span bytes, + Vec & channels) { WebPBitstreamFeatures features; if (WebPGetFeatures(bytes.data(), bytes.size(), &features) != VP8_STATUS_OK) { - return ImageDecodeError::DecodeFailed; + return Err{ImageLoadErr::DecodeFailed}; } - u32 const pitch = features.width * (features.has_alpha == 0 ? 3U : 4U); - u64 const buffer_size = - rgba8_size(features.has_alpha != 0, features.width, features.height); + u32 const pitch = features.width * (features.has_alpha == 0 ? 3U : 4U); + gpu::Format const fmt = features.has_alpha == 0 ? gpu::Format::R8G8B8_UNORM : + gpu::Format::R8G8B8A8_UNORM; + u64 const buffer_size = + rgba8_size(features.has_alpha != 0, features.width, features.height); - if (!image.channels.resize_uninit(buffer_size)) + if (!channels.resize_uninit(buffer_size)) { - return ImageDecodeError::OutOfMemory; + return Err{ImageLoadErr::OutOfMemory}; } if (features.has_alpha != 0) { - if (WebPDecodeRGBAInto(bytes.data(), bytes.size(), image.channels.data(), + if (WebPDecodeRGBAInto(bytes.data(), bytes.size(), channels.data(), buffer_size, pitch) == nullptr) { - image.channels.clear(); - return ImageDecodeError::DecodeFailed; + channels.clear(); + return Err{ImageLoadErr::DecodeFailed}; } } else { - if (WebPDecodeRGBInto(bytes.data(), bytes.size(), image.channels.data(), + if (WebPDecodeRGBInto(bytes.data(), bytes.size(), channels.data(), buffer_size, pitch) == nullptr) { - image.channels.clear(); - return ImageDecodeError::DecodeFailed; + channels.clear(); + return Err{ImageLoadErr::DecodeFailed}; } } - image.format = features.has_alpha == 0 ? gpu::Format::R8G8B8_UNORM : - gpu::Format::R8G8B8A8_UNORM; - image.width = (u32) features.width; - image.height = (u32) features.height; - return ImageDecodeError::None; + return Ok{ + DecodedImageInfo{.extent{(u32) features.width, (u32) features.height}, + .format = fmt} + }; } inline void png_stream_reader(png_structp png_ptr, unsigned char * out, @@ -70,17 +72,18 @@ inline void png_stream_reader(png_structp png_ptr, unsigned char * out, *input = input->slice(nbytes_to_read); } -ImageDecodeError decode_png(Span bytes, DecodedImage & image) +Result decode_png(Span bytes, + Vec & channels) { // skip magic number bytes = bytes.slice(8); png_structp png_ptr = - png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); if (png_ptr == nullptr) { - return ImageDecodeError::OutOfMemory; + return Err{ImageLoadErr::OutOfMemory}; } png_infop info_ptr = png_create_info_struct(png_ptr); @@ -88,7 +91,7 @@ ImageDecodeError decode_png(Span bytes, DecodedImage & image) if (png_ptr == nullptr) { png_destroy_read_struct(&png_ptr, nullptr, nullptr); - return ImageDecodeError::OutOfMemory; + return Err{ImageLoadErr::OutOfMemory}; } Span stream = bytes; @@ -107,13 +110,13 @@ ImageDecodeError decode_png(Span bytes, DecodedImage & image) if (status != 1) { - return ImageDecodeError::DecodeFailed; + return Err{ImageLoadErr::DecodeFailed}; } if (color_type != PNG_COLOR_TYPE_RGB && color_type != PNG_COLOR_TYPE_RGBA) { png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); - return ImageDecodeError::UnsupportedFormat; + return Err{ImageLoadErr::UnsupportedFormat}; } u32 ncomponents = (color_type == PNG_COLOR_TYPE_RGB) ? 3 : 4; @@ -122,12 +125,12 @@ ImageDecodeError decode_png(Span bytes, DecodedImage & image) u32 pitch = width * ncomponents; u64 buffer_size = (u64) height * pitch; - if (!image.channels.resize_uninit(buffer_size)) + if (!channels.resize_uninit(buffer_size)) { - return ImageDecodeError::OutOfMemory; + return Err{ImageLoadErr::OutOfMemory}; } - u8 * row = image.channels.data(); + u8 * row = channels.data(); for (u32 i = 0; i < height; i++) { png_read_row(png_ptr, row, nullptr); @@ -136,13 +139,13 @@ ImageDecodeError decode_png(Span bytes, DecodedImage & image) png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); - image.format = fmt; - image.width = width; - image.height = height; - return ImageDecodeError::None; + return Ok{ + DecodedImageInfo{.extent{width, height}, .format = fmt} + }; } -ImageDecodeError decode_jpg(Span bytes, DecodedImage & image) +Result decode_jpg(Span bytes, + Vec & channels) { jpeg_decompress_struct info; jpeg_error_mgr error_mgr; @@ -154,36 +157,36 @@ ImageDecodeError decode_jpg(Span bytes, DecodedImage & image) if (jpeg_read_header(&info, true) != JPEG_HEADER_OK) { jpeg_destroy_decompress(&info); - return ImageDecodeError::DecodeFailed; + return Err{ImageLoadErr::DecodeFailed}; } if (jpeg_start_decompress(&info) == 0) { jpeg_destroy_decompress(&info); - return ImageDecodeError::DecodeFailed; + return Err{ImageLoadErr::DecodeFailed}; } if (info.num_components != 3 && info.num_components != 4) { jpeg_destroy_decompress(&info); - return ImageDecodeError::UnsupportedFormat; + return Err{ImageLoadErr::UnsupportedFormat}; } - u32 width = info.output_width; - u32 height = info.output_height; - u32 ncomponents = info.num_components; - u32 pitch = width * ncomponents; - u64 buffer_size = (u64) height * pitch; - gpu::Format fmt = (ncomponents == 3) ? gpu::Format::R8G8B8_UNORM : - gpu::Format::R8G8B8A8_UNORM; + u32 width = info.output_width; + u32 height = info.output_height; + u32 ncomponents = info.num_components; + u32 pitch = width * ncomponents; + u64 buffer_size = (u64) height * pitch; + gpu::Format const fmt = (ncomponents == 3) ? gpu::Format::R8G8B8_UNORM : + gpu::Format::R8G8B8A8_UNORM; - if (!image.channels.resize_uninit(buffer_size)) + if (!channels.resize_uninit(buffer_size)) { jpeg_destroy_decompress(&info); - return ImageDecodeError::OutOfMemory; + return Err{ImageLoadErr::OutOfMemory}; } - u8 * scanline = image.channels.data(); + u8 * scanline = channels.data(); while (info.output_scanline < height) { u8 * scanlines[] = {scanline}; @@ -193,13 +196,13 @@ ImageDecodeError decode_jpg(Span bytes, DecodedImage & image) jpeg_finish_decompress(&info); jpeg_destroy_decompress(&info); - image.format = fmt; - image.width = width; - image.height = height; - return ImageDecodeError::None; + return Ok{ + DecodedImageInfo{.extent{width, height}, .format = fmt} + }; } -ImageDecodeError decode_image(Span bytes, DecodedImage & image) +Result decode_image(Span bytes, + Vec & channels) { static constexpr u8 JPG_MAGIC[] = {0xFF, 0xD8, 0xFF}; static constexpr u8 PNG_MAGIC[] = {0x89, 0x50, 0x4E, 0x47, @@ -210,21 +213,21 @@ ImageDecodeError decode_image(Span bytes, DecodedImage & image) if (range_eq(bytes.slice(0, size(JPG_MAGIC)), JPG_MAGIC)) { - return decode_jpg(bytes, image); + return decode_jpg(bytes, channels); } if (range_eq(bytes.slice(0, size(PNG_MAGIC)), PNG_MAGIC)) { - return decode_png(bytes, image); + return decode_png(bytes, channels); } if (range_eq(bytes.slice(0, size(WEBP_MAGIC1)), WEBP_MAGIC1) && range_eq(bytes.slice(8, size(WEBP_MAGIC2)), WEBP_MAGIC2)) { - return decode_webp(bytes, image); + return decode_webp(bytes, channels); } - return ImageDecodeError::UnsupportedFormat; + return Err{ImageLoadErr::UnsupportedFormat}; } -} // namespace ash +} // namespace ash diff --git a/ashura/engine/image_decoder.h b/ashura/engine/image_decoder.h index ae441c84a..672c516e6 100644 --- a/ashura/engine/image_decoder.h +++ b/ashura/engine/image_decoder.h @@ -1,9 +1,7 @@ /// SPDX-License-Identifier: MIT #pragma once -#include #include "ashura/gpu/gpu.h" -#include "ashura/std/image.h" #include "ashura/std/result.h" #include "ashura/std/types.h" #include "ashura/std/vec.h" @@ -11,30 +9,31 @@ namespace ash { -/// @param InvalidPath the image path provided is invalid -/// @param InvalidData detected image but image seems to be corrupted -/// @param UnsupportedChannels image contains unsupported channel types -/// @param UnsupportedFormat the image file format is unsupported -enum class [[nodiscard]] ImageDecodeError : i32 +enum class [[nodiscard]] ImageLoadErr : i32 { - None = 0, - OutOfMemory = 1, - InvalidPath = 2, - DecodeFailed = 3, - UnsupportedFormat = 4 + OutOfMemory = 0, + DecodeFailed = 1, + UnsupportedFormat = 2, + InvalidPath = 3, + IoErr = 4 }; -struct DecodedImage +struct DecodedImageInfo { - Vec channels = {}; - u32 width = 0; - u32 height = 0; - gpu::Format format = gpu::Format::Undefined; + gpu::Extent extent{1, 1}; + gpu::Format format = gpu::Format::Undefined; }; -ImageDecodeError decode_webp(Span bytes, DecodedImage & image); -ImageDecodeError decode_jpg(Span bytes, DecodedImage & image); -ImageDecodeError decode_png(Span bytes, DecodedImage & image); -ImageDecodeError decode_image(Span bytes, DecodedImage & image); +Result decode_webp(Span bytes, + Vec & channels); -} // namespace ash +Result decode_png(Span bytes, + Vec & channels); + +Result decode_jpg(Span bytes, + Vec & channels); + +Result decode_image(Span bytes, + Vec & channels); + +} // namespace ash diff --git a/ashura/engine/input.h b/ashura/engine/input.h index ef17d30c4..6d9aaf5ce 100644 --- a/ashura/engine/input.h +++ b/ashura/engine/input.h @@ -1,9 +1,11 @@ /// SPDX-License-Identifier: MIT #pragma once +#include "ashura/engine/text.h" #include "ashura/std/enum.h" #include "ashura/std/math.h" #include "ashura/std/result.h" +#include "ashura/std/time.h" #include "ashura/std/types.h" #include "ashura/std/vec.h" @@ -635,11 +637,11 @@ struct DropTextEvent }; using DropEvent = - Enum; + Enum; using WindowEvent = - Enum; + Enum; enum class SystemEventType : u32 { @@ -659,6 +661,44 @@ enum class SystemEventType : u32 using SystemEvent = Enum; +enum class TextInputType : u32 +{ + Text = 0, + Number = 1, + Name = 2, + Email = 3, + Username = 4, + PasswordHidden = 5, + PasswordVisible = 6, + NumberPasswordHidden = 7, + NumberPasswordVisible = 8 +}; + +enum class TextCapitalization : u32 +{ + None = 0, + Sentences = 1, + Words = 2, + Letters = 3 +}; + +struct TextInputInfo +{ + TextInputType type = TextInputType::Text; + + bool multiline = false; + + /// @brief can receive `Tab` key as input + bool esc_input = false; + + /// @brief can receive `Esc` key as input + bool tab_input = false; + + TextCapitalization cap = TextCapitalization::None; + + bool autocorrect = false; +}; + /// @param Normal region is normal and has no special properties /// @param Draggable region can drag entire window /// @param ResizeTopLeft region can resize top left window @@ -799,4 +839,282 @@ struct ClipBoard } }; -} // namespace ash +enum class DropType : u32 +{ + None = 0, + FilePath = 1, + Bytes = 2 +}; + +struct InputState +{ + struct Mouse + { + /// @brief did the mouse enter the window on this frame? + bool in = false; + + /// @brief did the mouse leave the window on this frame? + bool out = false; + + /// @brief did the mouse move on this frame? + bool moved = false; + + /// @brief did the mouse wheel get scrolled on this frame? + bool wheel_scrolled = false; + + /// @brief is any of the keys pressed on this frame + bool any_down = false; + + /// @brief is any of the keys released on this frame + bool any_up = false; + + /// @brief which mouse buttons were pressed on this frame + Bits downs{}; + + /// @brief which mouse buttons were released on this frame + Bits ups{}; + + /// @brief the current state of each mouse button + Bits states{}; + + /// @brief number of times the mouse was clicked so far + u32 num_clicks[NUM_MOUSE_BUTTONS]{}; + + /// @brief the position of the mouse on this frame + Vec2 position = {}; + + /// @brief translation of the mouse on this frame + Vec2 translation = {}; + + /// @brief translation of the mouse wheel on this frame + Vec2 wheel_translation = {}; + + void clear() + { + in = false; + out = false; + moved = false; + wheel_scrolled = false; + any_down = false; + any_up = false; + fill(downs, 0ULL); + fill(ups, 0ULL); + fill(states, 0ULL); + fill(num_clicks, 0ULL); + position = {}; + translation = {}; + wheel_translation = {}; + } + }; + + struct Keyboard + { + /// @brief did the window gain keyboard focus on this frame? + bool in = false; + + /// @brief did the window lose keyboard focus on this frame? + bool out = false; + + /// @brief is any of the keys pressed on this frame + bool any_down = false; + + /// @brief is any of the keys released on this frame + bool any_up = false; + + /// @brief bit mask of all the keys that were pressed on this frame + Bits downs{}; + + /// @brief bit mask of all the keys that were released on this frame + Bits ups{}; + + /// @brief bit mask of all the key states + Bits states{}; + + /// @brief bit mask of all the keys that were pressed on this frame, indexed using the scancode + Bits scan_downs{}; + + /// @brief bit mask of all the keys that were released on this frame, indexed using the scancode + Bits scan_ups{}; + + /// @brief bit mask of all the key states, indexed using the scancode + Bits scan_states{}; + + /// @brief hold state of the key modifiers on this frame + KeyModifiers modifiers = KeyModifiers::None; + + void clear() + { + in = false; + out = false; + any_down = false; + any_up = false; + fill(downs, 0ULL); + fill(ups, 0ULL); + fill(states, 0ULL); + fill(scan_downs, 0ULL); + fill(scan_ups, 0ULL); + fill(scan_states, 0ULL); + modifiers = KeyModifiers::None; + } + }; + + /// @brief timestamp of current frame + time_point timestamp = {}; + + /// @brief time elapsed between previous and current frame + nanoseconds timedelta = {}; + + /// @brief the current theme gotten from the window manager + SystemTheme theme = SystemTheme::Unknown; + + /// @brief the preferred text direction of the host system + TextDirection direction = TextDirection::LeftToRight; + + /// @brief current window mouse focus state + bool mouse_focused = false; + + /// @brief current window keyboard focus state + bool key_focused = false; + + /// @brief windows' current frame mouse state + Mouse mouse{}; + + /// @brief windows' current frame keyboard state + Keyboard key{}; + + /// @brief extent of the viewport the windows' views are in + Vec2 viewport_extent = {}; + + /// @brief current drop data type + DropType drop_type = DropType::None; + + /// @brief drag data associated with the current drag operation (if any, otherwise empty) + Vec drop_data{}; + + /// @brief if a text input came in + bool text_input = false; + + /// @brief current text input data from the IME or keyboard + Vec text{}; + + /// @brief is the application requested to close + bool close_requested = false; + + /// @brief did a window resize happen + bool resized = true; + + /// @brief did a window surface resize happen + bool surface_resized = true; + + bool dropped = false; + + bool drop_hovering = false; + + explicit InputState(AllocatorImpl allocator) : + drop_data{allocator}, + text{allocator} + { + } + + InputState(InputState const &) = delete; + InputState & operator=(InputState const &) = delete; + InputState(InputState &&) = default; + InputState & operator=(InputState &&) = default; + ~InputState() = default; + + void stamp(time_point time, nanoseconds delta) + { + timestamp = time; + timedelta = delta; + } + + void clear() + { + mouse.clear(); + key.clear(); + text_input = false; + text.clear(); + resized = false; + surface_resized = false; + + // if the there was a data drop on the last frame clear the buffer + if (dropped) + { + drop_data.clear(); + drop_type = DropType::None; + } + + dropped = false; + drop_hovering = false; + } + + void clone_to(InputState & dst) const + { + dst.clear(); + dst.timestamp = timestamp; + dst.timedelta = timedelta; + dst.theme = theme; + dst.direction = direction; + dst.mouse_focused = mouse_focused; + dst.key_focused = key_focused; + dst.mouse = mouse; + dst.key = key; + dst.viewport_extent = viewport_extent; + dst.drop_type = drop_type; + dst.drop_data.extend(drop_data).unwrap(); + dst.text_input = text_input; + dst.text.extend(text).unwrap(); + dst.close_requested = close_requested; + dst.resized = resized; + dst.surface_resized = resized; + dst.dropped = dropped; + dst.drop_hovering = drop_hovering; + } + + constexpr bool key_down(KeyCode k) const + { + return get_bit(key.downs, (usize) k); + } + + constexpr bool key_up(KeyCode k) const + { + return get_bit(key.ups, (usize) k); + } + + constexpr bool key_state(KeyCode k) const + { + return get_bit(key.states, (usize) k); + } + + constexpr bool key_down(ScanCode k) const + { + return get_bit(key.scan_downs, (usize) k); + } + + constexpr bool key_up(ScanCode k) const + { + return get_bit(key.scan_ups, (usize) k); + } + + constexpr bool key_state(ScanCode k) const + { + return get_bit(key.scan_states, (usize) k); + } + + constexpr bool mouse_down(MouseButton btn) const + { + return get_bit(mouse.downs, (u32) btn); + } + + constexpr bool mouse_up(MouseButton btn) const + { + return get_bit(mouse.ups, (u32) btn); + } + + constexpr bool mouse_state(MouseButton btn) const + { + return get_bit(mouse.states, (u32) btn); + } +}; + +} // namespace ash diff --git a/ashura/engine/light.h b/ashura/engine/light.h index d6235d4d6..d2cc0b99b 100644 --- a/ashura/engine/light.h +++ b/ashura/engine/light.h @@ -8,8 +8,8 @@ namespace ash struct PunctualLight { - Vec4 direction = {0, 0, 0, 0}; // xyz - Vec4 position = {0, 0, 0, 0}; // xyz + Vec4 direction = {0, 0, 0, 0}; // xyz + Vec4 position = {0, 0, 0, 0}; // xyz Vec4 color = {1, 1, 1, 1}; f32 inner_angle = 0; f32 outer_angle = 0; @@ -17,4 +17,4 @@ struct PunctualLight f32 radius = 0; }; -} // namespace ash +} // namespace ash diff --git a/ashura/engine/passes.cc b/ashura/engine/passes.cc index 2b56b3072..8b62700ce 100644 --- a/ashura/engine/passes.cc +++ b/ashura/engine/passes.cc @@ -5,15 +5,15 @@ namespace ash { -void BloomPass::acquire(GpuContext &, AssetMap &) +void BloomPass::acquire(GpuSystem &, AssetMap &) { } -void BloomPass::release(GpuContext &, AssetMap &) +void BloomPass::release(GpuSystem &, AssetMap &) { } -void BloomPass::encode(GpuContext &, gpu::CommandEncoder &, +void BloomPass::encode(GpuSystem &, gpu::CommandEncoder &, BloomPassParams const &) { /// E' = Blur(E) @@ -23,7 +23,7 @@ void BloomPass::encode(GpuContext &, gpu::CommandEncoder &, /// A' = Blur(A) + B' } -void BlurPass::acquire(GpuContext & ctx, AssetMap & assets) +void BlurPass::acquire(GpuSystem & gpu, AssetMap & assets) { // https://www.youtube.com/watch?v=ml-5OGZC7vE // @@ -41,16 +41,17 @@ void BlurPass::acquire(GpuContext & ctx, AssetMap & assets) .polygon_mode = gpu::PolygonMode::Fill, .cull_mode = gpu::CullMode::None, .front_face = - gpu::FrontFace::CounterClockWise, + gpu::FrontFace::CounterClockWise, .depth_bias_enable = false, .depth_bias_constant_factor = 0, .depth_bias_clamp = 0, - .depth_bias_slope_factor = 0}; + .depth_bias_slope_factor = 0, + .sample_count = gpu::SampleCount::C1}; gpu::DepthStencilState depth_stencil_state{.depth_test_enable = false, .depth_write_enable = false, .depth_compare_op = - gpu::CompareOp::Greater, + gpu::CompareOp::Greater, .depth_bounds_test_enable = false, .stencil_test_enable = false, .front_stencil = {}, @@ -59,161 +60,149 @@ void BlurPass::acquire(GpuContext & ctx, AssetMap & assets) .max_depth_bounds = 0}; gpu::ColorBlendAttachmentState attachment_states[] = { - {.blend_enable = false, - .src_color_blend_factor = gpu::BlendFactor::Zero, - .dst_color_blend_factor = gpu::BlendFactor::Zero, - .color_blend_op = gpu::BlendOp::Add, - .src_alpha_blend_factor = gpu::BlendFactor::Zero, - .dst_alpha_blend_factor = gpu::BlendFactor::Zero, - .alpha_blend_op = gpu::BlendOp::Add, - .color_write_mask = gpu::ColorComponents::All} + {.blend_enable = false, + .src_color_blend_factor = gpu::BlendFactor::Zero, + .dst_color_blend_factor = gpu::BlendFactor::Zero, + .color_blend_op = gpu::BlendOp::Add, + .src_alpha_blend_factor = gpu::BlendFactor::Zero, + .dst_alpha_blend_factor = gpu::BlendFactor::Zero, + .alpha_blend_op = gpu::BlendOp::Add, + .color_write_mask = gpu::ColorComponents::All} }; - gpu::ColorBlendState color_blend_state{ - .attachments = attachment_states, .blend_constant = {1, 1, 1, 1} - }; + gpu::ColorBlendState color_blend_state{.attachments = attachment_states, + .blend_constant = {}}; - gpu::DescriptorSetLayout set_layouts[] = {ctx.samplers_layout, - ctx.textures_layout}; + gpu::DescriptorSetLayout set_layouts[] = {gpu.samplers_layout, + gpu.textures_layout}; gpu::GraphicsPipelineInfo pipeline_info{ - .label = "Blur Graphics Pipeline"_str, - .vertex_shader = - gpu::ShaderStageInfo{.shader = vertex_shader, - .entry_point = "main"_str, - .specialization_constants = {}, - .specialization_constants_data = {}}, - .fragment_shader = - gpu::ShaderStageInfo{.shader = fragment_shader, - .entry_point = "main"_str, - .specialization_constants = {}, - .specialization_constants_data = {}}, - .color_formats = {&ctx.color_format, 1}, - .vertex_input_bindings = {}, - .vertex_attributes = {}, - .push_constants_size = sizeof(BlurParam), - .descriptor_set_layouts = set_layouts, - .primitive_topology = gpu::PrimitiveTopology::TriangleFan, - .rasterization_state = raster_state, - .depth_stencil_state = depth_stencil_state, - .color_blend_state = color_blend_state, - .cache = ctx.pipeline_cache + .label = "Blur Graphics Pipeline"_str, + .vertex_shader = gpu::ShaderStageInfo{.shader = vertex_shader, + .entry_point = "main"_str, + .specialization_constants = {}, + .specialization_constants_data = {}}, + .fragment_shader = + gpu::ShaderStageInfo{.shader = fragment_shader, + .entry_point = "main"_str, + .specialization_constants = {}, + .specialization_constants_data = {}}, + .color_formats = {&gpu.color_format, 1}, + .vertex_input_bindings = {}, + .vertex_attributes = {}, + .push_constants_size = sizeof(BlurParam), + .descriptor_set_layouts = set_layouts, + .primitive_topology = gpu::PrimitiveTopology::TriangleFan, + .rasterization_state = raster_state, + .depth_stencil_state = depth_stencil_state, + .color_blend_state = color_blend_state, + .cache = gpu.pipeline_cache }; downsample_pipeline = - ctx.device->create_graphics_pipeline(pipeline_info).unwrap(); + gpu.device->create_graphics_pipeline(pipeline_info).unwrap(); pipeline_info.vertex_shader.shader = assets.shaders["Blur/UpSample:VS"_str]; pipeline_info.fragment_shader.shader = assets.shaders["Blur/UpSample:FS"_str]; upsample_pipeline = - ctx.device->create_graphics_pipeline(pipeline_info).unwrap(); + gpu.device->create_graphics_pipeline(pipeline_info).unwrap(); } -void BlurPass::release(GpuContext & ctx, AssetMap &) +void BlurPass::release(GpuSystem & gpu, AssetMap &) { - ctx.device->uninit_graphics_pipeline(downsample_pipeline); - ctx.device->uninit_graphics_pipeline(upsample_pipeline); + gpu.device->uninit(downsample_pipeline); + gpu.device->uninit(upsample_pipeline); } -void sample(BlurPass & b, GpuContext & c, gpu::CommandEncoder & e, f32 rad, - gpu::DescriptorSet src_texture, u32 src_index, +void sample(BlurPass & b, GpuSystem & c, gpu::CommandEncoder & e, Vec2 radius, + gpu::DescriptorSet src_texture, TextureId src_id, gpu::Extent src_extent, gpu::Rect src_area, gpu::ImageView dst, Vec2U dst_offset, bool upsample) { - Vec2 radius = rad / as_vec2(src_extent); - Vec2 uv0 = as_vec2(src_area.offset) / as_vec2(src_extent); - Vec2 uv1 = as_vec2(src_area.end()) / as_vec2(src_extent); + radius /= as_vec2(src_extent); + Vec2 uv0 = as_vec2(src_area.offset) / as_vec2(src_extent); + Vec2 uv1 = as_vec2(src_area.end()) / as_vec2(src_extent); e.begin_rendering(gpu::RenderingInfo{ - .render_area = {.offset = dst_offset, .extent = src_area.extent}, - .num_layers = 1, - .color_attachments = span( - {gpu::RenderingAttachment{.view = dst, - .resolve = nullptr, - .resolve_mode = gpu::ResolveModes::None, - .load_op = gpu::LoadOp::Clear, - .store_op = gpu::StoreOp::Store}} - ), - .depth_attachment = {}, - .stencil_attachment = {} + .render_area = {.offset = dst_offset, .extent = src_area.extent}, + .num_layers = 1, + .color_attachments = + span({gpu::RenderingAttachment{.view = dst, + .resolve = nullptr, + .resolve_mode = gpu::ResolveModes::None, + .load_op = gpu::LoadOp::Load, + .store_op = gpu::StoreOp::Store}} + ), + .depth_attachment = {}, + .stencil_attachment = {} }); e.bind_graphics_pipeline(upsample ? b.upsample_pipeline : b.downsample_pipeline); e.set_graphics_state(gpu::GraphicsState{ - .scissor = {.offset = dst_offset, .extent = src_area.extent}, - .viewport = {.offset = as_vec2(dst_offset), - .extent = as_vec2(src_area.extent) } + .scissor = {.offset = dst_offset, .extent = src_area.extent}, + .viewport = {.offset = as_vec2(dst_offset), + .extent = as_vec2(src_area.extent) } }); e.bind_descriptor_sets(span({c.samplers, src_texture}), {}); e.push_constants(span({ - BlurParam{.uv = {uv0, uv1}, - .radius = radius, - .sampler = SAMPLER_LINEAR_CLAMPED, - .texture = src_index} + BlurParam{.uv = {uv0, uv1}, + .radius = radius, + .sampler = SamplerId::LinearClamped, + .texture = src_id} }) - .as_u8()); + .as_u8()); e.draw(4, 1, 0, 0); e.end_rendering(); } -void BlurPass::encode(GpuContext & ctx, gpu::CommandEncoder & e, +void BlurPass::encode(GpuSystem & gpu, gpu::CommandEncoder & e, BlurPassParams const & params) { - CHECK(params.passes > 0); - Vec2U extent = params.area.extent; - extent.x = min(extent.x, ctx.scratch_fbs[0].extent.x); - extent.y = min(extent.y, ctx.scratch_fbs[0].extent.y); - u32 radius = 1; + if (params.passes == 0) + { + return; + } - // downsample pass - sample(*this, ctx, e, (f32) radius, params.texture_view, params.texture, - params.extent, params.area, ctx.scratch_fbs[0].color.view, Vec2U{0, 0}, - false); + Framebuffer fbs[2] = {params.framebuffer, gpu.scratch_fb}; + gpu::Rect areas[2] = { + params.area, {.offset = {0, 0}, .extent = gpu.scratch_fb.extent()} + }; u32 src = 0; u32 dst = 1; - for (u32 i = 0; i < params.passes - 1; i++) + Vec2 const radius = params.radius / (f32) params.passes; + + // [ ] verify + + // downsample pass + for (u32 i = 0; i < params.passes; i++) { - radius += 1; - sample(*this, ctx, e, (f32) radius, ctx.scratch_fbs[src].color_texture, 0, - ctx.scratch_fbs[src].extent, - gpu::Rect{ - Vec2U{0, 0}, - params.area.extent - }, - ctx.scratch_fbs[dst].color.view, Vec2U{0, 0}, false); + sample(*this, gpu, e, radius * (f32) (i + 1), fbs[src].color.texture, + fbs[src].color.texture_id, areas[src].extent, areas[dst], + fbs[dst].color.view, areas[dst].offset, false); + src = (src + 1) & 1; dst = (dst + 1) & 1; } // upsample pass - for (u32 i = 0; i < params.passes - 1; i++) + for (u32 i = 0; i < params.passes; i++) { - sample(*this, ctx, e, (f32) radius, ctx.scratch_fbs[src].color_texture, 0, - ctx.scratch_fbs[src].extent, - gpu::Rect{ - Vec2U{0, 0}, - params.area.extent - }, - ctx.scratch_fbs[dst].color.view, Vec2U{0, 0}, true); + radius -= 1; + + sample(*this, gpu, e, (f32) radius, fbs[src].color.texture, + fbs[src].color.texture_id, areas[src].extent, areas[dst], + fbs[dst].color.view, areas[dst].offset, true); + src = (src + 1) & 1; dst = (dst + 1) & 1; - radius -= 1; } - - sample(*this, ctx, e, (f32) radius, ctx.scratch_fbs[src].color_texture, 0, - ctx.scratch_fbs[src].extent, - gpu::Rect{ - Vec2U{0, 0}, - params.area.extent - }, - params.image_view, params.area.offset, true); } -void NgonPass::acquire(GpuContext & ctx, AssetMap & assets) +void NgonPass::acquire(GpuSystem & gpu, AssetMap & assets) { gpu::Shader vertex_shader = assets.shaders["Ngon:VS"_str]; gpu::Shader fragment_shader = assets.shaders["Ngon:FS"_str]; @@ -222,16 +211,17 @@ void NgonPass::acquire(GpuContext & ctx, AssetMap & assets) .polygon_mode = gpu::PolygonMode::Fill, .cull_mode = gpu::CullMode::None, .front_face = - gpu::FrontFace::CounterClockWise, + gpu::FrontFace::CounterClockWise, .depth_bias_enable = false, .depth_bias_constant_factor = 0, .depth_bias_clamp = 0, - .depth_bias_slope_factor = 0}; + .depth_bias_slope_factor = 0, + .sample_count = gpu.sample_count}; gpu::DepthStencilState depth_stencil_state{.depth_test_enable = false, .depth_write_enable = false, .depth_compare_op = - gpu::CompareOp::Greater, + gpu::CompareOp::Greater, .depth_bounds_test_enable = false, .stencil_test_enable = false, .front_stencil = {}, @@ -240,79 +230,96 @@ void NgonPass::acquire(GpuContext & ctx, AssetMap & assets) .max_depth_bounds = 0}; gpu::ColorBlendAttachmentState attachment_states[] = { - {.blend_enable = true, - .src_color_blend_factor = gpu::BlendFactor::SrcAlpha, - .dst_color_blend_factor = gpu::BlendFactor::OneMinusSrcAlpha, - .color_blend_op = gpu::BlendOp::Add, - .src_alpha_blend_factor = gpu::BlendFactor::One, - .dst_alpha_blend_factor = gpu::BlendFactor::Zero, - .alpha_blend_op = gpu::BlendOp::Add, - .color_write_mask = gpu::ColorComponents::All} + {.blend_enable = true, + .src_color_blend_factor = gpu::BlendFactor::SrcAlpha, + .dst_color_blend_factor = gpu::BlendFactor::OneMinusSrcAlpha, + .color_blend_op = gpu::BlendOp::Add, + .src_alpha_blend_factor = gpu::BlendFactor::One, + .dst_alpha_blend_factor = gpu::BlendFactor::Zero, + .alpha_blend_op = gpu::BlendOp::Add, + .color_write_mask = gpu::ColorComponents::All} }; gpu::ColorBlendState color_blend_state{ - .attachments = attachment_states, .blend_constant = {1, 1, 1, 1} + .attachments = attachment_states, .blend_constant = {1, 1, 1, 1} }; gpu::DescriptorSetLayout set_layouts[] = { - ctx.ssbo_layout, ctx.ssbo_layout, ctx.ssbo_layout, ctx.samplers_layout, - ctx.textures_layout}; + gpu.ssbo_layout, gpu.ssbo_layout, gpu.ssbo_layout, gpu.samplers_layout, + gpu.textures_layout}; gpu::GraphicsPipelineInfo pipeline_info{ - .label = "Ngon Graphics Pipeline"_str, - .vertex_shader = - gpu::ShaderStageInfo{.shader = vertex_shader, - .entry_point = "main"_str, - .specialization_constants = {}, - .specialization_constants_data = {}}, - .fragment_shader = - gpu::ShaderStageInfo{.shader = fragment_shader, - .entry_point = "main"_str, - .specialization_constants = {}, - .specialization_constants_data = {}}, - .color_formats = {&ctx.color_format, 1}, - .vertex_input_bindings = {}, - .vertex_attributes = {}, - .push_constants_size = sizeof(Mat4), - .descriptor_set_layouts = set_layouts, - .primitive_topology = gpu::PrimitiveTopology::TriangleList, - .rasterization_state = raster_state, - .depth_stencil_state = depth_stencil_state, - .color_blend_state = color_blend_state, - .cache = ctx.pipeline_cache + .label = "Ngon Graphics Pipeline"_str, + .vertex_shader = gpu::ShaderStageInfo{.shader = vertex_shader, + .entry_point = "main"_str, + .specialization_constants = {}, + .specialization_constants_data = {}}, + .fragment_shader = + gpu::ShaderStageInfo{.shader = fragment_shader, + .entry_point = "main"_str, + .specialization_constants = {}, + .specialization_constants_data = {}}, + .color_formats = {&gpu.color_format, 1}, + .vertex_input_bindings = {}, + .vertex_attributes = {}, + .push_constants_size = sizeof(Mat4), + .descriptor_set_layouts = set_layouts, + .primitive_topology = gpu::PrimitiveTopology::TriangleList, + .rasterization_state = raster_state, + .depth_stencil_state = depth_stencil_state, + .color_blend_state = color_blend_state, + .cache = gpu.pipeline_cache }; - pipeline = ctx.device->create_graphics_pipeline(pipeline_info).unwrap(); + pipeline = gpu.device->create_graphics_pipeline(pipeline_info).unwrap(); } -void NgonPass::encode(GpuContext & ctx, gpu::CommandEncoder & e, +void NgonPass::encode(GpuSystem & gpu, gpu::CommandEncoder & e, NgonPassParams const & params) { - e.begin_rendering(params.rendering_info); + gpu::RenderingAttachment color[1]; + + if (params.framebuffer.color_msaa.is_some()) + { + color[0] = gpu::RenderingAttachment{ + .view = params.framebuffer.color_msaa.value().view, + .resolve = params.framebuffer.color.view, + .resolve_mode = gpu::ResolveModes::Average, + .load_op = gpu::LoadOp::DontCare, + .store_op = gpu::StoreOp::Store}; + } + else + { + color[0] = gpu::RenderingAttachment{.view = params.framebuffer.color.view}; + } + + gpu::RenderingInfo info{.render_area{.extent = params.framebuffer.extent()}, + .num_layers = 1, + .color_attachments = color}; + + e.begin_rendering(info); e.bind_graphics_pipeline(pipeline); e.bind_descriptor_sets( - - span({params.vertices_ssbo, params.indices_ssbo, params.params_ssbo, - ctx.samplers, params.textures}), - span({0, 0, 0})); + span({params.vertices_ssbo, params.indices_ssbo, params.params_ssbo, + gpu.samplers, params.textures}), + span({0, 0, 0})); e.push_constants(span({params.world_to_view}).as_u8()); - e.set_graphics_state(gpu::GraphicsState{.scissor = params.scissor, - .viewport = params.viewport}); - u32 const num_instances = params.index_counts.size32(); - for (u32 i = 0; i < num_instances; i++) + e.set_graphics_state( + gpu::GraphicsState{.scissor = params.scissor, .viewport = params.viewport}); + + for (auto [i, vertex_count] : enumerate(params.index_counts)) { - u32 vertex_count = params.index_counts[i]; - e.draw(vertex_count, 1, 0, i); + e.draw(vertex_count, 1, 0, params.first_instance + i); } e.end_rendering(); } -void NgonPass::release(GpuContext & ctx, AssetMap &) +void NgonPass::release(GpuSystem & gpu, AssetMap &) { - ctx.device->uninit_graphics_pipeline(pipeline); + gpu.device->uninit(pipeline); } -void PBRPass::acquire(GpuContext & ctx, AssetMap & assets) +void PBRPass::acquire(GpuSystem & gpu, AssetMap & assets) { gpu::Shader vertex_shader = assets.shaders["PBR:VS"_str]; gpu::Shader fragment_shader = assets.shaders["PBR:FS"_str]; @@ -321,16 +328,17 @@ void PBRPass::acquire(GpuContext & ctx, AssetMap & assets) .polygon_mode = gpu::PolygonMode::Fill, .cull_mode = gpu::CullMode::None, .front_face = - gpu::FrontFace::CounterClockWise, + gpu::FrontFace::CounterClockWise, .depth_bias_enable = false, .depth_bias_constant_factor = 0, .depth_bias_clamp = 0, - .depth_bias_slope_factor = 0}; + .depth_bias_slope_factor = 0, + .sample_count = gpu.sample_count}; gpu::DepthStencilState depth_stencil_state{.depth_test_enable = false, .depth_write_enable = false, .depth_compare_op = - gpu::CompareOp::Greater, + gpu::CompareOp::Greater, .depth_bounds_test_enable = false, .stencil_test_enable = false, .front_stencil = {}, @@ -339,88 +347,114 @@ void PBRPass::acquire(GpuContext & ctx, AssetMap & assets) .max_depth_bounds = 1}; gpu::ColorBlendAttachmentState attachment_states[] = { - {.blend_enable = false, - .src_color_blend_factor = gpu::BlendFactor::Zero, - .dst_color_blend_factor = gpu::BlendFactor::Zero, - .color_blend_op = gpu::BlendOp::Add, - .src_alpha_blend_factor = gpu::BlendFactor::Zero, - .dst_alpha_blend_factor = gpu::BlendFactor::Zero, - .alpha_blend_op = gpu::BlendOp::Add, - .color_write_mask = gpu::ColorComponents::All} + {.blend_enable = false, + .src_color_blend_factor = gpu::BlendFactor::Zero, + .dst_color_blend_factor = gpu::BlendFactor::Zero, + .color_blend_op = gpu::BlendOp::Add, + .src_alpha_blend_factor = gpu::BlendFactor::Zero, + .dst_alpha_blend_factor = gpu::BlendFactor::Zero, + .alpha_blend_op = gpu::BlendOp::Add, + .color_write_mask = gpu::ColorComponents::All} }; gpu::ColorBlendState color_blend_state{ - .attachments = attachment_states, .blend_constant = {1, 1, 1, 1} + .attachments = attachment_states, .blend_constant = {1, 1, 1, 1} }; gpu::DescriptorSetLayout const set_layouts[] = { - ctx.ssbo_layout, ctx.ssbo_layout, ctx.ssbo_layout, - ctx.ssbo_layout, ctx.samplers_layout, ctx.textures_layout}; + gpu.ssbo_layout, gpu.ssbo_layout, gpu.ssbo_layout, + gpu.ssbo_layout, gpu.samplers_layout, gpu.textures_layout}; gpu::GraphicsPipelineInfo pipeline_info{ - .label = "PBR Graphics Pipeline"_str, - .vertex_shader = - gpu::ShaderStageInfo{.shader = vertex_shader, - .entry_point = "main"_str, - .specialization_constants = {}, - .specialization_constants_data = {}}, - .fragment_shader = - gpu::ShaderStageInfo{.shader = fragment_shader, - .entry_point = "main"_str, - .specialization_constants = {}, - .specialization_constants_data = {}}, - .color_formats = {&ctx.color_format, 1}, - .depth_format = {&ctx.depth_stencil_format, 1}, - .vertex_input_bindings = {}, - .vertex_attributes = {}, - .push_constants_size = sizeof(Mat4), - .descriptor_set_layouts = set_layouts, - .primitive_topology = gpu::PrimitiveTopology::TriangleList, - .rasterization_state = raster_state, - .depth_stencil_state = depth_stencil_state, - .color_blend_state = color_blend_state, - .cache = ctx.pipeline_cache + .label = "PBR Graphics Pipeline"_str, + .vertex_shader = gpu::ShaderStageInfo{.shader = vertex_shader, + .entry_point = "main"_str, + .specialization_constants = {}, + .specialization_constants_data = {}}, + .fragment_shader = + gpu::ShaderStageInfo{.shader = fragment_shader, + .entry_point = "main"_str, + .specialization_constants = {}, + .specialization_constants_data = {}}, + .color_formats = {&gpu.color_format, 1}, + .depth_format = {&gpu.depth_stencil_format, 1}, + .vertex_input_bindings = {}, + .vertex_attributes = {}, + .push_constants_size = sizeof(Mat4), + .descriptor_set_layouts = set_layouts, + .primitive_topology = gpu::PrimitiveTopology::TriangleList, + .rasterization_state = raster_state, + .depth_stencil_state = depth_stencil_state, + .color_blend_state = color_blend_state, + .cache = gpu.pipeline_cache }; - pipeline = ctx.device->create_graphics_pipeline(pipeline_info).unwrap(); + pipeline = gpu.device->create_graphics_pipeline(pipeline_info).unwrap(); pipeline_info.rasterization_state.polygon_mode = gpu::PolygonMode::Line; wireframe_pipeline = - ctx.device->create_graphics_pipeline(pipeline_info).unwrap(); + gpu.device->create_graphics_pipeline(pipeline_info).unwrap(); } -void PBRPass::encode(GpuContext & ctx, gpu::CommandEncoder & e, +void PBRPass::encode(GpuSystem & gpu, gpu::CommandEncoder & e, PBRPassParams const & params) { - e.begin_rendering(params.rendering_info); + gpu::RenderingAttachment color[1]; + + if (params.framebuffer.color_msaa.is_some()) + { + color[0] = gpu::RenderingAttachment{ + .view = params.framebuffer.color_msaa.value().view, + .resolve = params.framebuffer.color.view, + .resolve_mode = gpu::ResolveModes::Average, + .load_op = gpu::LoadOp::DontCare, + .store_op = gpu::StoreOp::Store}; + } + else + { + color[0] = gpu::RenderingAttachment{.view = params.framebuffer.color.view}; + } + + gpu::RenderingAttachment depth[] = { + {.view = params.framebuffer.depth.view, + .load_op = gpu::LoadOp::Load, + .store_op = gpu::StoreOp::Store} + }; + + gpu::RenderingInfo info{.render_area{.extent = params.framebuffer.extent()}, + .num_layers = 1, + .color_attachments = color, + .depth_attachment = depth}; + + e.begin_rendering(info); e.bind_graphics_pipeline(params.wireframe ? this->wireframe_pipeline : this->pipeline); e.set_graphics_state(gpu::GraphicsState{ - .scissor = params.scissor, - .viewport = params.viewport, - .blend_constant = {1, 1, 1, 1}, - .depth_test_enable = true, - .depth_compare_op = gpu::CompareOp::Less, - .depth_write_enable = true + .scissor = params.scissor, + .viewport = params.viewport, + .blend_constant = {1, 1, 1, 1}, + .depth_test_enable = true, + .depth_compare_op = gpu::CompareOp::Less, + .depth_write_enable = true }); e.bind_descriptor_sets( - span({params.vertices_ssbo, params.indices_ssbo, params.params_ssbo, - params.lights_ssbo, ctx.samplers, params.textures}), - span({0, 0, 0, 0})); + span({params.vertices_ssbo, params.indices_ssbo, params.params_ssbo, + params.lights_ssbo, gpu.samplers, params.textures}), + span({0, 0, 0, 0})); e.push_constants(span({params.world_to_view}).as_u8()); e.draw(params.num_indices, 1, 0, params.instance); e.end_rendering(); } -void PBRPass::release(GpuContext & ctx, AssetMap &) +void PBRPass::release(GpuSystem & gpu, AssetMap &) { - ctx.device->uninit_graphics_pipeline(pipeline); - ctx.device->uninit_graphics_pipeline(wireframe_pipeline); + gpu.device->uninit(pipeline); + gpu.device->uninit(wireframe_pipeline); } -void RRectPass::acquire(GpuContext & ctx, AssetMap & assets) +void RRectPass::acquire(GpuSystem & gpu, AssetMap & assets) { gpu::Shader vertex_shader = assets.shaders["RRect:VS"_str]; gpu::Shader fragment_shader = assets.shaders["RRect:FS"_str]; @@ -429,16 +463,17 @@ void RRectPass::acquire(GpuContext & ctx, AssetMap & assets) .polygon_mode = gpu::PolygonMode::Fill, .cull_mode = gpu::CullMode::None, .front_face = - gpu::FrontFace::CounterClockWise, + gpu::FrontFace::CounterClockWise, .depth_bias_enable = false, .depth_bias_constant_factor = 0, .depth_bias_clamp = 0, - .depth_bias_slope_factor = 0}; + .depth_bias_slope_factor = 0, + .sample_count = gpu.sample_count}; gpu::DepthStencilState depth_stencil_state{.depth_test_enable = false, .depth_write_enable = false, .depth_compare_op = - gpu::CompareOp::Greater, + gpu::CompareOp::Greater, .depth_bounds_test_enable = false, .stencil_test_enable = false, .front_stencil = {}, @@ -447,68 +482,86 @@ void RRectPass::acquire(GpuContext & ctx, AssetMap & assets) .max_depth_bounds = 0}; gpu::ColorBlendAttachmentState attachment_states[] = { - {.blend_enable = true, - .src_color_blend_factor = gpu::BlendFactor::SrcAlpha, - .dst_color_blend_factor = gpu::BlendFactor::OneMinusSrcAlpha, - .color_blend_op = gpu::BlendOp::Add, - .src_alpha_blend_factor = gpu::BlendFactor::One, - .dst_alpha_blend_factor = gpu::BlendFactor::Zero, - .alpha_blend_op = gpu::BlendOp::Add, - .color_write_mask = gpu::ColorComponents::All} + {.blend_enable = true, + .src_color_blend_factor = gpu::BlendFactor::SrcAlpha, + .dst_color_blend_factor = gpu::BlendFactor::OneMinusSrcAlpha, + .color_blend_op = gpu::BlendOp::Add, + .src_alpha_blend_factor = gpu::BlendFactor::One, + .dst_alpha_blend_factor = gpu::BlendFactor::Zero, + .alpha_blend_op = gpu::BlendOp::Add, + .color_write_mask = gpu::ColorComponents::All} }; gpu::ColorBlendState color_blend_state{ - .attachments = attachment_states, .blend_constant = {1, 1, 1, 1} + .attachments = attachment_states, .blend_constant = {1, 1, 1, 1} }; gpu::DescriptorSetLayout set_layouts[] = { - ctx.ssbo_layout, ctx.samplers_layout, ctx.textures_layout}; + gpu.ssbo_layout, gpu.samplers_layout, gpu.textures_layout}; gpu::GraphicsPipelineInfo pipeline_info{ - .label = "RRect Graphics Pipeline"_str, - .vertex_shader = - gpu::ShaderStageInfo{.shader = vertex_shader, - .entry_point = "main"_str, - .specialization_constants = {}, - .specialization_constants_data = {}}, - .fragment_shader = - gpu::ShaderStageInfo{.shader = fragment_shader, - .entry_point = "main"_str, - .specialization_constants = {}, - .specialization_constants_data = {}}, - .color_formats = {&ctx.color_format, 1}, - .vertex_input_bindings = {}, - .vertex_attributes = {}, - .push_constants_size = sizeof(Mat4), - .descriptor_set_layouts = set_layouts, - .primitive_topology = gpu::PrimitiveTopology::TriangleFan, - .rasterization_state = raster_state, - .depth_stencil_state = depth_stencil_state, - .color_blend_state = color_blend_state, - .cache = ctx.pipeline_cache + .label = "RRect Graphics Pipeline"_str, + .vertex_shader = gpu::ShaderStageInfo{.shader = vertex_shader, + .entry_point = "main"_str, + .specialization_constants = {}, + .specialization_constants_data = {}}, + .fragment_shader = + gpu::ShaderStageInfo{.shader = fragment_shader, + .entry_point = "main"_str, + .specialization_constants = {}, + .specialization_constants_data = {}}, + .color_formats = {&gpu.color_format, 1}, + .vertex_input_bindings = {}, + .vertex_attributes = {}, + .push_constants_size = sizeof(Mat4), + .descriptor_set_layouts = set_layouts, + .primitive_topology = gpu::PrimitiveTopology::TriangleFan, + .rasterization_state = raster_state, + .depth_stencil_state = depth_stencil_state, + .color_blend_state = color_blend_state, + .cache = gpu.pipeline_cache }; - pipeline = ctx.device->create_graphics_pipeline(pipeline_info).unwrap(); + pipeline = gpu.device->create_graphics_pipeline(pipeline_info).unwrap(); } -void RRectPass::encode(GpuContext & ctx, gpu::CommandEncoder & e, +void RRectPass::encode(GpuSystem & gpu, gpu::CommandEncoder & e, RRectPassParams const & params) { - e.begin_rendering(params.rendering_info); + gpu::RenderingAttachment color[1]; + + if (params.framebuffer.color_msaa.is_some()) + { + color[0] = gpu::RenderingAttachment{ + .view = params.framebuffer.color_msaa.value().view, + .resolve = params.framebuffer.color.view, + .resolve_mode = gpu::ResolveModes::Average, + .load_op = gpu::LoadOp::DontCare, + .store_op = gpu::StoreOp::Store}; + } + else + { + color[0] = gpu::RenderingAttachment{.view = params.framebuffer.color.view}; + } + + gpu::RenderingInfo info{.render_area{.extent = params.framebuffer.extent()}, + .num_layers = 1, + .color_attachments = color}; + + e.begin_rendering(info); e.bind_graphics_pipeline(pipeline); - e.set_graphics_state(gpu::GraphicsState{.scissor = params.scissor, - .viewport = params.viewport}); + e.set_graphics_state( + gpu::GraphicsState{.scissor = params.scissor, .viewport = params.viewport}); e.bind_descriptor_sets( - span({params.params_ssbo, ctx.samplers, params.textures}), - span({0})); + span({params.params_ssbo, gpu.samplers, params.textures}), span({0})); e.push_constants(span({params.world_to_view}).as_u8()); e.draw(4, params.num_instances, 0, params.first_instance); e.end_rendering(); } -void RRectPass::release(GpuContext & ctx, AssetMap &) +void RRectPass::release(GpuSystem & gpu, AssetMap &) { - ctx.device->uninit_graphics_pipeline(pipeline); + gpu.device->uninit(pipeline); } -} // namespace ash +} // namespace ash diff --git a/ashura/engine/passes.h b/ashura/engine/passes.h index c30a96070..8a2383a89 100644 --- a/ashura/engine/passes.h +++ b/ashura/engine/passes.h @@ -1,7 +1,7 @@ /// SPDX-License-Identifier: MIT #pragma once #include "ashura/engine/assets.h" -#include "ashura/engine/gpu_context.h" +#include "ashura/engine/gpu_system.h" #include "ashura/gpu/gpu.h" #include "ashura/std/types.h" @@ -13,12 +13,13 @@ namespace ash /// data needed for executing rendering operations. Passes dispatch /// compute/graphics shaders using their specified arguments. They are mostly /// used by renderers. +// [ ] cache setup struct Pass { - virtual Span id() = 0; - virtual void acquire(GpuContext &, AssetMap &) = 0; - virtual void release(GpuContext &, AssetMap &) = 0; - virtual ~Pass() = default; + virtual Span id() = 0; + virtual void acquire(GpuSystem &, AssetMap &) = 0; + virtual void release(GpuSystem &, AssetMap &) = 0; + virtual ~Pass() = default; }; struct BloomPassParams @@ -33,37 +34,35 @@ struct BloomPass : Pass { BloomPass() = default; + virtual ~BloomPass() override = default; + virtual Span id() override { return "Bloom"_str; } - virtual void acquire(GpuContext & ct, AssetMap & assets) override; + virtual void acquire(GpuSystem & ct, AssetMap & assets) override; - virtual void release(GpuContext & ctx, AssetMap & assets) override; + virtual void release(GpuSystem & gpu, AssetMap & assets) override; - void encode(GpuContext & ctx, gpu::CommandEncoder & encoder, + void encode(GpuSystem & gpu, gpu::CommandEncoder & encoder, BloomPassParams const & params); - - virtual ~BloomPass() override = default; }; struct BlurParam { - Vec2 uv[2] = {}; - Vec2 radius = {}; - u32 sampler = 0; - u32 texture = 0; + Vec2 uv[2] = {}; + Vec2 radius = {}; + SamplerId sampler = SamplerId::Linear; + TextureId texture = TextureId::White; }; struct BlurPassParams { - gpu::ImageView image_view = nullptr; - Vec2U extent = {}; - gpu::DescriptorSet texture_view = nullptr; - u32 texture = 0; - u32 passes = 1; - gpu::Rect area = {}; + Framebuffer framebuffer = {}; + Vec2 radius = {1, 1}; + u32 passes = 1; + gpu::Rect area = {}; }; struct BlurPass : Pass @@ -73,37 +72,36 @@ struct BlurPass : Pass BlurPass() = default; + virtual ~BlurPass() override = default; + virtual Span id() override { return "Blur"_str; } - virtual void acquire(GpuContext & ctx, AssetMap & assets) override; + virtual void acquire(GpuSystem & gpu, AssetMap & assets) override; - virtual void release(GpuContext & ctx, AssetMap & assets) override; + virtual void release(GpuSystem & gpu, AssetMap & assets) override; - virtual ~BlurPass() override = default; - - void encode(GpuContext & ctx, gpu::CommandEncoder & encoder, + void encode(GpuSystem & gpu, gpu::CommandEncoder & encoder, BlurPassParams const & params); }; -/// @param transform needs to transform from [-1, +1] to clip space struct NgonParam { - Mat4 transform = {}; - Vec4 tint[4] = {}; - Vec2 uv[2] = {}; - f32 tiling = 1; - u32 sampler = 0; - u32 albedo = 0; - u32 first_index = 0; - u32 first_vertex = 0; + Mat4 transform = {}; + Vec4 tint[4] = {}; + Vec2 uv[2] = {}; + f32 tiling = 1; + SamplerId sampler = SamplerId::Linear; + TextureId albedo = TextureId::White; + u32 first_index = 0; + u32 first_vertex = 0; }; struct NgonPassParams { - gpu::RenderingInfo rendering_info = {}; + Framebuffer framebuffer = {}; gpu::Rect scissor = {}; gpu::Viewport viewport = {}; Mat4 world_to_view = {}; @@ -111,6 +109,7 @@ struct NgonPassParams gpu::DescriptorSet indices_ssbo = nullptr; gpu::DescriptorSet params_ssbo = nullptr; gpu::DescriptorSet textures = nullptr; + u32 first_instance = 0; Span index_counts = {}; }; @@ -120,50 +119,50 @@ struct NgonPass : Pass NgonPass() = default; + virtual ~NgonPass() override = default; + virtual Span id() override { return "Ngon"_str; } - virtual void acquire(GpuContext & ctx, AssetMap & assets) override; - - virtual void release(GpuContext & ctx, AssetMap & assets) override; + virtual void acquire(GpuSystem & gpu, AssetMap & assets) override; - virtual ~NgonPass() override = default; + virtual void release(GpuSystem & gpu, AssetMap & assets) override; - void encode(GpuContext & ctx, gpu::CommandEncoder & encoder, + void encode(GpuSystem & gpu, gpu::CommandEncoder & encoder, NgonPassParams const & params); }; -/// @see https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos +/// @see https://github.com/KhronosGroup/glTF/tree/acfcbe65e40c53d6d3aa55a7299982bf2c01c75d/extensions/2.0/Khronos /// @see -/// https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/main/source/Renderer/shaders/textures.glsl +/// https://github.com/KhronosGroup/glTF-Sample-Renderer/blob/63b7c128266cfd86bbd3f25caf8b3db3fe854015/source/Renderer/shaders/textures.glsl#L1 struct PBRParam { - Mat4 transform = {}; - Vec4 eye_position = {0, 0, 0, 0}; - Vec4 albedo = {1, 1, 1, 1}; - f32 metallic = 0; - f32 roughness = 0; - f32 normal = 0; - f32 occlusion = 0; - Vec4 emissive = {0, 0, 0, 0}; - f32 ior = 1.5F; - f32 clearcoat = 0; - f32 clearcoat_roughness = 0; - f32 clearcoat_normal = 0; - u32 sampler = 0; - u32 albedo_map = 0; - u32 metallic_map = 0; - u32 roughness_map = 0; - u32 normal_map = 0; - u32 occlusion_map = 0; - u32 emissive_map = 0; - u32 clearcoat_map = 0; - u32 clearcoat_roughness_map = 0; - u32 clearcoat_normal_map = 0; - u32 first_vertex = 0; - u32 first_light = 0; + Mat4 transform = {}; + Vec4 eye_position = {0, 0, 0, 0}; + Vec4 albedo = {1, 1, 1, 1}; + f32 metallic = 0; + f32 roughness = 0; + f32 normal = 0; + f32 occlusion = 0; + Vec4 emissive = {0, 0, 0, 0}; + f32 ior = 1.5F; + f32 clearcoat = 0; + f32 clearcoat_roughness = 0; + f32 clearcoat_normal = 0; + SamplerId sampler = SamplerId::Linear; + TextureId albedo_map = TextureId::White; + TextureId metallic_map = TextureId::White; + TextureId roughness_map = TextureId::White; + TextureId normal_map = TextureId::White; + TextureId occlusion_map = TextureId::White; + TextureId emissive_map = TextureId::White; + TextureId clearcoat_map = TextureId::White; + TextureId clearcoat_roughness_map = TextureId::White; + TextureId clearcoat_normal_map = TextureId::White; + u32 first_vertex = 0; + u32 first_light = 0; }; struct PBRVertex @@ -174,18 +173,18 @@ struct PBRVertex struct PBRPassParams { - gpu::RenderingInfo rendering_info = {}; - gpu::Rect scissor = {}; - gpu::Viewport viewport = {}; - Mat4 world_to_view = {}; - bool wireframe = false; - gpu::DescriptorSet vertices_ssbo = nullptr; - gpu::DescriptorSet indices_ssbo = nullptr; - gpu::DescriptorSet params_ssbo = nullptr; - gpu::DescriptorSet lights_ssbo = nullptr; - gpu::DescriptorSet textures = nullptr; - u32 instance = 0; - u32 num_indices = 0; + Framebuffer framebuffer = {}; + gpu::Rect scissor = {}; + gpu::Viewport viewport = {}; + Mat4 world_to_view = {}; + bool wireframe = false; + gpu::DescriptorSet vertices_ssbo = nullptr; + gpu::DescriptorSet indices_ssbo = nullptr; + gpu::DescriptorSet params_ssbo = nullptr; + gpu::DescriptorSet lights_ssbo = nullptr; + gpu::DescriptorSet textures = nullptr; + u32 instance = 0; + u32 num_indices = 0; }; struct PBRPass : Pass @@ -195,40 +194,39 @@ struct PBRPass : Pass PBRPass() = default; + virtual ~PBRPass() override = default; + virtual Span id() override { return "PBR"_str; } - virtual void acquire(GpuContext & ctx, AssetMap & assets) override; - - virtual void release(GpuContext & ctx, AssetMap & assets) override; + virtual void acquire(GpuSystem & gpu, AssetMap & assets) override; - virtual ~PBRPass() override = default; + virtual void release(GpuSystem & gpu, AssetMap & assets) override; - void encode(GpuContext & ctx, gpu::CommandEncoder & encoder, + void encode(GpuSystem & gpu, gpu::CommandEncoder & encoder, PBRPassParams const & params); }; -/// @param transform needs to transform from [-1, +1] to clip space struct RRectParam { - Mat4 transform = {}; - Vec4 tint[4] = {}; - Vec4 radii = {}; - Vec2 uv[2] = {}; - f32 tiling = 1; - f32 aspect_ratio = 1; - f32 stroke = 0; - f32 thickness = 0; - f32 edge_smoothness = 0.0015F; - u32 sampler = 0; - u32 albedo = 0; + Mat4 transform = {}; + Vec4 tint[4] = {}; + Vec4 radii = {}; + Vec2 uv[2] = {}; + f32 tiling = 1; + f32 aspect_ratio = 1; + f32 stroke = 0; + f32 thickness = 0; + f32 edge_smoothness = 0; + SamplerId sampler = SamplerId::Linear; + TextureId albedo = TextureId::White; }; struct RRectPassParams { - gpu::RenderingInfo rendering_info = {}; + Framebuffer framebuffer = {}; gpu::Rect scissor = {}; gpu::Viewport viewport = {}; Mat4 world_to_view = {}; @@ -249,14 +247,14 @@ struct RRectPass : Pass RRectPass() = default; - virtual void acquire(GpuContext & ctx, AssetMap & assets) override; + virtual ~RRectPass() override = default; - virtual void release(GpuContext & ctx, AssetMap & assets) override; + virtual void acquire(GpuSystem & gpu, AssetMap & assets) override; - virtual ~RRectPass() override = default; + virtual void release(GpuSystem & gpu, AssetMap & assets) override; - void encode(GpuContext & ctx, gpu::CommandEncoder & encoder, + void encode(GpuSystem & gpu, gpu::CommandEncoder & encoder, RRectPassParams const & params); }; -} // namespace ash +} // namespace ash diff --git a/ashura/engine/rect_pack.h b/ashura/engine/rect_pack.h index 98113fa4d..77f0b64a9 100644 --- a/ashura/engine/rect_pack.h +++ b/ashura/engine/rect_pack.h @@ -1,600 +1,305 @@ -// ------------------------------------------------------------------------------ -// This software is available under 2 licenses -- choose whichever you prefer. -// ------------------------------------------------------------------------------ -// ALTERNATIVE A - MIT License -// Copyright (c) 2017 Sean Barrett -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this 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. 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. -// ------------------------------------------------------------------------------ -// ALTERNATIVE B - Public Domain (www.unlicense.org) -// This is free and unencumbered software released into the public domain. -// Anyone is free to copy, modify, publish, use, compile, sell, or distribute -// this software, either in source code form or as a compiled binary, for any -// purpose, commercial or non-commercial, and by any means. In jurisdictions -// that recognize copyright laws, the author or authors of this software -// dedicate any and all copyright interest in the software to the public domain. -// We make this dedication for the benefit of the public at large and to the -// detriment of our heirs and successors. We intend this dedication to be an -// overt act of relinquishment in perpetuity of all present and future rights to -// this software under copyright law. -// 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 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. -// ------------------------------------------------------------------------------ -// -// stb_rect_pack.h - v1.01 - public domain - rectangle packing -// Sean Barrett 2014 -// -// Useful for e.g. packing rectangular textures into an atlas. -// Does not do rotation. -// -// Before #including, -// -// #define STB_RECT_PACK_IMPLEMENTATION -// -// in the file that you want to have the implementation. -// -// Not necessarily the awesomest packing method, but better than -// the totally naive one in stb_truetype (which is primarily what -// this is meant to replace). -// -// Has only had a few tests run, may have issues. -// -// More docs to come. -// -// This library currently uses the Skyline Bottom-Left algorithm. -// -// Please note: better rectangle packers are welcome! Please -// implement them to the same API, but with a different init -// function. -// -// Credits -// -// Library -// Sean Barrett -// Minor features -// Martins Mozeiko -// github:IntellectualKitty -// -// Bugfixes / warning fixes -// Jeremy Jaussaud -// Fabian Giesen -// -// Version history: -// -// 1.01 (2021-07-11) always use large rect mode, expose STBRP__MAXVAL in -// public section 1.00 (2019-02-25) avoid small space waste; gracefully -// fail too-wide rectangles 0.99 (2019-02-07) warning fixes 0.11 -// (2017-03-03) return packing success/fail result 0.10 (2016-10-25) -// remove cast-away-const to avoid warnings 0.09 (2016-08-27) fix compiler -// warnings 0.08 (2015-09-13) really fix bug with empty rects (w=0 or h=0) -// 0.07 (2015-09-13) fix bug with empty rects (w=0 or h=0) -// 0.06 (2015-04-15) added STBRP_SORT to allow replacing qsort -// 0.05: added STBRP_ASSERT to allow replacing assert -// 0.04: fixed minor bug in STBRP_LARGE_RECTS support -// 0.01: initial release -// -// LICENSE -// -// See end of file for license information. - +/// SPDX-License-Identifier: MIT #pragma once #include "ashura/std/error.h" +#include "ashura/std/option.h" +#include "ashura/std/range.h" #include "ashura/std/types.h" -#include +#include "ashura/std/vec.h" namespace ash { -namespace rect_pack -{ -struct rect +struct PackRect { - u32 glyph_index = 0; + Vec2I pos{}; - // input: - i32 w = 0; - i32 h = 0; + Vec2I extent{}; - // output: - i32 x = 0; - i32 y = 0; - u32 layer = 0; + // true if packed + bool32 packed = false; - // non-zero if valid packing - i32 was_packed = 0; + u32 id = 0; }; -constexpr int rect_height_compare(void const * a, void const * b) +struct RectPacker { - rect const * p = (rect const *) a; - rect const * q = (rect const *) b; - if (p->h > q->h) - return -1; - if (p->h < q->h) - return 1; - return (p->w > q->w) ? -1 : (p->w < q->w); -} - -constexpr int rect_original_order(void const * a, void const * b) -{ - rect const * p = (rect const *) a; - rect const * q = (rect const *) b; - return (p->was_packed < q->was_packed) ? -1 : (p->was_packed > q->was_packed); -} + struct Node + { + Vec2I pos{}; + Node * next = nullptr; + }; -////////////////////////////////////////////////////////////////////////////// -// -// the details of the following structures don't matter to you, but they must -// be visible so you can handle the memory allocations for them + struct FindResult + { + Vec2I pos{}; + Node ** prev_link = nullptr; + }; -struct Node -{ - i32 x = 0, y = 0; - Node * next = nullptr; -}; + Vec2I extent{}; -enum class Heuristic -{ - BL_sortHeight, - BF_sortHeight -}; + Node * active_head = nullptr; -enum class Mode -{ - Default, - InitSkyline -}; + Node * free_head = nullptr; -struct Context -{ - i32 width = 0, height = 0; - i32 align = 0; - Mode init_mode = Mode::Default; - Heuristic heuristic = Heuristic::BL_sortHeight; - i32 num_nodes = 0; - Node * active_head = nullptr; - Node * free_head = nullptr; - // we allocate two extra nodes so optimal - // user-node-count is 'width' not 'width+2' - Node extra[2]; -}; + Vec nodes{}; -// find minimum y position if it starts at x1 -inline i32 skyline_find_min_y(Context & c, Node * first, i32 x0, i32 width, - i32 * pwaste) -{ - Node * node = first; - i32 x1 = x0 + width; - i32 min_y, visited_width, waste_area; + /// @brief Make a rectangle packer to pack a rectangle that is 'width' by 'height' in dimensions. + static RectPacker make(Vec2I extent, AllocatorImpl allocator) + { + RectPacker packer{.nodes{allocator}}; - (void) c; + packer.reset(extent); - CHECK(first->x <= x0); + return packer; + } -#if 0 - // skip in case we're past the node - while (node->next->x <= x0) - ++node; -#else - // we ended up handling this in the caller for efficiency - CHECK(node->next->x > x0); -#endif + // You must call this function every time you start packing into a new target. + void reset(Vec2I extent) + { + i32 const num_nodes = extent.x + 2; - CHECK(node->x <= x0); + nodes.resize(num_nodes).unwrap(); - min_y = 0; - waste_area = 0; - visited_width = 0; - while (node->x < x1) - { - if (node->y > min_y) - { - // raise min_y higher. - // we've accounted for all waste up to min_y, - // but we'll now add more waste for everything we've visted - waste_area += visited_width * (node->y - min_y); - min_y = node->y; - // the first time through, visited_width might be reduced - if (node->x < x0) - visited_width += node->next->x - x0; - else - visited_width += node->next->x - node->x; - } - else + // the full width node + nodes[0] = Node{.pos{}, .next = &nodes[1]}; + + // the sentinel node (lets us not store width explicitly) + nodes[1] = Node{ + .pos{extent.x, (1 << 30)}, + .next = nullptr + }; + + // link the nodes + for (i32 i = 2; i < num_nodes - 1; i++) { - // add waste area - i32 under_width = node->next->x - node->x; - if (under_width + visited_width > width) - under_width = width - visited_width; - waste_area += under_width * (min_y - node->y); - visited_width += under_width; + nodes[i].next = &nodes[i + 1]; } - node = node->next; - } - *pwaste = waste_area; - return min_y; -} + nodes[num_nodes - 1].next = nullptr; -struct FindResult -{ - i32 x = 0, y = 0; - Node ** prev_link = nullptr; -}; + this->extent = extent; + this->active_head = &nodes[0]; + this->free_head = &nodes[2]; + } -inline FindResult skyline_find_best_pos(Context & ctx, i32 width, i32 height) -{ - i32 best_waste = (1 << 30), best_x, best_y = (1 << 30); - FindResult find_result; - Node ** prev, *node, *tail, **best = nullptr; + // find minimum y position if it starts at x1 + static Tuple find_min_y(Node & first, i32 x0, i32 width) + { + CHECK(first.pos.x <= x0); - // align to multiple of ctx.align - width = (width + ctx.align - 1); - width -= width % ctx.align; - CHECK(width % ctx.align == 0); + // we ended up handling this in the caller for efficiency + Node * iter = &first; + CHECK(iter->next->pos.x > x0); + CHECK(iter->pos.x <= x0); - // if it can't possibly fit, bail immediately - if (width > ctx.width || height > ctx.height) - { - find_result.prev_link = nullptr; - find_result.x = find_result.y = 0; - return find_result; - } + i32 min_y = 0; + i32 waste_area = 0; + i32 visited_width = 0; + i32 const x1 = x0 + width; - node = ctx.active_head; - prev = &ctx.active_head; - while (node->x + width <= ctx.width) - { - i32 y, waste; - y = skyline_find_min_y(ctx, node, node->x, width, &waste); - if (ctx.heuristic == Heuristic::BL_sortHeight) - { // actually just want - // to test BL - // bottom left - if (y < best_y) + while (iter->pos.x < x1) + { + if (iter->pos.y > min_y) { - best_y = y; - best = prev; + // raise min_y higher. we've accounted for all waste up to min_y, + // but we'll now add more waste for everything we've visted + waste_area += visited_width * (iter->pos.y - min_y); + min_y = iter->pos.y; + // the first time through, visited_width might be reduced + visited_width += + iter->next->pos.x - ((iter->pos.x < x0) ? x0 : iter->pos.x); } - } - else - { - // best-fit - if (y + height <= ctx.height) + else { - // can only use it if it first vertically - if (y < best_y || (y == best_y && waste < best_waste)) + // add waste area + i32 under_width = iter->next->pos.x - iter->pos.x; + if (under_width + visited_width > width) { - best_y = y; - best_waste = waste; - best = prev; + under_width = width - visited_width; } + waste_area += under_width * (min_y - iter->pos.y); + visited_width += under_width; } + iter = iter->next; } - prev = &node->next; - node = node->next; + + return {min_y, waste_area}; } - best_x = (best == nullptr) ? 0 : (*best)->x; + Option find_best_pos(Vec2I size) + { + // if it can't possibly fit, bail immediately + if (size.x > extent.x || size.y > extent.y) + { + return none; + } - // if doing best-fit (BF), we also have to try aligning right edge to each - // node position - // - // e.g, if fitting - // - // ____________________ - // |____________________| - // - // into - // - // | | - // | ____________| - // |____________| - // - // then right-aligned reduces waste, but bottom-left BL is always chooses - // left-aligned - // - // This makes BF take about 2x the time + Node * node = active_head; + Node ** prev = &active_head; + Node ** best = nullptr; + i32 best_y = (1 << 30); - if (ctx.heuristic == Heuristic::BF_sortHeight) - { - tail = ctx.active_head; - node = ctx.active_head; - prev = &ctx.active_head; - // find first node that's admissible - while (tail->x < width) - tail = tail->next; - while (tail) + while (node->pos.x + size.x <= extent.x) { - i32 xpos = tail->x - width; - i32 y, waste; - CHECK(xpos >= 0); - // find the left position that matches this - while (node->next->x <= xpos) - { - prev = &node->next; - node = node->next; - } - CHECK(node->next->x > xpos && node->x <= xpos); - y = skyline_find_min_y(ctx, node, xpos, width, &waste); - if (y + height <= ctx.height) + auto [y, waste] = find_min_y(*node, node->pos.x, extent.x); + + // actually just want to test BL bottom left + if (y < best_y) { - if (y <= best_y) - { - if (y < best_y || waste < best_waste || - (waste == best_waste && xpos < best_x)) - { - best_x = xpos; - CHECK(y <= best_y); - best_y = y; - best_waste = waste; - best = prev; - } - } + best_y = y; + best = prev; } - tail = tail->next; + + prev = &node->next; + node = node->next; + } + + if (best == nullptr) + { + return none; } - } - find_result.prev_link = best; - find_result.x = best_x; - find_result.y = best_y; - return find_result; -} + i32 const best_x = (*best)->pos.x; -inline FindResult skyline_pack_rectangle(Context & ctx, i32 width, i32 height) -{ - // find best position according to heuristic - FindResult res = skyline_find_best_pos(ctx, width, height); - Node * node, *cur; - - // bail if: - // 1. it failed - // 2. the best node doesn't fit (we don't always check this) - // 3. we're out of memory - if (res.prev_link == nullptr || res.y + height > ctx.height || - ctx.free_head == nullptr) - { - res.prev_link = nullptr; - return res; + return FindResult{ + .pos{best_x, best_y}, + .prev_link = best + }; } - // on success, create new node - node = ctx.free_head; - node->x = res.x; - node->y = res.y + height; + Option pack_rect(Vec2I size) + { + // find best position according to heuristic + FindResult res; - ctx.free_head = node->next; + Option r = find_best_pos(size); - // insert the new node into the right starting point, and - // let 'cur' point to the remaining nodes needing to be - // stiched back in + // it failed + if (!r) + { + return none; + } - cur = *res.prev_link; - if (cur->x < res.x) - { - // preserve the existing one, so start testing with the next one - Node * next = cur->next; - cur->next = node; - cur = next; - } - else - { - *res.prev_link = node; - } + res = r.unwrap(); - // from here, traverse cur and free the nodes, until we get to one - // that shouldn't be freed - while (cur->next && cur->next->x <= res.x + width) - { - Node * next = cur->next; - // move the current node to the free list - cur->next = ctx.free_head; - ctx.free_head = cur; - cur = next; - } + // the best node doesn't fit (we don't always check this) + if (res.pos.y + extent.y > extent.y) + { + return none; + } - // stitch the list back in - node->next = cur; + // we're out of memory + if (free_head == nullptr) + { + return none; + } - if (cur->x < res.x + width) - cur->x = res.x + width; + // create new node + Node * node = free_head; -#ifdef _DEBUG - cur = ctx.active_head; - while (cur->x < ctx.width) - { - CHECK(cur->x < cur->next->x); - cur = cur->next; - } - CHECK(cur->next == nullptr); + node->pos = res.pos + Vec2I{0, extent.y}; - { - i32 count = 0; - cur = ctx.active_head; - while (cur) + free_head = node->next; + + // insert the new node into the right starting point, and let 'cur' point + // to the remaining nodes needing to be stiched back in + + Node * cur = *res.prev_link; + + if (cur->pos.x < res.pos.x) { - cur = cur->next; - ++count; + // preserve the existing one, so start testing with the next one + Node * next = cur->next; + cur->next = node; + cur = next; } - cur = ctx.free_head; - while (cur) + else { - cur = cur->next; - ++count; + *res.prev_link = node; } - CHECK(count == ctx.num_nodes + 2); - } -#endif - - return res; -} - -// Initialize a rectangle packer to: -// pack a rectangle that is 'width' by 'height' in dimensions -// using temporary storage provided by the array 'nodes', which is -// 'num_nodes' long -// -// You must call this function every time you start packing into a new target. -// -// There is no "shutdown" function. The 'nodes' memory must stay valid for -// the following pack_rects() call (or calls), but can be freed after -// the call (or calls) finish. -// -// Note: to guarantee best results, either: -// 1. make sure 'num_nodes' >= 'width' -// or 2. call allow_out_of_mem() defined below with 'allow_out_of_mem = -// 1' -// -// If you don't do either of the above things, widths will be quantized to -// multiples of small integers to guarantee the algorithm doesn't run out of -// temporary storage. -// -// If you do #2, then the non-quantized algorithm will be used, but the -// algorithm may run out of temporary storage and be unable to pack some -// rectangles. -// -inline Context init(i32 width, i32 height, Node * nodes, i32 num_nodes, - bool allow_out_of_mem) -{ - Context ctx; - i32 i = 0; - for (; i < num_nodes - 1; ++i) - { - nodes[i].next = &nodes[i + 1]; - } + // from here, traverse cur and free the nodes, until we get to one + // that shouldn't be freed + while (cur->next && cur->next->pos.x <= res.pos.x + extent.x) + { + Node * next = cur->next; + // move the current node to the free list + cur->next = free_head; + free_head = cur; + cur = next; + } - nodes[i].next = nullptr; - ctx.init_mode = Mode::InitSkyline; - ctx.heuristic = Heuristic::BL_sortHeight; - ctx.free_head = &nodes[0]; - ctx.active_head = &ctx.extra[0]; - ctx.width = width; - ctx.height = height; - ctx.num_nodes = num_nodes; + // stitch the list back in + node->next = cur; - if (allow_out_of_mem) - { - // if it's ok to run out of memory, then don't bother aligning them; - // this gives better packing, but may fail due to OOM (even though - // the rectangles easily fit). @TODO a smarter approach would be to only - // quantize once we've hit OOM, then we could get rid of this parameter. - ctx.align = 1; - } - else - { - // if it's not ok to run out of memory, then quantize the widths - // so that num_nodes is always enough nodes. - // - // I.e. num_nodes * align >= width - // align >= width / num_nodes - // align = ceil(width/num_nodes) - ctx.align = (ctx.width + ctx.num_nodes - 1) / ctx.num_nodes; - } - - // node 0 is the full width, node 1 is the sentinel (lets us not store width - // explicitly) - ctx.extra[0].x = 0; - ctx.extra[0].y = 0; - ctx.extra[0].next = &ctx.extra[1]; - ctx.extra[1].x = width; - ctx.extra[1].y = (1 << 30); - ctx.extra[1].next = nullptr; - - return ctx; -} - -// Assign packed locations to rectangles. The rectangles are of type -// 'rect' defined below, stored in the array 'rects', and there -// are 'num_rects' many of them. -// -// Rectangles which are successfully packed have the 'was_packed' flag -// set to a non-zero value and 'x' and 'y' store the minimum location -// on each axis (i.e. bottom-left in cartesian coordinates, top-left -// if you imagine y increasing downwards). Rectangles which do not fit -// have the 'was_packed' flag set to 0. -// -// You should not try to access the 'rects' array from another thread -// while this function is running, as the function temporarily reorders -// the array while it executes. -// -// To pack into another rectangle, you need to call init_target -// again. To continue packing into the same rectangle, you can call -// this function again. Calling this multiple times with multiple rect -// arrays will probably produce worse packing results than calling it -// a single time with the full rectangle array, but the option is -// available. -// -// The function returns 1 if all of the rectangles were successfully -// packed and 0 otherwise. -inline bool pack_rects(Context & ctx, rect * rects, i32 num_rects) -{ - i32 all_rects_packed = 1; + if (cur->pos.x < res.pos.x + extent.x) + { + cur->pos.x = res.pos.x + extent.x; + } - // we use the 'was_packed' field internally to allow sorting/unsorting - for (i32 i = 0; i < num_rects; ++i) - { - rects[i].was_packed = i; + return res.pos; } - // sort according to heuristic - qsort(rects, (usize) num_rects, sizeof(rects[0]), rect_height_compare); - - for (i32 i = 0; i < num_rects; ++i) + // Assign packed locations to rectangles. + // + // Rectangles which are successfully packed have the 'packed' flag + // set to a non-zero value and 'x' and 'y' store the minimum location + // on each axis (i.e. bottom-left in cartesian coordinates, top-left + // if you imagine y increasing downwards). Rectangles which do not fit + // have the 'packed' flag set to 0. + // + // To pack into another rectangle, you need to call `reset`. + // To continue packing into the same rectangle, you can call + // this function again. Calling this multiple times with multiple rect + // arrays will probably produce worse packing results than calling it + // a single time with the full rectangle array, but the option is + // available. + // + Tuple, Span> pack(Span rects) { - if (rects[i].w == 0 || rects[i].h == 0) + for (auto & r : rects) { - rects[i].x = rects[i].y = 0; // empty rect needs no space + r.packed = false; } - else + + sort(rects, [](PackRect const & a, PackRect const & b) { + // sort by height, then by width + if (a.extent.y > b.extent.y) + { + return true; + } + if (a.extent.y < b.extent.y) + { + return false; + } + return a.extent.x > b.extent.x; + }); + + for (PackRect & r : rects) { - FindResult find_result = - skyline_pack_rectangle(ctx, rects[i].w, rects[i].h); - if (find_result.prev_link) + if (r.extent.x == 0 | r.extent.y == 0) { - rects[i].x = static_cast(find_result.x); - rects[i].y = static_cast(find_result.y); + // empty rect needs no space + r.pos = Vec2I{}; + r.packed = true; } else { - rects[i].x = rects[i].y = I32_MAX; + pack_rect(r.extent).match( + [&](Vec2I pos) { + r.packed = true; + r.pos = pos; + }, + [&]() { + r.packed = false; + r.pos = Vec2I::splat(I32_MAX); + }); } } - } - - // unsort - qsort(rects, static_cast(num_rects), sizeof(rects[0]), - rect_original_order); - // set was_packed flags and all_rects_packed status - for (i32 i = 0; i < num_rects; ++i) - { - rects[i].was_packed = !(rects[i].x == I32_MAX && rects[i].y == I32_MAX); - if (!rects[i].was_packed) - all_rects_packed = 0; + return partition(rects, [](PackRect const & r) { return r.packed; }); } +}; - // return the all_rects_packed status - return static_cast(all_rects_packed); -} - -} // namespace rect_pack -} // namespace ash +} // namespace ash diff --git a/ashura/engine/render_text.h b/ashura/engine/render_text.h index 97012f983..2c90f3714 100644 --- a/ashura/engine/render_text.h +++ b/ashura/engine/render_text.h @@ -31,263 +31,250 @@ struct TextHighlight /// @param runs Run-End encoded sequences of the runs struct RenderText { - struct Inner + bool dirty_ = true; + bool use_kerning_ = true; + bool use_ligatures_ = true; + TextDirection direction_ = TextDirection::LeftToRight; + f32 alignment_ = -1; + Vec text_ = {}; + Vec runs_ = {}; + Vec styles_ = {}; + Vec fonts_ = {}; + Span language_ = {}; + TextLayout layout_ = {}; + Vec highlights_ = {}; + + /// @brief Styles specified runs of text, performing run merging and + /// splitting in the process. If there's previously no runs, the first added + /// run will be the default and span the whole of the text. + /// @param first first codepoint index to be patched + /// @param count range of the number of codepoints to be patched + /// @param style font style to be applied + /// @param font font configuration to be applied + void run(TextStyle const & style, FontStyle const & font, u32 first = 0, + u32 count = U32_MAX) { - bool dirty = true; - bool use_kerning = true; - bool use_ligatures = true; - TextDirection direction = TextDirection::LeftToRight; - f32 alignment = -1; - Vec text = {}; - Vec runs = {}; - Vec styles = {}; - Vec fonts = {}; - Span language = {}; - TextLayout layout = {}; - Vec highlights = {}; - - /// @brief Styles specified runs of text, performing run merging and - /// splitting in the process. If there's previously no runs, the first added - /// run will be the default and span the whole of the text. - /// @param first first codepoint index to be patched - /// @param count range of the number of codepoints to be patched - /// @param style font style to be applied - /// @param font font configuration to be applied - void style(TextStyle const & style, FontStyle const & font, u32 first = 0, - u32 count = U32_MAX) + if (count == 0) { - if (count == 0) - { - return; - } + return; + } - if (runs.is_empty()) - { - runs.push(U32_MAX).unwrap(); - styles.push(style).unwrap(); - fonts.push(font).unwrap(); - dirty = true; - return; - } + if (runs_.is_empty()) + { + runs_.push(U32_MAX).unwrap(); + styles_.push(style).unwrap(); + fonts_.push(font).unwrap(); + dirty_ = true; + return; + } - u32 const end = sat_add(first, count); + u32 const end = sat_add(first, count); - Span const first_run_span = binary_find(runs.view(), gt, first); + Span const first_run_span = binary_find(runs_.view(), gt, first); - /// should never happen since there's always a U32_MAX run end - CHECK(!first_run_span.is_empty()); + /// should never happen since there's always a U32_MAX run end + CHECK(!first_run_span.is_empty()); - Span const last_run_span = binary_find(first_run_span, geq, end); + Span const last_run_span = binary_find(first_run_span, geq, end); - /// should never happen since there's always a U32_MAX run end - CHECK(!last_run_span.is_empty()); + /// should never happen since there's always a U32_MAX run end + CHECK(!last_run_span.is_empty()); - u32 first_run = (u32) (first_run_span.pbegin() - runs.view().pbegin()); - u32 last_run = (u32) (last_run_span.pbegin() - runs.view().pbegin()); + u32 first_run = (u32) (first_run_span.pbegin() - runs_.view().pbegin()); + u32 last_run = (u32) (last_run_span.pbegin() - runs_.view().pbegin()); - u32 const first_run_begin = (first_run == 0) ? 0 : runs[first_run - 1]; - u32 const last_run_end = runs[last_run]; + u32 const first_run_begin = (first_run == 0) ? 0 : runs_[first_run - 1]; + u32 const last_run_end = runs_[last_run]; - /// run merging + /// run merging - /// merge middle + /// merge middle - if (last_run > (first_run + 1)) - { - u32 const first_erase = first_run + 1; - u32 const num_erase = last_run - first_erase; - runs.erase(first_erase, num_erase); - styles.erase(first_erase, num_erase); - fonts.erase(first_erase, num_erase); - last_run -= num_erase; - } + if (last_run > (first_run + 1)) + { + u32 const first_erase = first_run + 1; + u32 const num_erase = last_run - first_erase; + runs_.erase(first_erase, num_erase); + styles_.erase(first_erase, num_erase); + fonts_.erase(first_erase, num_erase); + last_run -= num_erase; + } - /// merge left - if (first_run_begin == first) - { - u32 const first_erase = first_run; - u32 const num_erase = last_run - first_run; - runs.erase(first_erase, num_erase); - styles.erase(first_erase, num_erase); - fonts.erase(first_erase, num_erase); - last_run -= num_erase; - } + /// merge left + if (first_run_begin == first) + { + u32 const first_erase = first_run; + u32 const num_erase = last_run - first_run; + runs_.erase(first_erase, num_erase); + styles_.erase(first_erase, num_erase); + fonts_.erase(first_erase, num_erase); + last_run -= num_erase; + } - /// merge right - if (last_run_end == end) - { - u32 const first_erase = first_run + 1; - u32 const num_erase = (last_run + 1) - first_erase; - runs.erase(first_erase, num_erase); - styles.erase(first_erase, num_erase); - fonts.erase(first_erase, num_erase); - last_run -= num_erase; - } + /// merge right + if (last_run_end == end) + { + u32 const first_erase = first_run + 1; + u32 const num_erase = (last_run + 1) - first_erase; + runs_.erase(first_erase, num_erase); + styles_.erase(first_erase, num_erase); + fonts_.erase(first_erase, num_erase); + last_run -= num_erase; + } - (void) last_run; + (void) last_run; - /// run splitting - if (first_run_begin == first && last_run_end == end) + /// run splitting + if (first_run_begin == first && last_run_end == end) + { + styles_[first_run] = style; + fonts_[first_run] = font; + } + else + { + if (first_run_begin == first) { - styles[first_run] = style; - fonts[first_run] = font; + // split with new on left + runs_.insert(first_run, end).unwrap(); + styles_.insert(first_run, style).unwrap(); + fonts_.insert(first_run, font).unwrap(); + } + else if (last_run_end == end) + { + // split with new on right + runs_[first_run] = first; + runs_.insert(first_run + 1, end).unwrap(); + styles_.insert(first_run + 1, style).unwrap(); + fonts_.insert(first_run + 1, font).unwrap(); } else { - if (first_run_begin == first) - { - // split with new on left - runs.insert(first_run, end).unwrap(); - styles.insert(first_run, style).unwrap(); - fonts.insert(first_run, font).unwrap(); - } - else if (last_run_end == end) - { - // split with new on right - runs[first_run] = first; - runs.insert(first_run + 1, end).unwrap(); - styles.insert(first_run + 1, style).unwrap(); - fonts.insert(first_run + 1, font).unwrap(); - } - else - { - // split with new in the middle of the run - runs[first_run] = first; - runs.insert(first_run + 1, end).unwrap(); - styles.insert(first_run + 1, style).unwrap(); - fonts.insert(first_run + 1, font).unwrap(); - runs.insert(first_run + 2, last_run_end).unwrap(); - styles.insert(first_run + 2, styles[first_run]).unwrap(); - fonts.insert(first_run + 2, fonts[first_run]).unwrap(); - } + // split with new in the middle of the run + runs_[first_run] = first; + runs_.insert(first_run + 1, end).unwrap(); + styles_.insert(first_run + 1, style).unwrap(); + fonts_.insert(first_run + 1, font).unwrap(); + runs_.insert(first_run + 2, last_run_end).unwrap(); + styles_.insert(first_run + 2, styles_[first_run]).unwrap(); + fonts_.insert(first_run + 2, fonts_[first_run]).unwrap(); } - - dirty = true; } - }; - Inner inner{}; + dirty_ = true; + } void flush_text() { - inner.dirty = true; + dirty_ = true; } void highlight(TextHighlight const & highlight) { - inner.highlights.push(highlight).unwrap(); + highlights_.push(highlight).unwrap(); } void clear_highlights() { - inner.highlights.clear(); + highlights_.clear(); } void set_direction(TextDirection direction) { - if (inner.direction == direction) + if (direction_ == direction) { return; } - inner.direction = direction; + direction_ = direction; flush_text(); } void set_language(Span language) { - if (range_eq(inner.language, language)) + if (range_eq(language_, language)) { return; } - inner.language = language; + language_ = language; flush_text(); } void set_alignment(f32 alignment) { - if (inner.alignment == alignment) + if (alignment_ == alignment) { return; } - inner.alignment = alignment; + alignment_ = alignment; flush_text(); } Span get_text() const { - return inner.text; + return text_; } void set_text(Span utf32, TextStyle const & style, FontStyle const & font) { set_text(utf32); - inner.style(style, font); + run(style, font); flush_text(); } void set_text(Span utf32) { - inner.text.clear(); - inner.text.extend(utf32).unwrap(); + text_.clear(); + text_.extend(utf32).unwrap(); flush_text(); } void set_text(Span utf8, TextStyle const & style, FontStyle const & font) { - inner.style(style, font); + run(style, font); set_text(utf8); flush_text(); } void set_text(Span utf8) { - inner.text.clear(); - utf8_decode(utf8, inner.text).unwrap(); - flush_text(); - } - - void style(TextStyle const & style, FontStyle const & font, u32 first = 0, - u32 count = U32_MAX) - { - inner.style(style, font, first, count); + text_.clear(); + utf8_decode(utf8, text_).unwrap(); flush_text(); } TextBlock block() const { - return TextBlock{.text = inner.text, - .runs = inner.runs, - .fonts = inner.fonts, - .direction = inner.direction, - .language = inner.language, - .use_kerning = inner.use_kerning, - .use_ligatures = inner.use_ligatures}; + return TextBlock{.text = text_, + .runs = runs_, + .fonts = fonts_, + .direction = direction_, + .language = language_, + .use_kerning = use_kerning_, + .use_ligatures = use_ligatures_}; } TextBlockStyle block_style(f32 aligned_width) const { - return TextBlockStyle{.runs = inner.styles, - .alignment = inner.alignment, - .align_width = aligned_width}; + return TextBlockStyle{ + .runs = styles_, .alignment = alignment_, .align_width = aligned_width}; } void layout(f32 max_width) { - if (!inner.dirty && max_width == inner.layout.extent.x) + if (!dirty_ && max_width == layout_.extent.x) { return; } - layout_text(block(), max_width, inner.layout); + layout_text(block(), max_width, layout_); } void render(Canvas & canvas, CRect const & region, CRect const & clip, f32 zoom) const { (void) zoom; - canvas.text({.center = region.center}, block(), inner.layout, + canvas.text({.center = region.center}, block(), layout_, block_style(region.extent.x), clip); // [ ] zoom // [ ] render highlights @@ -297,4 +284,4 @@ struct RenderText } }; -} // namespace ash +} // namespace ash diff --git a/ashura/engine/renderer.cc b/ashura/engine/renderer.cc index cf341b3b5..b3ab9d0bf 100644 --- a/ashura/engine/renderer.cc +++ b/ashura/engine/renderer.cc @@ -1,50 +1,48 @@ /// SPDX-License-Identifier: MIT #include "ashura/engine/renderer.h" #include "ashura/engine/canvas.h" -#include "ashura/std/math.h" -#include "ashura/std/range.h" namespace ash { -void Renderer::begin_frame(GpuContext & ctx, RenderTarget const &, - Canvas & canvas) +void Renderer::begin_frame(GpuSystem & gpu, Framebuffer const &, + Canvas & canvas) { - gpu::CommandEncoder & enc = ctx.encoder(); - Resources & r = resources[ctx.ring_index()]; + gpu::CommandEncoder & enc = gpu.encoder(); + Resources & r = resources[gpu.ring_index()]; - r.ngon_vertices.copy(ctx, span(canvas.ngon_vertices).as_u8()); - r.ngon_indices.copy(ctx, span(canvas.ngon_indices).as_u8()); - r.ngon_params.copy(ctx, span(canvas.ngon_params).as_u8()); - r.rrect_params.copy(ctx, span(canvas.rrect_params).as_u8()); - r.ngon_vertices.copy(ctx, span(canvas.ngon_vertices).as_u8()); + r.ngon_vertices.copy(gpu, span(canvas.ngon_vertices).as_u8()); + r.ngon_indices.copy(gpu, span(canvas.ngon_indices).as_u8()); + r.ngon_params.copy(gpu, span(canvas.ngon_params).as_u8()); + r.rrect_params.copy(gpu, span(canvas.rrect_params).as_u8()); + r.ngon_vertices.copy(gpu, span(canvas.ngon_vertices).as_u8()); - for (Dyn const & p : pipelines) + for (Dyn const & p : pipelines) { - p->begin_frame(ctx, passes, enc); + p->begin_frame(gpu, passes, enc); } } -void Renderer::end_frame(GpuContext & ctx, RenderTarget const &, Canvas &) +void Renderer::end_frame(GpuSystem & gpu, Framebuffer const &, Canvas &) { - gpu::CommandEncoder & enc = ctx.encoder(); + gpu::CommandEncoder & enc = gpu.encoder(); - for (Dyn const & p : pipelines) + for (Dyn const & p : pipelines) { - p->end_frame(ctx, passes, enc); + p->end_frame(gpu, passes, enc); } } -void Renderer::render_frame(GpuContext & ctx, RenderTarget const & rt, +void Renderer::render_frame(GpuSystem & gpu, Framebuffer const & fb, Canvas & canvas) { - Resources & r = resources[ctx.ring_index()]; - gpu::CommandEncoder & enc = ctx.encoder(); + Resources & r = resources[gpu.ring_index()]; + gpu::CommandEncoder & enc = gpu.encoder(); Canvas::RenderContext render_ctx{.canvas = canvas, - .gpu = ctx, + .gpu = gpu, .passes = passes, - .rt = rt, + .framebuffer = fb, .enc = enc, .rrects = r.rrect_params, .ngons = r.ngon_params, @@ -57,4 +55,4 @@ void Renderer::render_frame(GpuContext & ctx, RenderTarget const & rt, } } -} // namespace ash +} // namespace ash diff --git a/ashura/engine/renderer.h b/ashura/engine/renderer.h index 4d2529d14..e47ff74ce 100644 --- a/ashura/engine/renderer.h +++ b/ashura/engine/renderer.h @@ -1,6 +1,6 @@ /// SPDX-License-Identifier: MIT #pragma once -#include "ashura/engine/gpu_context.h" +#include "ashura/engine/gpu_system.h" #include "ashura/engine/passes.h" #include "ashura/std/dyn.h" @@ -9,21 +9,22 @@ namespace ash struct PassContext { - BloomPass * bloom; - BlurPass * blur; - NgonPass * ngon; - PBRPass * pbr; - RRectPass * rrect; + BloomPass * bloom; + BlurPass * blur; + NgonPass * ngon; + PBRPass * pbr; + RRectPass * rrect; Vec> all; explicit PassContext(BloomPass & bloom, BlurPass & blur, NgonPass & ngon, - PBRPass & pbr, RRectPass & rrect, Vec> all) : - bloom{&bloom}, - blur{&blur}, - ngon{&ngon}, - pbr{&pbr}, - rrect{&rrect}, - all{std::move(all)} + PBRPass & pbr, RRectPass & rrect, + Vec> all) : + bloom{&bloom}, + blur{&blur}, + ngon{&ngon}, + pbr{&pbr}, + rrect{&rrect}, + all{std::move(all)} { } @@ -43,11 +44,15 @@ struct PassContext Vec> all{allocator}; - all.push(transmute(std::move(bloom), static_cast(pbloom))).unwrap(); - all.push(transmute(std::move(blur), static_cast(pblur))).unwrap(); - all.push(transmute(std::move(ngon), static_cast(pngon))).unwrap(); + all.push(transmute(std::move(bloom), static_cast(pbloom))) + .unwrap(); + all.push(transmute(std::move(blur), static_cast(pblur))) + .unwrap(); + all.push(transmute(std::move(ngon), static_cast(pngon))) + .unwrap(); all.push(transmute(std::move(pbr), static_cast(ppbr))).unwrap(); - all.push(transmute(std::move(rrect), static_cast(prrect))).unwrap(); + all.push(transmute(std::move(rrect), static_cast(prrect))) + .unwrap(); return PassContext{*pbloom, *pblur, *pngon, *ppbr, *prrect, std::move(all)}; } @@ -58,50 +63,40 @@ struct PassContext PassContext & operator=(PassContext &&) = default; ~PassContext() = default; - void acquire(GpuContext & ctx, AssetMap & assets) + void acquire(GpuSystem & gpu, AssetMap & assets) { for (auto const & p : all) { - p->acquire(ctx, assets); + p->acquire(gpu, assets); }; } - void release(GpuContext & ctx, AssetMap & assets) + void release(GpuSystem & gpu, AssetMap & assets) { for (auto const & p : all) { - p->release(ctx, assets); + p->release(gpu, assets); }; } }; -struct RenderPipeline +struct GpuPipeline { virtual Span id() = 0; - virtual void acquire(GpuContext & ctx, PassContext & passes, + virtual void acquire(GpuSystem & gpu, PassContext & passes, AssetMap & assets) = 0; - virtual void release(GpuContext & ctx, PassContext & passes, + virtual void release(GpuSystem & gpu, PassContext & passes, AssetMap & assets) = 0; - virtual void begin_frame(GpuContext & ctx, PassContext & passes, + virtual void begin_frame(GpuSystem & gpu, PassContext & passes, gpu::CommandEncoder & enc) = 0; - virtual void end_frame(GpuContext & ctx, PassContext & passes, + virtual void end_frame(GpuSystem & gpu, PassContext & passes, gpu::CommandEncoder & enc) = 0; - virtual ~RenderPipeline() = default; -}; - -struct RenderTarget -{ - gpu::RenderingInfo info{}; - gpu::Viewport viewport{}; - gpu::Extent extent{}; - gpu::DescriptorSet color_descriptor = nullptr; - gpu::DescriptorSet depth_descriptor = nullptr; - gpu::DescriptorSet stencil_descriptor = nullptr; + virtual ~GpuPipeline() = default; }; struct Canvas; @@ -127,12 +122,12 @@ struct Renderer PassContext passes; - Vec> pipelines; + Vec> pipelines; Renderer(AllocatorImpl allocator, PassContext passes) : - resources{}, - passes{std::move(passes)}, - pipelines{allocator} + resources{}, + passes{std::move(passes)}, + pipelines{allocator} { } @@ -148,58 +143,58 @@ struct Renderer Renderer & operator=(Renderer &&) = default; ~Renderer() = default; - void acquire(GpuContext & ctx, AssetMap & assets) + void acquire(GpuSystem & gpu, AssetMap & assets) { - passes.acquire(ctx, assets); + passes.acquire(gpu, assets); - for (Dyn const & p : pipelines) + for (Dyn const & p : pipelines) { - p->acquire(ctx, passes, assets); + p->acquire(gpu, passes, assets); } - resources.resize(ctx.buffering).unwrap(); + resources.resize(gpu.buffering).unwrap(); } - void release(GpuContext & ctx, AssetMap & assets) + void release(GpuSystem & gpu, AssetMap & assets) { for (Resources & r : resources) { - r.pbr_params.uninit(ctx); - r.pbr_light_params.uninit(ctx); - r.ngon_vertices.uninit(ctx); - r.ngon_indices.uninit(ctx); - r.ngon_params.uninit(ctx); - r.rrect_params.uninit(ctx); + r.pbr_params.uninit(gpu); + r.pbr_light_params.uninit(gpu); + r.ngon_vertices.uninit(gpu); + r.ngon_indices.uninit(gpu); + r.ngon_params.uninit(gpu); + r.rrect_params.uninit(gpu); } resources.reset(); - for (Dyn const & p : pipelines) + for (Dyn const & p : pipelines) { - p->release(ctx, passes, assets); + p->release(gpu, passes, assets); } - passes.release(ctx, assets); + passes.release(gpu, assets); } - void add_pass(GpuContext & ctx, Dyn pass, AssetMap & assets) + void add_pass(GpuSystem & gpu, Dyn pass, AssetMap & assets) { - pass->acquire(ctx, assets); + pass->acquire(gpu, assets); passes.all.push(std::move(pass)).unwrap(); } - void add_pipeline(GpuContext & ctx, Dyn pipeline, + void add_pipeline(GpuSystem & gpu, Dyn pipeline, AssetMap & assets) { - pipeline->acquire(ctx, passes, assets); + pipeline->acquire(gpu, passes, assets); pipelines.push(std::move(pipeline)).unwrap(); } - void begin_frame(GpuContext & ctx, RenderTarget const & rt, Canvas & canvas); + void begin_frame(GpuSystem & gpu, Framebuffer const & fb, Canvas & canvas); - void end_frame(GpuContext & ctx, RenderTarget const & rt, Canvas & canvas); + void end_frame(GpuSystem & gpu, Framebuffer const & fb, Canvas & canvas); - void render_frame(GpuContext & ctx, RenderTarget const & rt, Canvas & canvas); + void render_frame(GpuSystem & gpu, Framebuffer const & fb, Canvas & canvas); }; -} // namespace ash +} // namespace ash diff --git a/ashura/engine/scene.h b/ashura/engine/scene.h index 44818a49b..8abfb6b53 100644 --- a/ashura/engine/scene.h +++ b/ashura/engine/scene.h @@ -27,4 +27,4 @@ struct SceneNode struct Scene; -} // namespace ash +} // namespace ash diff --git a/ashura/engine/shader.cc b/ashura/engine/shader.cc index 5b10cfc23..a92bdafcb 100644 --- a/ashura/engine/shader.cc +++ b/ashura/engine/shader.cc @@ -1,312 +1,203 @@ /// SPDX-License-Identifier: MIT #include "ashura/engine/shader.h" #include "SPIRV/GlslangToSpv.h" -#include "SPIRV/spirv.hpp" -#include "SPIRV/spvIR.h" -#include "ashura/std/fs.h" #include "ashura/std/vec.h" #include "glslang/Public/ShaderLang.h" -#include namespace ash { constexpr TBuiltInResource SHADER_RESOURCE_LIMITS = { - .maxLights = 32, - .maxClipPlanes = 6, - .maxTextureUnits = 32, - .maxTextureCoords = 32, - .maxVertexAttribs = 64, - .maxVertexUniformComponents = 4'096, - .maxVaryingFloats = 64, - .maxVertexTextureImageUnits = 32, - .maxCombinedTextureImageUnits = 80, - .maxTextureImageUnits = 32, - .maxFragmentUniformComponents = 4'096, - .maxDrawBuffers = 32, - .maxVertexUniformVectors = 128, - .maxVaryingVectors = 8, - .maxFragmentUniformVectors = 16, - .maxVertexOutputVectors = 16, - .maxFragmentInputVectors = 15, - .minProgramTexelOffset = -8, - .maxProgramTexelOffset = 7, - .maxClipDistances = 8, - .maxComputeWorkGroupCountX = 65'535, - .maxComputeWorkGroupCountY = 65'535, - .maxComputeWorkGroupCountZ = 65'535, - .maxComputeWorkGroupSizeX = 1'024, - .maxComputeWorkGroupSizeY = 1'024, - .maxComputeWorkGroupSizeZ = 64, - .maxComputeUniformComponents = 1'024, - .maxComputeTextureImageUnits = 16, - .maxComputeImageUniforms = 8, - .maxComputeAtomicCounters = 8, - .maxComputeAtomicCounterBuffers = 1, - .maxVaryingComponents = 60, - .maxVertexOutputComponents = 64, - .maxGeometryInputComponents = 64, - .maxGeometryOutputComponents = 128, - .maxFragmentInputComponents = 128, - .maxImageUnits = 8, - .maxCombinedImageUnitsAndFragmentOutputs = 8, - .maxCombinedShaderOutputResources = 8, - .maxImageSamples = 0, - .maxVertexImageUniforms = 0, - .maxTessControlImageUniforms = 0, - .maxTessEvaluationImageUniforms = 0, - .maxGeometryImageUniforms = 0, - .maxFragmentImageUniforms = 8, - .maxCombinedImageUniforms = 8, - .maxGeometryTextureImageUnits = 16, - .maxGeometryOutputVertices = 256, - .maxGeometryTotalOutputComponents = 1'024, - .maxGeometryUniformComponents = 1'024, - .maxGeometryVaryingComponents = 64, - .maxTessControlInputComponents = 128, - .maxTessControlOutputComponents = 128, - .maxTessControlTextureImageUnits = 16, - .maxTessControlUniformComponents = 1'024, - .maxTessControlTotalOutputComponents = 4'096, - .maxTessEvaluationInputComponents = 128, - .maxTessEvaluationOutputComponents = 128, - .maxTessEvaluationTextureImageUnits = 16, - .maxTessEvaluationUniformComponents = 1'024, - .maxTessPatchComponents = 120, - .maxPatchVertices = 32, - .maxTessGenLevel = 64, - .maxViewports = 16, - .maxVertexAtomicCounters = 0, - .maxTessControlAtomicCounters = 0, - .maxTessEvaluationAtomicCounters = 0, - .maxGeometryAtomicCounters = 0, - .maxFragmentAtomicCounters = 8, - .maxCombinedAtomicCounters = 8, - .maxAtomicCounterBindings = 1, - .maxVertexAtomicCounterBuffers = 0, - .maxTessControlAtomicCounterBuffers = 0, - .maxTessEvaluationAtomicCounterBuffers = 0, - .maxGeometryAtomicCounterBuffers = 0, - .maxFragmentAtomicCounterBuffers = 1, - .maxCombinedAtomicCounterBuffers = 1, - .maxAtomicCounterBufferSize = 16'384, - .maxTransformFeedbackBuffers = 4, - .maxTransformFeedbackInterleavedComponents = 64, - .maxCullDistances = 8, - .maxCombinedClipAndCullDistances = 8, - .maxSamples = 4, - .maxMeshOutputVerticesNV = 256, - .maxMeshOutputPrimitivesNV = 512, - .maxMeshWorkGroupSizeX_NV = 32, - .maxMeshWorkGroupSizeY_NV = 1, - .maxMeshWorkGroupSizeZ_NV = 1, - .maxTaskWorkGroupSizeX_NV = 32, - .maxTaskWorkGroupSizeY_NV = 1, - .maxTaskWorkGroupSizeZ_NV = 1, - .maxMeshViewCountNV = 4, - .maxMeshOutputVerticesEXT = 256, - .maxMeshOutputPrimitivesEXT = 256, - .maxMeshWorkGroupSizeX_EXT = 128, - .maxMeshWorkGroupSizeY_EXT = 128, - .maxMeshWorkGroupSizeZ_EXT = 128, - .maxTaskWorkGroupSizeX_EXT = 128, - .maxTaskWorkGroupSizeY_EXT = 128, - .maxTaskWorkGroupSizeZ_EXT = 128, - .maxMeshViewCountEXT = 4, - .maxDualSourceDrawBuffersEXT = 1, - .limits = { - .nonInductiveForLoops = true, - .whileLoops = true, - .doWhileLoops = true, - .generalUniformIndexing = true, - .generalAttributeMatrixVectorIndexing = true, - .generalVaryingIndexing = true, - .generalSamplerIndexing = true, - .generalVariableIndexing = true, - .generalConstantMatrixVectorIndexing = true, - } + .maxLights = 32, + .maxClipPlanes = 6, + .maxTextureUnits = 32, + .maxTextureCoords = 32, + .maxVertexAttribs = 64, + .maxVertexUniformComponents = 4'096, + .maxVaryingFloats = 64, + .maxVertexTextureImageUnits = 32, + .maxCombinedTextureImageUnits = 80, + .maxTextureImageUnits = 32, + .maxFragmentUniformComponents = 4'096, + .maxDrawBuffers = 32, + .maxVertexUniformVectors = 128, + .maxVaryingVectors = 8, + .maxFragmentUniformVectors = 16, + .maxVertexOutputVectors = 16, + .maxFragmentInputVectors = 15, + .minProgramTexelOffset = -8, + .maxProgramTexelOffset = 7, + .maxClipDistances = 8, + .maxComputeWorkGroupCountX = 65'535, + .maxComputeWorkGroupCountY = 65'535, + .maxComputeWorkGroupCountZ = 65'535, + .maxComputeWorkGroupSizeX = 1'024, + .maxComputeWorkGroupSizeY = 1'024, + .maxComputeWorkGroupSizeZ = 64, + .maxComputeUniformComponents = 1'024, + .maxComputeTextureImageUnits = 16, + .maxComputeImageUniforms = 8, + .maxComputeAtomicCounters = 8, + .maxComputeAtomicCounterBuffers = 1, + .maxVaryingComponents = 60, + .maxVertexOutputComponents = 64, + .maxGeometryInputComponents = 64, + .maxGeometryOutputComponents = 128, + .maxFragmentInputComponents = 128, + .maxImageUnits = 8, + .maxCombinedImageUnitsAndFragmentOutputs = 8, + .maxCombinedShaderOutputResources = 8, + .maxImageSamples = 0, + .maxVertexImageUniforms = 0, + .maxTessControlImageUniforms = 0, + .maxTessEvaluationImageUniforms = 0, + .maxGeometryImageUniforms = 0, + .maxFragmentImageUniforms = 8, + .maxCombinedImageUniforms = 8, + .maxGeometryTextureImageUnits = 16, + .maxGeometryOutputVertices = 256, + .maxGeometryTotalOutputComponents = 1'024, + .maxGeometryUniformComponents = 1'024, + .maxGeometryVaryingComponents = 64, + .maxTessControlInputComponents = 128, + .maxTessControlOutputComponents = 128, + .maxTessControlTextureImageUnits = 16, + .maxTessControlUniformComponents = 1'024, + .maxTessControlTotalOutputComponents = 4'096, + .maxTessEvaluationInputComponents = 128, + .maxTessEvaluationOutputComponents = 128, + .maxTessEvaluationTextureImageUnits = 16, + .maxTessEvaluationUniformComponents = 1'024, + .maxTessPatchComponents = 120, + .maxPatchVertices = 32, + .maxTessGenLevel = 64, + .maxViewports = 16, + .maxVertexAtomicCounters = 0, + .maxTessControlAtomicCounters = 0, + .maxTessEvaluationAtomicCounters = 0, + .maxGeometryAtomicCounters = 0, + .maxFragmentAtomicCounters = 8, + .maxCombinedAtomicCounters = 8, + .maxAtomicCounterBindings = 1, + .maxVertexAtomicCounterBuffers = 0, + .maxTessControlAtomicCounterBuffers = 0, + .maxTessEvaluationAtomicCounterBuffers = 0, + .maxGeometryAtomicCounterBuffers = 0, + .maxFragmentAtomicCounterBuffers = 1, + .maxCombinedAtomicCounterBuffers = 1, + .maxAtomicCounterBufferSize = 16'384, + .maxTransformFeedbackBuffers = 4, + .maxTransformFeedbackInterleavedComponents = 64, + .maxCullDistances = 8, + .maxCombinedClipAndCullDistances = 8, + .maxSamples = 4, + .maxMeshOutputVerticesNV = 256, + .maxMeshOutputPrimitivesNV = 512, + .maxMeshWorkGroupSizeX_NV = 32, + .maxMeshWorkGroupSizeY_NV = 1, + .maxMeshWorkGroupSizeZ_NV = 1, + .maxTaskWorkGroupSizeX_NV = 32, + .maxTaskWorkGroupSizeY_NV = 1, + .maxTaskWorkGroupSizeZ_NV = 1, + .maxMeshViewCountNV = 4, + .maxMeshOutputVerticesEXT = 256, + .maxMeshOutputPrimitivesEXT = 256, + .maxMeshWorkGroupSizeX_EXT = 128, + .maxMeshWorkGroupSizeY_EXT = 128, + .maxMeshWorkGroupSizeZ_EXT = 128, + .maxTaskWorkGroupSizeX_EXT = 128, + .maxTaskWorkGroupSizeY_EXT = 128, + .maxTaskWorkGroupSizeZ_EXT = 128, + .maxMeshViewCountEXT = 4, + .maxDualSourceDrawBuffersEXT = 1, + .limits{ + .nonInductiveForLoops = true, + .whileLoops = true, + .doWhileLoops = true, + .generalUniformIndexing = true, + .generalAttributeMatrixVectorIndexing = true, + .generalVaryingIndexing = true, + .generalSamplerIndexing = true, + .generalVariableIndexing = true, + .generalConstantMatrixVectorIndexing = true, + } }; struct Includer : glslang::TShader::Includer { - Includer() + ShaderCompileInfo info; + AllocatorImpl allocator; + + Includer(ShaderCompileInfo info, AllocatorImpl allocator) : + info{info}, + allocator{allocator} { } - Includer(Includer const &) = delete; - Includer(Includer &&) = delete; - Includer & operator=(Includer const &) = delete; - Includer & operator=(Includer &&) = delete; + Includer(Includer const &) = default; + Includer(Includer &&) = default; + Includer & operator=(Includer const &) = default; + Includer & operator=(Includer &&) = default; + virtual ~Includer() override = default; virtual IncludeResult * includeLocal(char const * header_name, char const * includer_name, size_t inclusion_depth) override { - return include_path(header_name, includer_name, (int) inclusion_depth, - true); + (void) includer_name; + (void) inclusion_depth; + + return info.on_load(Span{header_name, strlen(header_name)}) + .match( + [&](Span header_data) -> IncludeResult * { + IncludeResult * result; + if (!allocator.nalloc(1, result)) + { + info.on_log(LogLevels::Error, + "Failed to allocate memory for Include Result"); + return nullptr; + } + + new (result) IncludeResult{header_name, header_data.data(), + header_data.size(), nullptr}; + + return result; + }, + [&]() -> IncludeResult * { return nullptr; }); } virtual IncludeResult * includeSystem(char const * header_name, char const * includer_name, size_t inclusion_depth) override { - return include_path(header_name, includer_name, (int) inclusion_depth, - false); - } - - virtual void releaseInclude(IncludeResult * result) override - { - if (result != nullptr) - { - auto * blob = reinterpret_cast *>(result->userData); - blob->reset(); - default_allocator.ndealloc(blob, 1); - result->~IncludeResult(); - default_allocator.ndealloc(result, 1); - } - } - - virtual ~Includer() override - { + return includeLocal(header_name, includer_name, inclusion_depth); } - IncludeResult * include_path(char const * header_name, - char const * includer_name, int depth, - bool is_local) + virtual void releaseInclude(IncludeResult * inc) override { - IncludeResult * result = nullptr; - if (is_local) + if (inc != nullptr) { - result = include_local(header_name, includer_name, depth); - if (result != nullptr) - { - return result; - } - return include_system(header_name, includer_name, depth); + inc->~IncludeResult(); + allocator.ndealloc(inc, 1); + info.on_drop(Span{inc->headerName.data(), inc->headerLength}); } - - result = include_system(header_name, includer_name, depth); - if (result != nullptr) - { - return result; - } - return include_local(header_name, includer_name, depth); } - - IncludeResult * include_local(char const * header_name, - char const * includer_name, int depth) - { - (void) includer_name; - (void) depth; - { - std::filesystem::path first_cnd{includer_name}; - first_cnd /= header_name; - - if (std::filesystem::exists(first_cnd)) - { - std::string p = first_cnd.string(); - return include_file(header_name, p.c_str()); - } - } - - for (Span folder : local_directories) - { - std::filesystem::path cnd{ - std::string_view{folder.data(), folder.size()} - }; - cnd /= header_name; - - if (std::filesystem::exists(cnd)) - { - std::string p = cnd.string(); - return include_file(header_name, p.c_str()); - } - } - - return nullptr; - } - - IncludeResult * include_system(char const * header_name, - char const * includer_name, int depth) - { - (void) includer_name; - (void) depth; - for (Span folder : system_directories) - { - std::filesystem::path cnd{ - std::string_view{folder.data(), folder.size()} - }; - cnd /= header_name; - - if (std::filesystem::exists(cnd)) - { - std::string p = cnd.string(); - return include_file(header_name, p.c_str()); - } - } - return nullptr; - } - - IncludeResult * include_file(char const * header_name, char const * path) - { - Vec * blob; - if (!default_allocator.nalloc(1, blob)) - { - return nullptr; - } - new (blob) Vec{}; - defer blob_{[&] { - if (blob != nullptr) - { - blob->reset(); - default_allocator.ndealloc(blob, 1); - } - }}; - - if (!read_file(Span{path, strlen(path)}, *blob)) - { - return nullptr; - } - - IncludeResult * result; - if (!default_allocator.nalloc(1, result)) - { - return nullptr; - } - - new (result) - IncludeResult{header_name, (char *) blob->data(), blob->size(), blob}; - blob = nullptr; - - return result; - } - - Span const> system_directories; - Span const> local_directories; }; -ShaderCompileError - compile_shader(Logger & logger, Vec & spirv, Span file, - ShaderType type, Span preamble, - Span entry_point, - Span const> system_directories, - Span const> local_directories) +Result compile_shader(ShaderCompileInfo const & info, + Vec & spirv, + AllocatorImpl allocator) { if (!glslang::InitializeProcess()) { - return ShaderCompileError::InitError; + return Err{ShaderCompileError::InitError}; } - defer glsl_([] { glslang::FinalizeProcess(); }); - EShLanguage language = EShLanguage::EShLangVertex; - Vec buff; + defer glsl_([] { glslang::FinalizeProcess(); }); - if (!read_file(file, buff)) - { - return ShaderCompileError::IOError; - } + EShLanguage language = EShLanguage::EShLangVertex; - switch (type) + switch (info.type) { case ShaderType::Compute: language = EShLanguage::EShLangCompute; @@ -324,8 +215,20 @@ ShaderCompileError CHECK_UNREACHABLE(); } - char const * buff_p = (char *) buff.data(); - int buff_length = (int) buff.size(); + Option> buff = info.on_load(info.file); + + if (!buff) + { + return Err{ShaderCompileError::IOError}; + } + + defer unload{[&]() { info.on_drop(info.file); }}; + + CHECK(buff.value().size() <= I32_MAX); + + char const * buff_p = (char *) buff.value().data(); + int buff_length = (int) buff.value().size32(); + glslang::TShader shader{language}; shader.setStringsWithLengths(&buff_p, &buff_length, 1); shader.setEnvInput(glslang::EShSourceGlsl, language, glslang::EShClientVulkan, @@ -333,59 +236,67 @@ ShaderCompileError shader.setEnvClient(glslang::EShClientVulkan, glslang::EShTargetVulkan_1_0); shader.setEnvTarget(glslang::EShTargetSpv, glslang::EShTargetSpv_1_0); - Vec entry_point_s; - if (!entry_point_s.extend(entry_point)) - { - return ShaderCompileError::OutOfMemory; - } - if (!entry_point_s.push((char) 0)) - { - return ShaderCompileError::OutOfMemory; - } - shader.setEntryPoint(entry_point_s.data()); - shader.setSourceEntryPoint(entry_point_s.data()); - Vec preamble_s; - if (!preamble_s.extend(preamble)) + auto check_log = [&](auto & object) { + char const * info_log = object.getInfoLog(); + usize const info_len = info_log == nullptr ? 0 : strlen(info_log); + char const * debug_log = object.getInfoDebugLog(); + usize const debug_len = debug_log == nullptr ? 0 : strlen(debug_log); + + if (info_len != 0) + { + info.on_log(LogLevels::Info, Span{info_log, info_len}); + } + + if (debug_len != 0) + { + info.on_log(LogLevels::Debug, Span{debug_log, debug_len}); + } + }; + + constexpr char const * entry_point = "main"; + + shader.setEntryPoint(entry_point); + shader.setSourceEntryPoint(entry_point); + + defer shader_log_{[&] { check_log(shader); }}; + + Vec preamble{allocator}; + + if (!preamble.extend(info.preamble)) { - return ShaderCompileError::OutOfMemory; + return Err{ShaderCompileError::OutOfMemory}; } - if (!preamble_s.extend(span({'\n', '\0'}))) + + if (!preamble.extend(span({'\n', '\0'}))) { - return ShaderCompileError::OutOfMemory; + return Err{ShaderCompileError::OutOfMemory}; } - shader.setPreamble(preamble_s.data()); - Includer includer; - includer.system_directories = system_directories; - includer.local_directories = local_directories; + shader.setPreamble(preamble.data()); - // expand to source - std::string str; - if (!shader.preprocess(&SHADER_RESOURCE_LIMITS, 100, ENoProfile, false, false, - EShMsgDefault, &str, includer)) - { - logger.error(shader.getInfoLog(), "\n", shader.getInfoDebugLog()); - return ShaderCompileError::CompileFailed; - } + Includer includer{info, allocator}; if (!shader.parse(&SHADER_RESOURCE_LIMITS, 100, false, EShMsgDefault, includer)) { - logger.error(shader.getInfoLog(), "\n", shader.getInfoDebugLog()); - return ShaderCompileError::CompileFailed; + return Err{ShaderCompileError::CompileFailed}; } glslang::TProgram program; program.addShader(&shader); + + defer program_log_{[&] { check_log(program); }}; + if (!program.link(EShMsgDefault)) { - return ShaderCompileError::LinkFailed; + return Err{ShaderCompileError::LinkFailed}; } glslang::TIntermediate * intermediate = program.getIntermediate(language); + if (intermediate == nullptr) { - return ShaderCompileError::LinkFailed; + return Err{ShaderCompileError::LinkFailed}; } glslang::SpvOptions spvOptions{.generateDebugInfo = true, @@ -403,82 +314,18 @@ ShaderCompileError std::vector spirv_v; glslang::GlslangToSpv(*intermediate, spirv_v, &spv_logger, &spvOptions); std::string conv_messages = spv_logger.getAllMessages(); + if (!conv_messages.empty()) { - logger.warn(conv_messages); + info.on_log(LogLevels::Info, conv_messages); } if (!spirv.extend(spirv_v)) { - return ShaderCompileError::OutOfMemory; - } - - return ShaderCompileError::None; -} - -ShaderCompileError - pack_shader(Vec, Vec>> & compiled, - Span id, Span root_directory, - Span file, Span preamble) -{ - ShaderType type = ShaderType::Compute; - - if (ends_with(file, ".comp"_str)) - { - type = ShaderType::Compute; - } - else if (ends_with(file, ".frag"_str)) - { - type = ShaderType::Fragment; - } - else if (ends_with(file, ".vert"_str)) - { - type = ShaderType::Vertex; - } - else - { - CHECK_DESC(false, "invalid shader extension"); - } - - Vec file_path; - if (!file_path.extend(root_directory) || !path_append(file_path, file)) - { - return ShaderCompileError::OutOfMemory; - } - - Vec spirv; - ShaderCompileError error = - compile_shader(*logger, spirv, file_path, type, preamble, "main"_str, - span({root_directory}), {}); - - if (error != ShaderCompileError::None) - { - return error; + return Err{ShaderCompileError::OutOfMemory}; } - if (!compiled.push(id, std::move(spirv))) - { - return ShaderCompileError::OutOfMemory; - } - - return ShaderCompileError::None; -} - -ShaderCompileError - pack_shaders(Vec, Vec>> & compiled, - Span entries, - Span root_directory) -{ - for (ShaderUnit const & entry : entries) - { - ShaderCompileError error = pack_shader(compiled, entry.id, root_directory, - entry.file, entry.preamble); - if (error != ShaderCompileError::None) - { - return error; - } - } - return ShaderCompileError::None; + return Ok{}; } -} // namespace ash +} // namespace ash diff --git a/ashura/engine/shader.h b/ashura/engine/shader.h index c085e111c..16e2d780a 100644 --- a/ashura/engine/shader.h +++ b/ashura/engine/shader.h @@ -8,7 +8,7 @@ namespace ash { -enum class ShaderType : u8 +enum class ShaderType : u32 { Compute = 0, Vertex = 1, @@ -18,32 +18,26 @@ enum class ShaderType : u8 enum class ShaderCompileError : i32 { - None = 0, - OutOfMemory = 1, - IOError = 2, - CompileFailed = 3, - LinkFailed = 4, - SpirvConversionFailed = 5, - InitError = 6 + OutOfMemory = 0, + IOError = 1, + CompileFailed = 2, + LinkFailed = 3, + SpirvConversionFailed = 4, + InitError = 5 }; -ShaderCompileError - compile_shader(Logger & logger, Vec & spirv, Span file, - ShaderType type, Span preamble, - Span entry_point, - Span const> system_directories, - Span const> local_directories); - -struct ShaderUnit +struct ShaderCompileInfo { - Span id = {}; - Span file = {}; - Span preamble = {}; + ShaderType type = ShaderType::Compute; + Span file{}; + Span preamble{}; + Fn)> on_log{}; + Fn>(Span)> on_load{}; + Fn)> on_drop{}; }; -ShaderCompileError - pack_shaders(Vec, Vec>> & compiled, - Span entries, - Span root_directory); +Result compile_shader(ShaderCompileInfo const & info, + Vec & spirv, + AllocatorImpl allocator); -} // namespace ash +} // namespace ash diff --git a/ashura/engine/tests/render_text.cc b/ashura/engine/tests/render_text.cc index 38d3249a0..a7d9684b7 100644 --- a/ashura/engine/tests/render_text.cc +++ b/ashura/engine/tests/render_text.cc @@ -11,32 +11,32 @@ TEST(RenderText, RunManagement) RenderText text; - ASSERT_EQ(text.inner.runs.size32(), 0); + ASSERT_EQ(text.runs_.size32(), 0); - text.style({}, {}, 0, 1); - ASSERT_EQ(text.inner.runs.size32(), 1); + text.run({}, {}, 0, 1); + ASSERT_EQ(text.runs_.size32(), 1); - text.style({}, {}, 0, 1); - ASSERT_EQ(text.inner.runs.size32(), 2); + text.run({}, {}, 0, 1); + ASSERT_EQ(text.runs_.size32(), 2); - text.style({}, {}, 0, 1); - ASSERT_EQ(text.inner.runs.size32(), 2); + text.run({}, {}, 0, 1); + ASSERT_EQ(text.runs_.size32(), 2); - text.style({}, {}, 0, 8); - ASSERT_EQ(text.inner.runs.size32(), 2); + text.run({}, {}, 0, 8); + ASSERT_EQ(text.runs_.size32(), 2); - text.style({}, {}, 0, 2); - ASSERT_EQ(text.inner.runs.size32(), 3); + text.run({}, {}, 0, 2); + ASSERT_EQ(text.runs_.size32(), 3); - text.style({}, {}, 0, 8); - ASSERT_EQ(text.inner.runs.size32(), 2); + text.run({}, {}, 0, 8); + ASSERT_EQ(text.runs_.size32(), 2); - text.style({}, {}, 1, 7); - ASSERT_EQ(text.inner.runs.size32(), 3); + text.run({}, {}, 1, 7); + ASSERT_EQ(text.runs_.size32(), 3); - text.style({}, {}, 1, 4); - ASSERT_EQ(text.inner.runs.size32(), 4); + text.run({}, {}, 1, 4); + ASSERT_EQ(text.runs_.size32(), 4); - text.style({}, {}, 0, U32_MAX); - ASSERT_EQ(text.inner.runs.size32(), 1); + text.run({}, {}, 0, U32_MAX); + ASSERT_EQ(text.runs_.size32(), 1); } diff --git a/ashura/engine/tests/text_compositor.cc b/ashura/engine/tests/text_compositor.cc index fc72d650a..e06684e57 100644 --- a/ashura/engine/tests/text_compositor.cc +++ b/ashura/engine/tests/text_compositor.cc @@ -27,16 +27,16 @@ TEST(TextCompositor, Main) text, clip, 1, {}); ASSERT_TRUE(inserted); - ASSERT_EQ(cmp.inner.current_record, 1); - ASSERT_EQ(cmp.inner.latest_record, 1); - ASSERT_EQ(cmp.inner.buffer_pos, text.size32()); + ASSERT_EQ(cmp.current_record_, 1); + ASSERT_EQ(cmp.latest_record_, 1); + ASSERT_EQ(cmp.buffer_pos_, text.size32()); cmp.command(text, layout, 0, 0, TextCommand::SelectLine, fn(insert), noop, {}, clip, 1, {}); - ASSERT_EQ(cmp.inner.current_record, 1); - ASSERT_EQ(cmp.inner.latest_record, 1); - ASSERT_EQ(cmp.inner.buffer_pos, text.size32()); + ASSERT_EQ(cmp.current_record_, 1); + ASSERT_EQ(cmp.latest_record_, 1); + ASSERT_EQ(cmp.buffer_pos_, text.size32()); ASSERT_EQ(cmp.get_cursor().first, 0); ASSERT_EQ(cmp.get_cursor().last, text.size32() - 1); ASSERT_EQ(cmp.get_cursor().as_slice(text.size32()).offset, 0); diff --git a/ashura/engine/text.cc b/ashura/engine/text.cc index 9654a3d46..0c3613eeb 100644 --- a/ashura/engine/text.cc +++ b/ashura/engine/text.cc @@ -29,21 +29,21 @@ static inline void shape(hb_font_t * font, hb_buffer_t * buffer, { // tags are opentype feature tags hb_feature_t const shaping_features[] = { - // kerning operations - {.tag = HB_TAG('k', 'e', 'r', 'n'), - .value = use_kerning, - .start = HB_FEATURE_GLOBAL_START, - .end = HB_FEATURE_GLOBAL_END}, - // standard ligature glyph substitution - {.tag = HB_TAG('l', 'i', 'g', 'a'), - .value = use_ligatures, - .start = HB_FEATURE_GLOBAL_START, - .end = HB_FEATURE_GLOBAL_END}, - // contextual ligature glyph substitution - {.tag = HB_TAG('c', 'l', 'i', 'g'), - .value = use_ligatures, - .start = HB_FEATURE_GLOBAL_START, - .end = HB_FEATURE_GLOBAL_END} + // kerning operations + {.tag = HB_TAG('k', 'e', 'r', 'n'), + .value = use_kerning, + .start = HB_FEATURE_GLOBAL_START, + .end = HB_FEATURE_GLOBAL_END}, + // standard ligature glyph substitution + {.tag = HB_TAG('l', 'i', 'g', 'a'), + .value = use_ligatures, + .start = HB_FEATURE_GLOBAL_START, + .end = HB_FEATURE_GLOBAL_END}, + // contextual ligature glyph substitution + {.tag = HB_TAG('c', 'l', 'i', 'g'), + .value = use_ligatures, + .start = HB_FEATURE_GLOBAL_START, + .end = HB_FEATURE_GLOBAL_END} }; hb_buffer_reset(buffer); @@ -61,12 +61,12 @@ static inline void shape(hb_font_t * font, hb_buffer_t * buffer, u32 num_pos; hb_glyph_position_t const * glyph_pos = - hb_buffer_get_glyph_positions(buffer, &num_pos); + hb_buffer_get_glyph_positions(buffer, &num_pos); CHECK(!(glyph_pos == nullptr && num_pos > 0)); u32 num_info; hb_glyph_info_t const * glyph_info = - hb_buffer_get_glyph_infos(buffer, &num_info); + hb_buffer_get_glyph_infos(buffer, &num_info); CHECK(!(glyph_info == nullptr && num_info > 0)); CHECK(num_pos == num_info); @@ -151,9 +151,9 @@ static inline void segment_levels(Span text, if (length > 0) { SBParagraphRef paragraph = SBAlgorithmCreateParagraph( - algorithm, first, length, - (base == TextDirection::LeftToRight) ? SBLevelDefaultLTR : - SBLevelDefaultRTL); + algorithm, first, length, + (base == TextDirection::LeftToRight) ? SBLevelDefaultLTR : + SBLevelDefaultRTL); CHECK(paragraph != nullptr); CHECK(SBParagraphGetLength(paragraph) == length); @@ -215,10 +215,10 @@ static inline void insert_run(TextLayout & l, FontStyle const & s, u32 first, hb_glyph_info_t const & info = infos[i]; hb_glyph_position_t const & pos = positions[i]; GlyphShape shape{ - .glyph = info.codepoint, - .cluster = info.cluster, - .advance = pos.x_advance, - .offset = {pos.x_offset, -pos.y_offset} + .glyph = info.codepoint, + .cluster = info.cluster, + .advance = pos.x_advance, + .offset = {pos.x_offset, -pos.y_offset} }; l.glyphs[first_glyph + i] = shape; @@ -226,23 +226,23 @@ static inline void insert_run(TextLayout & l, FontStyle const & s, u32 first, } l.runs - .push(TextRun{ - .first_codepoint = first, - .num_codepoints = count, - .style = style, - .font_height = s.font_height, - .line_height = max(s.line_height, 1.0F), - .first_glyph = first_glyph, - .num_glyphs = num_glyphs, - .metrics = TextRunMetrics{.advance = advance, - .ascent = font_metrics.ascent, - .descent = font_metrics.descent}, - .base_level = base_level, - .level = level, - .paragraph = paragraph, - .breakable = breakable + .push(TextRun{ + .first_codepoint = first, + .num_codepoints = count, + .style = style, + .font_height = s.font_height, + .line_height = max(s.line_height, 1.0F), + .first_glyph = first_glyph, + .num_glyphs = num_glyphs, + .metrics = TextRunMetrics{.advance = advance, + .ascent = font_metrics.ascent, + .descent = font_metrics.descent}, + .base_level = base_level, + .level = level, + .paragraph = paragraph, + .breakable = breakable }) - .unwrap(); + .unwrap(); } /// See Unicode Embedding Level Reordering: @@ -335,10 +335,10 @@ void layout_text(TextBlock const & block, f32 max_width, TextLayout & layout) { hb_language_t language = - block.language.is_empty() ? - hb_language_get_default() : - hb_language_from_string(block.language.data(), - (i32) block.language.size()); + block.language.is_empty() ? + hb_language_get_default() : + hb_language_from_string(block.language.data(), + (i32) block.language.size()); hb_buffer_t * buffer = hb_buffer_create(); CHECK(buffer != nullptr); defer buffer_{[&] { hb_buffer_destroy(buffer); }}; @@ -370,7 +370,7 @@ void layout_text(TextBlock const & block, f32 max_width, TextLayout & layout) Span positions = {}; shape(f->hb_font, buffer, block.text, first, i - first, hb_script_from_iso15924_tag( - SBScriptGetOpenTypeTag(SBScript{(u8) first_segment.script})), + SBScriptGetOpenTypeTag(SBScript{(u8) first_segment.script})), ((first_segment.level & 0x1) == 0) ? HB_DIRECTION_LTR : HB_DIRECTION_RTL, language, block.use_kerning, block.use_ligatures, infos, @@ -406,10 +406,10 @@ void layout_text(TextBlock const & block, f32 max_width, TextLayout & layout) first_run.line_height; while ( - i < num_runs && !layout.runs[i].paragraph && - !(layout.runs[i].breakable && (au_to_px(layout.runs[i].metrics.advance, - layout.runs[i].font_height) + - width) > max_width)) + i < num_runs && !layout.runs[i].paragraph && + !(layout.runs[i].breakable && + (au_to_px(layout.runs[i].metrics.advance, layout.runs[i].font_height) + + width) > max_width)) { TextRun const & r = layout.runs[i]; TextRunMetrics const & m = r.metrics; @@ -423,19 +423,19 @@ void layout_text(TextBlock const & block, f32 max_width, TextLayout & layout) TextRun const & last_run = layout.runs[i - 1]; u32 const first_codepoint = first_run.first_codepoint; u32 const num_codepoints = - (last_run.first_codepoint + last_run.num_codepoints) - first_codepoint; + (last_run.first_codepoint + last_run.num_codepoints) - first_codepoint; Line line{ - .first_codepoint = first_codepoint, - .num_codepoints = num_codepoints, - .first_run = first, - .num_runs = (i - first), - .metrics = LineMetrics{.width = width, - .height = height, - .ascent = ascent, - .descent = descent, - .level = base_level}, - .paragraph = paragraph + .first_codepoint = first_codepoint, + .num_codepoints = num_codepoints, + .first_run = first, + .num_runs = (i - first), + .metrics = LineMetrics{.width = width, + .height = height, + .ascent = ascent, + .descent = descent, + .level = base_level}, + .paragraph = paragraph }; layout.lines.push(line).unwrap(); @@ -482,7 +482,7 @@ TextHitResult hit_text(TextLayout const & layout, f32 style_align_width, Line const & ln = layout.lines[l]; TextDirection const direction = level_to_direction(ln.metrics.level); f32 const alignment = - style_alignment * ((direction == TextDirection::LeftToRight) ? 1 : -1); + style_alignment * ((direction == TextDirection::LeftToRight) ? 1 : -1); f32 cursor = space_align(block_width, ln.metrics.width, alignment) - ln.metrics.width * 0.5F; @@ -490,9 +490,9 @@ TextHitResult hit_text(TextLayout const & layout, f32 style_align_width, { TextRun const & run = layout.runs[r]; bool const intersects = - (pos.x >= cursor && - pos.x <= (cursor + au_to_px(run.metrics.advance, run.font_height))) || - (r == ln.num_runs - 1); + (pos.x >= cursor && + pos.x <= (cursor + au_to_px(run.metrics.advance, run.font_height))) || + (r == ln.num_runs - 1); if (!intersects) { continue; @@ -503,25 +503,25 @@ TextHitResult hit_text(TextLayout const & layout, f32 style_align_width, GlyphShape const & glyph = layout.glyphs[run.first_glyph + g]; f32 const advance = au_to_px(glyph.advance, run.font_height); bool const intersects = - (pos.x >= glyph_cursor && pos.x <= (glyph_cursor + advance)) || - (g == run.num_glyphs - 1); + (pos.x >= glyph_cursor && pos.x <= (glyph_cursor + advance)) || + (g == run.num_glyphs - 1); if (!intersects) { glyph_cursor += advance; continue; } u32 const column = (glyph.cluster > ln.first_codepoint) ? - (glyph.cluster - ln.first_codepoint) : - 0; + (glyph.cluster - ln.first_codepoint) : + 0; return TextHitResult{ - .cluster = glyph.cluster, .line = l, .column = column}; + .cluster = glyph.cluster, .line = l, .column = column}; } cursor += au_to_px(run.metrics.advance, run.font_height); } u32 const column = (ln.num_codepoints == 0) ? 0 : (ln.num_codepoints - 1); return TextHitResult{ - .cluster = ln.first_codepoint + column, .line = l, .column = column}; + .cluster = ln.first_codepoint + column, .line = l, .column = column}; } -} // namespace ash +} // namespace ash diff --git a/ashura/engine/text.h b/ashura/engine/text.h index 945cf7f1c..ab9b28b5f 100644 --- a/ashura/engine/text.h +++ b/ashura/engine/text.h @@ -239,13 +239,15 @@ struct FontStyle /// @param shadow_scale relative. multiplied by font_height /// @param shadow_offset px. offset from center of glyph +// [ ] define text styler or renderer hook. examine our use cases +// [ ] translation of text components? struct TextStyle { f32 underline_thickness = 0; f32 strikethrough_thickness = 0; f32 shadow_scale = 0; Vec2 shadow_offset = Vec2{0, 0}; - ColorGradient foreground = {}; + ColorGradient color = {}; ColorGradient background = {}; ColorGradient underline = {}; ColorGradient strikethrough = {}; @@ -290,7 +292,7 @@ struct TextBlockStyle /// @param cluster unicode grapheme cluster within the text run /// @param advance context-dependent horizontal-layout advance /// @param offset context-dependent text shaping offset from normal font glyph -/// position, i.e. offset from Glyph::bearing +/// position, i.e. offset from GlyphMetrics::bearing struct GlyphShape { u32 glyph = 0; @@ -429,4 +431,4 @@ void layout_text(TextBlock const & block, f32 max_width, TextLayout & layout); TextHitResult hit_text(TextLayout const & layout, f32 align_width, f32 alignment, Vec2 pos); -} // namespace ash +} // namespace ash diff --git a/ashura/engine/text_compositor.cc b/ashura/engine/text_compositor.cc index a5aafad7c..9d0625166 100644 --- a/ashura/engine/text_compositor.cc +++ b/ashura/engine/text_compositor.cc @@ -8,84 +8,65 @@ namespace ash { -static inline u32 goto_line(TextLayout const & layout, u32 alignment, u32 line) +void TextCompositor::pop_records(u32 num) { - if (layout.lines.is_empty()) - { - return 0; - } - - line = min(line, layout.lines.size32() - 1); - - Line const & ln = layout.lines[line]; - - if (alignment > ln.num_codepoints) - { - return ln.first_codepoint + (ln.num_codepoints - 1); - } - - return ln.first_codepoint + alignment; -} - -void TextCompositor::Inner::pop_records(u32 num) -{ - CHECK(num <= records.size32()); + CHECK(num <= records_.size32()); u32 reclaimed = 0; for (u32 i = 0; i < num; i++) { - reclaimed += records[i].slice.span; + reclaimed += records_[i].slice.span; } - mem::move(span(buffer).slice(reclaimed), span(buffer)); - mem::move(span(records).slice(num), span(records)); - buffer_usage -= reclaimed; - latest_record -= num; - current_record -= num; + mem::move(span(buffer_).slice(reclaimed), span(buffer_)); + mem::move(span(records_).slice(num), span(records_)); + buffer_usage_ -= reclaimed; + latest_record_ -= num; + current_record_ -= num; } -void TextCompositor::Inner::append_record(bool is_insert, u32 text_pos, - Span segment) +void TextCompositor::append_record(bool is_insert, u32 text_pos, + Span segment) { - if (segment.size32() > buffer.size32()) + if (segment.size32() > buffer_.size32()) { // clear all records as we can't insert a new record without invalidating // the history - pop_records(records.size32()); + pop_records(records_.size32()); return; } - while (buffer_usage + segment.size32() > buffer.size32()) + while (buffer_usage_ + segment.size32() > buffer_.size32()) { // pop half, to amortize shifting cost. // always pop by atleast 1. since the buffer can fit it and atleast 1 // record would be using the available memory. - pop_records(max(records.size32() >> 1, 1U)); + pop_records(max(records_.size32() >> 1, 1U)); } - if (current_record + 1 >= records.size32()) + if (current_record_ + 1 >= records_.size32()) { - pop_records(max(records.size32() >> 1, 1U)); + pop_records(max(records_.size32() >> 1, 1U)); } - mem::copy(segment, span(buffer).slice(buffer_pos)); + mem::copy(segment, span(buffer_).slice(buffer_pos_)); - current_record++; - latest_record = current_record; - records[latest_record] = TextEditRecord{ - .slice = Slice32{text_pos, segment.size32()}, - .is_insert = is_insert + current_record_++; + latest_record_ = current_record_; + records_[latest_record_] = TextEditRecord{ + .slice = Slice32{text_pos, segment.size32()}, + .is_insert = is_insert }; - buffer_pos += segment.size32(); + buffer_pos_ += segment.size32(); } -void TextCompositor::Inner::undo(Insert insert, Erase erase) +void TextCompositor::undo(Insert insert, Erase erase) { - if (current_record == 0) + if (current_record_ == 0) { return; } // undo changes of current record - TextEditRecord & record = records[current_record]; - buffer_pos -= record.slice.span; + TextEditRecord & record = records_[current_record_]; + buffer_pos_ -= record.slice.span; if (record.is_insert) { erase(record.slice); @@ -93,57 +74,57 @@ void TextCompositor::Inner::undo(Insert insert, Erase erase) else { insert(record.slice.offset, - span(buffer).slice(buffer_pos, record.slice.span)); + span(buffer_).slice(buffer_pos_, record.slice.span)); } - current_record--; + current_record_--; if (record.is_insert) { - cursor = TextCursor::from_slice(record.slice); + cursor_ = TextCursor::from_slice(record.slice); } } -void TextCompositor::Inner::redo(Insert insert, Erase erase) +void TextCompositor::redo(Insert insert, Erase erase) { - if (current_record + 1 > latest_record) + if (current_record_ + 1 > latest_record_) { return; } - current_record++; + current_record_++; // apply changes of next record - TextEditRecord & record = records[current_record]; + TextEditRecord & record = records_[current_record_]; if (record.is_insert) { insert(record.slice.offset, - span(buffer).slice(buffer_pos, record.slice.span)); + span(buffer_).slice(buffer_pos_, record.slice.span)); } else { erase(record.slice); } - buffer_pos += record.slice.span; + buffer_pos_ += record.slice.span; if (!record.is_insert) { - cursor = TextCursor::from_slice(record.slice); + cursor_ = TextCursor::from_slice(record.slice); } } -void TextCompositor::Inner::unselect() +void TextCompositor::unselect() { - cursor = cursor.unselect(); + cursor_ = cursor_.unselect(); } -void TextCompositor::Inner::delete_selection(Span text, Erase erase) +void TextCompositor::delete_selection(Span text, Erase erase) { - if (cursor.is_empty()) + if (cursor_.is_empty()) { return; } - Slice32 const selection = cursor.as_slice(text.size32()); + Slice32 const selection = cursor_.as_slice(text.size32()); append_record(false, selection.offset, text.slice(selection)); erase(selection); - cursor = cursor.to_begin(); + cursor_ = cursor_.to_begin(); } static constexpr bool is_symbol(Span symbols, u32 c) @@ -230,7 +211,7 @@ static inline LinePosition line_translate(TextLayout const & layout, u32 cursor, { u32 const alignment = cursor - line.first_codepoint; u32 const next_line = - (u32) clamp(ln + dy, (i64) 0, (i64) (layout.lines.size32() - 1)); + (u32) clamp(ln + dy, (i64) 0, (i64) (layout.lines.size32() - 1)); return LinePosition{.line = next_line, .alignment = alignment}; } } @@ -238,10 +219,11 @@ static inline LinePosition line_translate(TextLayout const & layout, u32 cursor, return LinePosition{}; } -void TextCompositor::Inner::command( - Span text, TextLayout const & layout, f32 align_width, - f32 alignment, TextCommand cmd, Insert insert, Erase erase, - Span input, ClipBoard & clipboard, u32 lines_per_page, Vec2 pos) +void TextCompositor::command(Span text, TextLayout const & layout, + f32 align_width, f32 alignment, TextCommand cmd, + Insert insert, Erase erase, Span input, + ClipBoard & clipboard, u32 lines_per_page, + Vec2 pos) { switch (cmd) { @@ -252,28 +234,28 @@ void TextCompositor::Inner::command( break; case TextCommand::BackSpace: { - if (cursor.is_empty()) + if (cursor_.is_empty()) { - cursor = cursor.to_begin(); - cursor.first--; - cursor = cursor.clamp(text.size32()); + cursor_ = cursor_.to_begin(); + cursor_.first--; + cursor_ = cursor_.clamp(text.size32()); } delete_selection(text, erase); } break; case TextCommand::Delete: { - if (cursor.is_empty()) + if (cursor_.is_empty()) { - cursor.last++; - cursor = cursor.clamp(text.size32()); + cursor_.last++; + cursor_ = cursor_.clamp(text.size32()); } delete_selection(text, erase); } break; case TextCommand::InputText: { - Slice32 selection = cursor.as_slice(text.size32()); + Slice32 selection = cursor_.as_slice(text.size32()); delete_selection(text, noop); append_record(true, selection.offset, input); erase(selection); @@ -282,196 +264,202 @@ void TextCompositor::Inner::command( break; case TextCommand::Left: { - if (!cursor.is_empty()) + if (!cursor_.is_empty()) { - cursor = cursor.to_begin(); + cursor_ = cursor_.to_begin(); break; } - cursor = cursor.to_begin(); - cursor.first = --cursor.last; - cursor = cursor.clamp(text.size32()); + cursor_ = cursor_.to_begin(); + cursor_.first = --cursor_.last; + cursor_ = cursor_.clamp(text.size32()); } break; case TextCommand::Right: { - if (!cursor.is_empty()) + if (!cursor_.is_empty()) { - cursor = cursor.to_end(); + cursor_ = cursor_.to_end(); return; } - cursor = cursor.to_end(); - cursor.first = ++cursor.last; - cursor = cursor.clamp(text.size32()); + cursor_ = cursor_.to_end(); + cursor_.first = ++cursor_.last; + cursor_ = cursor_.clamp(text.size32()); } break; case TextCommand::WordStart: { - Slice32 word = cursor_boundary(text, word_symbols, cursor); - cursor.first = cursor.last = word.offset; + Slice32 word = cursor_boundary(text, word_symbols_, cursor_); + cursor_.first = cursor_.last = word.offset; } break; case TextCommand::WordEnd: { - Slice32 word = cursor_boundary(text, word_symbols, cursor); - cursor.first = cursor.last = - (word.span == 0) ? word.offset : (word.offset + word.span - 1); + Slice32 word = cursor_boundary(text, word_symbols_, cursor_); + cursor_.first = cursor_.last = + (word.span == 0) ? word.offset : (word.offset + word.span - 1); } break; case TextCommand::LineStart: { - Slice32 line = cursor_boundary(text, line_symbols, cursor); - cursor.first = cursor.last = line.offset; + Slice32 line = cursor_boundary(text, line_symbols_, cursor_); + cursor_.first = cursor_.last = line.offset; } break; case TextCommand::LineEnd: { - Slice32 line = cursor_boundary(text, line_symbols, cursor); - cursor.first = cursor.last = - (line.span == 0) ? line.offset : (line.offset + line.span - 1); + Slice32 line = cursor_boundary(text, line_symbols_, cursor_); + cursor_.first = cursor_.last = + (line.span == 0) ? line.offset : (line.offset + line.span - 1); } break; case TextCommand::Up: { - LinePosition line = line_translate(layout, (u32) cursor.last, -1); - cursor.first = cursor.last = goto_line(layout, line.alignment, line.line); + LinePosition line = line_translate(layout, (u32) cursor_.last, -1); + cursor_.first = cursor_.last = + goto_line(layout, line.alignment, line.line); } break; case TextCommand::Down: { - LinePosition line = line_translate(layout, (u32) cursor.last, 1); - cursor.first = cursor.last = goto_line(layout, line.alignment, line.line); + LinePosition line = line_translate(layout, (u32) cursor_.last, 1); + cursor_.first = cursor_.last = + goto_line(layout, line.alignment, line.line); } break; case TextCommand::PageUp: { LinePosition line = - line_translate(layout, (u32) cursor.last, -(i64) lines_per_page); - cursor.first = cursor.last = goto_line(layout, line.alignment, line.line); + line_translate(layout, (u32) cursor_.last, -(i64) lines_per_page); + cursor_.first = cursor_.last = + goto_line(layout, line.alignment, line.line); } break; case TextCommand::PageDown: { LinePosition line = - line_translate(layout, (u32) cursor.last, lines_per_page); - cursor.first = cursor.last = goto_line(layout, line.alignment, line.line); + line_translate(layout, (u32) cursor_.last, lines_per_page); + cursor_.first = cursor_.last = + goto_line(layout, line.alignment, line.line); } break; case TextCommand::SelectLeft: { - cursor.last--; - cursor = cursor.clamp(text.size32()); + cursor_.last--; + cursor_ = cursor_.clamp(text.size32()); } break; case TextCommand::SelectRight: { - cursor.last++; - cursor = cursor.clamp(text.size32()); + cursor_.last++; + cursor_ = cursor_.clamp(text.size32()); } break; case TextCommand::SelectUp: { - LinePosition line = line_translate(layout, (u32) cursor.last, -1); - cursor.last = goto_line(layout, line.alignment, line.line); + LinePosition line = line_translate(layout, (u32) cursor_.last, -1); + cursor_.last = goto_line(layout, line.alignment, line.line); } break; case TextCommand::SelectDown: { - LinePosition line = line_translate(layout, (u32) cursor.last, 1); - cursor.last = goto_line(layout, line.alignment, line.line); + LinePosition line = line_translate(layout, (u32) cursor_.last, 1); + cursor_.last = goto_line(layout, line.alignment, line.line); } break; case TextCommand::SelectWordStart: { - Slice32 word = cursor_boundary(text, word_symbols, cursor); - cursor.last = word.offset; + Slice32 word = cursor_boundary(text, word_symbols_, cursor_); + cursor_.last = word.offset; } break; case TextCommand::SelectWordEnd: { - Slice32 word = cursor_boundary(text, word_symbols, cursor); - cursor.last = - (word.span == 0) ? word.offset : (word.offset + word.span - 1); + Slice32 word = cursor_boundary(text, word_symbols_, cursor_); + cursor_.last = + (word.span == 0) ? word.offset : (word.offset + word.span - 1); } break; case TextCommand::SelectLineStart: { - Slice32 line = cursor_boundary(text, line_symbols, cursor); - cursor.last = line.offset; + Slice32 line = cursor_boundary(text, line_symbols_, cursor_); + cursor_.last = line.offset; } break; case TextCommand::SelectLineEnd: { - Slice32 line = cursor_boundary(text, line_symbols, cursor); - cursor.last = - (line.span == 0) ? line.offset : (line.offset + line.span - 1); + Slice32 line = cursor_boundary(text, line_symbols_, cursor_); + cursor_.last = + (line.span == 0) ? line.offset : (line.offset + line.span - 1); } break; case TextCommand::SelectPageUp: { LinePosition line = - line_translate(layout, (u32) cursor.last, lines_per_page); - cursor.last = goto_line(layout, line.alignment, line.line); + line_translate(layout, (u32) cursor_.last, lines_per_page); + cursor_.last = goto_line(layout, line.alignment, line.line); } break; case TextCommand::SelectPageDown: { LinePosition line = - line_translate(layout, (u32) cursor.last, lines_per_page); - cursor.last = goto_line(layout, line.alignment, line.line); + line_translate(layout, (u32) cursor_.last, lines_per_page); + cursor_.last = goto_line(layout, line.alignment, line.line); } break; case TextCommand::SelectCodepoint: { - cursor.first = cursor.last++; - cursor = cursor.clamp(text.size32()); + cursor_.first = cursor_.last++; + cursor_ = cursor_.clamp(text.size32()); } break; case TextCommand::SelectWord: { - Slice32 word = cursor_boundary(text, word_symbols, cursor); - cursor.first = word.offset; - cursor.last = - (word.span == 0) ? word.offset : (word.offset + word.span - 1); + Slice32 word = cursor_boundary(text, word_symbols_, cursor_); + cursor_.first = word.offset; + cursor_.last = + (word.span == 0) ? word.offset : (word.offset + word.span - 1); } break; case TextCommand::SelectLine: { - Slice32 line = cursor_boundary(text, line_symbols, cursor); - cursor.first = line.offset; - cursor.last = - (line.span == 0) ? line.offset : (line.offset + line.span - 1); + Slice32 line = cursor_boundary(text, line_symbols_, cursor_); + cursor_.first = line.offset; + cursor_.last = + (line.span == 0) ? line.offset : (line.offset + line.span - 1); } break; case TextCommand::SelectAll: { - cursor.first = 0; - cursor.last = text.size32(); + cursor_.first = 0; + cursor_.last = text.size32(); } break; case TextCommand::Cut: { - Vec data_u8; - utf8_encode(text.slice(cursor.as_slice(text.size32())), data_u8).unwrap(); + Vec data_u8{allocator_}; + utf8_encode(text.slice(cursor_.as_slice(text.size32())), data_u8) + .unwrap(); delete_selection(text, erase); clipboard.set_text(data_u8).unwrap(); } break; case TextCommand::Copy: { - Vec data_u8; - utf8_encode(text.slice(cursor.as_slice(text.size32())), data_u8).unwrap(); + Vec data_u8{allocator_}; + utf8_encode(text.slice(cursor_.as_slice(text.size32())), data_u8) + .unwrap(); clipboard.set_text(data_u8).unwrap(); } break; case TextCommand::Paste: { - Vec data_u32; - Vec data_u8; + Vec data_u32{allocator_}; + Vec data_u8{allocator_}; clipboard.get_text(data_u8).unwrap(); utf8_decode(data_u8, data_u32).unwrap(); - Slice32 selection = cursor.as_slice(text.size32()); + Slice32 selection = cursor_.as_slice(text.size32()); delete_selection(text, noop); append_record(true, selection.offset, data_u32); erase(selection); @@ -491,19 +479,19 @@ void TextCompositor::Inner::command( case TextCommand::Hit: { TextHitResult const hit = hit_text(layout, align_width, alignment, pos); - cursor.first = cursor.last = hit.cluster; + cursor_.first = cursor_.last = hit.cluster; } break; case TextCommand::HitSelect: { TextHitResult const hit = hit_text(layout, align_width, alignment, pos); - cursor.last = hit.cluster; + cursor_.last = hit.cluster; } break; case TextCommand::NewLine: { Span input = U"\n"_str; - Slice32 selection = cursor.as_slice(text.size32()); + Slice32 selection = cursor_.as_slice(text.size32()); delete_selection(text, noop); append_record(true, selection.offset, input); erase(selection); @@ -512,8 +500,8 @@ void TextCompositor::Inner::command( break; case TextCommand::Tab: { - Span input = span(TAB_STRING).slice(0, tab_width); - Slice32 selection = cursor.as_slice(text.size32()); + Span input = span(TAB_STRING).slice(0, tab_width_); + Slice32 selection = cursor_.as_slice(text.size32()); delete_selection(text, noop); append_record(true, selection.offset, input); erase(selection); @@ -526,4 +514,4 @@ void TextCompositor::Inner::command( } } -} // namespace ash +} // namespace ash diff --git a/ashura/engine/text_compositor.h b/ashura/engine/text_compositor.h index 2fe7a7f73..ad320482e 100644 --- a/ashura/engine/text_compositor.h +++ b/ashura/engine/text_compositor.h @@ -167,100 +167,73 @@ struct TextCompositor static constexpr c32 TAB_STRING[] = {'\t', '\t', '\t', '\t', '\t', '\t', '\t', '\t'}; - struct Inner + AllocatorImpl allocator_ = default_allocator; + TextCursor cursor_ = {}; + Vec buffer_ = {}; + Vec records_ = {}; + u32 buffer_usage_ = 0; + u32 buffer_pos_ = 0; + u32 latest_record_ = 0; + u32 current_record_ = 0; + u32 tab_width_ = 1; + Span word_symbols_ = DEFAULT_WORD_SYMBOLS; + Span line_symbols_ = DEFAULT_LINE_SYMBOLS; + + TextCompositor(u32 num_buffer_codepoints = 16_KB, u32 num_records = 1'024) : + buffer_{allocator_}, + records_{allocator_} { - TextCursor cursor = {}; - Vec buffer = {}; - Vec records = {}; - u32 buffer_usage = 0; - u32 buffer_pos = 0; - u32 latest_record = 0; - u32 current_record = 0; - u32 tab_width = 1; - Span word_symbols = DEFAULT_WORD_SYMBOLS; - Span line_symbols = DEFAULT_LINE_SYMBOLS; - - Inner(u32 num_buffer_codepoints, u32 num_records) + // [ ] use make() + CHECK(num_buffer_codepoints > 0); + CHECK(num_records > 0); + CHECK(is_pow2(num_buffer_codepoints)); + CHECK(is_pow2(num_records)); + buffer_.resize_uninit(num_buffer_codepoints).unwrap(); + records_.resize(num_records).unwrap(); + } + + static u32 goto_line(TextLayout const & layout, u32 alignment, u32 line) + { + if (layout.lines.is_empty()) { - // [ ] use make() - CHECK(num_buffer_codepoints > 0); - CHECK(num_records > 0); - CHECK(is_pow2(num_buffer_codepoints)); - CHECK(is_pow2(num_records)); - buffer.resize_uninit(num_buffer_codepoints).unwrap(); - records.resize(num_records).unwrap(); + return 0; } - void pop_records(u32 num); - - void append_record(bool is_insert, u32 text_pos, Span segment); - - void undo(Insert insert, Erase erase); + line = min(line, layout.lines.size32() - 1); - void redo(Insert insert, Erase erase); + Line const & ln = layout.lines[line]; - void unselect(); - - void delete_selection(Span text, Erase erase); - - void command(Span text, TextLayout const & layout, - f32 align_width, f32 alignment, TextCommand cmd, Insert insert, - Erase erase, Span input, ClipBoard & clipboard, - u32 lines_per_page, Vec2 pos); - }; - - Inner inner; + if (alignment > ln.num_codepoints) + { + return ln.first_codepoint + (ln.num_codepoints - 1); + } - TextCompositor(u32 num_buffer_codepoints = 16_KB, u32 num_records = 1'024) : - inner{num_buffer_codepoints, num_records} - { + return ln.first_codepoint + alignment; } TextCursor get_cursor() const { - return inner.cursor; + return cursor_; } - void pop_records(u32 num) - { - inner.pop_records(num); - } + void pop_records(u32 num); - void append_record(bool is_insert, u32 text_pos, Span segment) - { - inner.append_record(is_insert, text_pos, segment); - } + void append_record(bool is_insert, u32 text_pos, Span segment); - void undo(Insert insert, Erase erase) - { - inner.undo(insert, erase); - } + void undo(Insert insert, Erase erase); - void redo(Insert insert, Erase erase) - { - inner.redo(insert, erase); - } + void redo(Insert insert, Erase erase); - void unselect() - { - inner.unselect(); - } + void unselect(); - void delete_selection(Span text, Erase erase) - { - inner.delete_selection(text, erase); - } + void delete_selection(Span text, Erase erase); /// @param text original text /// @param input text from IME to insert void command(Span text, TextLayout const & layout, f32 align_width, f32 alignment, TextCommand cmd, Insert insert, Erase erase, Span input, ClipBoard & clipboard, u32 lines_per_page, - Vec2 pos) - { - inner.command(text, layout, align_width, alignment, cmd, insert, erase, - input, clipboard, lines_per_page, pos); - } + Vec2 pos); }; -} // namespace ash +} // namespace ash diff --git a/ashura/engine/view.h b/ashura/engine/view.h index 5b2938246..a0408fb75 100644 --- a/ashura/engine/view.h +++ b/ashura/engine/view.h @@ -5,9 +5,7 @@ #include "ashura/engine/color.h" #include "ashura/engine/input.h" #include "ashura/engine/renderer.h" -#include "ashura/engine/text.h" #include "ashura/std/math.h" -#include "ashura/std/time.h" #include "ashura/std/types.h" namespace ash @@ -30,8 +28,8 @@ struct Size f32 scale = 0; f32 rmin = 0; f32 rmax = 1; - f32 min = F32_MIN; - f32 max = F32_MAX; + f32 min = 0; + f32 max = F32_INF; constexpr f32 operator()(f32 value) const { @@ -57,8 +55,8 @@ struct Frame } constexpr Frame(f32 width, f32 height, bool constrain = true) : - width{.offset = width, .rmax = constrain ? 1 : F32_INF}, - height{.offset = height, .rmax = constrain ? 1 : F32_INF} + width{.offset = width, .rmax = constrain ? 1 : F32_INF}, + height{.offset = height, .rmax = constrain ? 1 : F32_INF} { } @@ -128,10 +126,10 @@ struct CornerRadii } constexpr CornerRadii(Size tl, Size tr, Size bl, Size br) : - tl{tl}, - tr{tr}, - bl{bl}, - br{br} + tl{tl}, + tr{tr}, + bl{bl}, + br{br} { } @@ -140,12 +138,13 @@ struct CornerRadii } constexpr CornerRadii(f32 s, bool constrained) : - CornerRadii{ - Size{.offset = s, .rmax = constrained ? 1 : F32_INF} + CornerRadii{ + Size{.offset = s, .rmax = constrained ? 1 : F32_INF} } { } + // [ ] this should be resolved on both width and height constexpr Vec4 operator()(f32 height) const { return Vec4{tl(height), tr(height), bl(height), br(height)}; @@ -204,281 +203,9 @@ struct ViewEvents bool text_input = false; }; -enum class DropType : u32 -{ - None = 0, - FilePath = 1, - Bytes = 2 -}; - -struct FrameState -{ - struct Mouse - { - /// @brief did the mouse enter the window on this frame? - bool in = false; - - /// @brief did the mouse leave the window on this frame? - bool out = false; - - /// @brief did the mouse move on this frame? - bool moved = false; - - /// @brief did the mouse wheel get scrolled on this frame? - bool wheel_scrolled = false; - - /// @brief is any of the keys pressed on this frame - bool any_down = false; - - /// @brief is any of the keys released on this frame - bool any_up = false; - - /// @brief which mouse buttons were pressed on this frame - Bits downs{}; - - /// @brief which mouse buttons were released on this frame - Bits ups{}; - - /// @brief the current state of each mouse button - Bits states{}; - - /// @brief number of times the mouse was clicked so far - u32 num_clicks[NUM_MOUSE_BUTTONS]{}; - - /// @brief the position of the mouse on this frame - Vec2 position = {}; - - /// @brief translation of the mouse on this frame - Vec2 translation = {}; - - /// @brief translation of the mouse wheel on this frame - Vec2 wheel_translation = {}; - - void clear_frame_() - { - in = false; - out = false; - moved = false; - wheel_scrolled = false; - any_down = false; - any_up = false; - fill(downs, 0ULL); - fill(ups, 0ULL); - fill(states, 0ULL); - fill(num_clicks, 0ULL); - position = {}; - translation = {}; - wheel_translation = {}; - - // [ ] preserve position - // [ ] need to be preserved when swapping frames - } - }; - - struct Keyboard - { - /// @brief did the window gain keyboard focus on this frame? - bool in = false; - - /// @brief did the window lose keyboard focus on this frame? - bool out = false; - - /// @brief is any of the keys pressed on this frame - bool any_down = false; - - /// @brief is any of the keys released on this frame - bool any_up = false; - - /// @brief bit mask of all the keys that were pressed on this frame - Bits downs{}; - - /// @brief bit mask of all the keys that were released on this frame - Bits ups{}; - - /// @brief bit mask of all the key states - Bits states{}; - - /// @brief bit mask of all the keys that were pressed on this frame, indexed using the scancode - Bits scan_downs{}; - - /// @brief bit mask of all the keys that were released on this frame, indexed using the scancode - Bits scan_ups{}; - - /// @brief bit mask of all the key states, indexed using the scancode - Bits scan_states{}; - - /// @brief hold state of the key modifiers on this frame - KeyModifiers modifiers = KeyModifiers::None; - - void clear_frame_() - { - in = false; - out = false; - any_down = false; - any_up = false; - fill(downs, 0ULL); - fill(ups, 0ULL); - fill(states, 0ULL); - fill(scan_downs, 0ULL); - fill(scan_ups, 0ULL); - fill(scan_states, 0ULL); - modifiers = KeyModifiers::None; - } - }; - - /// @brief timestamp of current frame - time_point timestamp = {}; - - /// @brief time elapsed between previous and current frame - nanoseconds timedelta = {}; - - /// @brief the current theme gotten from the window manager - SystemTheme theme = SystemTheme::Unknown; - - /// @brief the preferred text direction of the host system - TextDirection direction = TextDirection::LeftToRight; - - /// @brief current window mouse focus state - bool mouse_focused = false; - - /// @brief current window keyboard focus state - bool key_focused = false; - - /// @brief windows' current frame mouse state - Mouse mouse{}; - - /// @brief windows' current frame keyboard state - Keyboard key{}; - - /// @brief extent of the viewport the windows' views are in - Vec2 viewport_extent = {}; - - /// @brief current drop data type - DropType drop_type = DropType::None; - - /// @brief drag data associated with the current drag operation (if any, otherwise empty) - Vec drop_data{}; - - /// @brief if a text input came in - bool text_input = false; - - /// @brief current text input data from the IME or keyboard - Vec text{}; - - /// @brief is the application requested to close - bool close_requested = false; - - /// @brief did a window resize happen - bool resized = true; - - /// @brief did a window surface resize happen - bool surface_resized = true; - - bool dropped = false; - - bool drop_hovering = false; - - void begin_frame_(time_point time, nanoseconds delta) - { - timestamp = time; - timedelta = delta; - } - - void clear_frame_() - { - mouse.clear_frame_(); - key.clear_frame_(); - text_input = false; - text.clear(); - resized = false; - surface_resized = false; - - // if the there was a data drop on the last frame clear the buffer - if (dropped) - { - drop_data.clear(); - drop_type = DropType::None; - } - - dropped = false; - drop_hovering = false; - } - - constexpr bool key_down(KeyCode k) const - { - return get_bit(key.downs, (usize) k); - } - - constexpr bool key_up(KeyCode k) const - { - return get_bit(key.ups, (usize) k); - } - - constexpr bool key_state(KeyCode k) const - { - return get_bit(key.states, (usize) k); - } - - constexpr bool key_down(ScanCode k) const - { - return get_bit(key.scan_downs, (usize) k); - } - - constexpr bool key_up(ScanCode k) const - { - return get_bit(key.scan_ups, (usize) k); - } - - constexpr bool key_state(ScanCode k) const - { - return get_bit(key.scan_states, (usize) k); - } - - constexpr bool mouse_down(MouseButton btn) const - { - return get_bit(mouse.downs, (u32) btn); - } - - constexpr bool mouse_up(MouseButton btn) const - { - return get_bit(mouse.ups, (u32) btn); - } - - constexpr bool mouse_state(MouseButton btn) const - { - return get_bit(mouse.states, (u32) btn); - } -}; - /// @brief Global View Context, Properties of the context all the views for /// a specific window are in. -struct ViewContext : FrameState -{ - /// @brief User-provided app context or null - void * app = nullptr; - - /// @brief clipboard system - ClipBoard * clipboard = nullptr; - - FrameState state_buffer{}; - - constexpr ViewContext(void * app, ClipBoard & clipboard) : - app{app}, - clipboard{&clipboard} - { - } - - constexpr ViewContext(ViewContext const &) = delete; - constexpr ViewContext(ViewContext &&) = default; - constexpr ViewContext & operator=(ViewContext const &) = delete; - constexpr ViewContext & operator=(ViewContext &&) = default; - constexpr ~ViewContext() = default; - - void swap_frame_state_() - { - // [ ] todo - } -}; +using ViewContext = InputState; /// @brief makes a zoom transform matrix relative to the center of a viewport. /// defines the translation and scaling components. @@ -523,17 +250,10 @@ struct ViewState /// events bool focusable = false; - /// @brief can receive text input when focused (excluding tab - /// key/non-text keys) - bool text_input = false; - - /// @brief can receive `Tab` key as input when focused - bool tab_input = false; - - /// @brief can receive `Esc` key as input when focused - bool esc_input = false; + /// @brief if set, will be treated as a text input area + Option text = none; - /// @brief user to focus on view + /// @brief grab focus of the user bool grab_focus = false; /// @brief is view a viewport @@ -569,36 +289,36 @@ struct CoreViewTheme f32 focus_thickness = 0; }; +// [ ] simpler color model: +// - primary color +// - text color +// - surface color +// - error, warning, success inline constexpr CoreViewTheme DEFAULT_THEME = { - .background = Vec4U8{0x19, 0x19, 0x19, 0xFF} - .norm(), - .surface = Vec4U8{0x33, 0x33, 0x33, 0xFF} - .norm(), - .primary = mdc::DEEP_ORANGE_600.norm(), - .primary_variant = mdc::DEEP_ORANGE_400.norm(), - .secondary = mdc::PURPLE_600.norm(), - .secondary_variant = mdc::PURPLE_400.norm(), - .error = mdc::RED_500.norm(), - .warning = mdc::YELLOW_800.norm(), - .success = mdc::GREEN_700.norm(), - .active = Vec4U8{0x70, 0x70, 0x70, 0xFF} - .norm(), - .inactive = Vec4U8{0x47, 0x47, 0x47, 0xFF} - .norm(), - .on_background = mdc::WHITE.norm(), - .on_surface = mdc::WHITE.norm(), - .on_primary = mdc::WHITE.norm(), - .on_secondary = mdc::WHITE.norm(), - .on_error = mdc::WHITE.norm(), - .on_warning = mdc::WHITE.norm(), - .on_success = mdc::WHITE.norm(), - .body_font_height = 16, - .h1_font_height = 30, - .h2_font_height = 27, - .h3_font_height = 22, - .line_height = 1.2F, - .focus_thickness = 1 -}; + .background = norm(Vec4U8{0x19, 0x19, 0x19, 0xFF}), + .surface = norm(Vec4U8{0x33, 0x33, 0x33, 0xFF}), + .primary = norm(mdc::DEEP_ORANGE_600), + .primary_variant = norm(mdc::DEEP_ORANGE_400), + .secondary = norm(mdc::PURPLE_600), + .secondary_variant = norm(mdc::PURPLE_400), + .error = norm(mdc::RED_500), + .warning = norm(mdc::YELLOW_800), + .success = norm(mdc::GREEN_700), + .active = norm(Vec4U8{0x70, 0x70, 0x70, 0xFF}), + .inactive = norm(Vec4U8{0x47, 0x47, 0x47, 0xFF}), + .on_background = norm(mdc::WHITE), + .on_surface = norm(mdc::WHITE), + .on_primary = norm(mdc::WHITE), + .on_secondary = norm(mdc::WHITE), + .on_error = norm(mdc::WHITE), + .on_warning = norm(mdc::WHITE), + .on_success = norm(mdc::WHITE), + .body_font_height = 16, + .h1_font_height = 30, + .h2_font_height = 27, + .h3_font_height = 22, + .line_height = 1.2F, + .focus_thickness = 1}; /// @param extent extent of the view within the parent. if it is a viewport, /// this is the visible extent of the viewport within the parent viewport. @@ -611,7 +331,7 @@ struct ViewLayout Vec2 extent = {}; Vec2 viewport_extent = {}; Affine3 viewport_transform = Affine3::identity(); - Option fixed_position = None; + Option fixed_position = none; }; /// @brief Base view class. All view types must inherit from this struct. @@ -624,20 +344,17 @@ struct ViewLayout /// The coordinate system used is one in which the center of the screen is (0, /// 0) and ranges from [-0.5w, +0.5w] on both axes. i.e. top-left is [-0.5w, /// -0.5h] and bottom-right is [+0.5w, +0.5h]. +/// @param id id of the view if mounted, otherwise U64_MAX +/// @param last_rendered_frame last frame the view was rendered at +/// @param focus_idx index in the focus tree +/// @param region canvas-space region of the view struct View { - /// @param id id of the view if mounted, otherwise U64_MAX - /// @param last_rendered_frame last frame the view was rendered at - /// @param focus_idx index in the focus tree - /// @param region canvas-space region of the view - struct Inner - { - u64 id = U64_MAX; - u64 last_rendered_frame = 0; - u32 focus_idx = 0; - CRect region = {}; - f32 zoom = 1; - } inner = {}; + u64 id_ = U64_MAX; + u64 last_rendered_frame_ = 0; + u32 focus_idx_ = 0; + CRect region_ = {}; + f32 zoom_ = 1; constexpr View() = default; constexpr View(View const &) = delete; @@ -649,7 +366,7 @@ struct View /// @returns the ID currently allocated to the view or U64_MAX constexpr u64 id() const { - return inner.id; + return id_; } /// @brief called on every frame. used for state changes, animations, task @@ -762,4 +479,4 @@ struct View } }; -} // namespace ash +} // namespace ash diff --git a/ashura/engine/view_system.h b/ashura/engine/view_system.h index 4569feb5f..802d0f195 100644 --- a/ashura/engine/view_system.h +++ b/ashura/engine/view_system.h @@ -73,19 +73,19 @@ struct ViewSystem struct State { /// @brief mouse pointed view - Option pointed = None; + Option pointed = none; /// @brief drag data soure view - Option drag_src = None; + Option drag_src = none; /// @brief current cursor Cursor cursor = Cursor::Default; /// @brief focus state - Option focus = None; + Option focus = none; /// @brief grab focus state - Option grab_focus = None; + Option grab_focus = none; /// @brief if mouse went down on this frame bool mouse_down = false; @@ -133,6 +133,9 @@ struct ViewSystem /// @brief current frame state State f1 = {}; + /// @brief current frame input state + InputState s1; + Vec views; Vec nodes; @@ -165,32 +168,33 @@ struct ViewSystem Vec focus_ordering; explicit ViewSystem(AllocatorImpl allocator) : - views{allocator}, - nodes{allocator}, - tab_indices{allocator}, - viewports{allocator}, - is_hidden{allocator}, - is_pointable{allocator}, - is_clickable{allocator}, - is_scrollable{allocator}, - is_draggable{allocator}, - is_droppable{allocator}, - is_focusable{allocator}, - is_text_input{allocator}, - is_tab_input{allocator}, - is_esc_input{allocator}, - centers{allocator}, - extents{allocator}, - viewport_extents{allocator}, - viewport_transforms{allocator}, - is_fixed_positioned{allocator}, - fixed_positions{allocator}, - z_indices{allocator}, - layers{allocator}, - transforms{allocator}, - clips{allocator}, - z_ordering{allocator}, - focus_ordering{allocator} + s1{allocator}, + views{allocator}, + nodes{allocator}, + tab_indices{allocator}, + viewports{allocator}, + is_hidden{allocator}, + is_pointable{allocator}, + is_clickable{allocator}, + is_scrollable{allocator}, + is_draggable{allocator}, + is_droppable{allocator}, + is_focusable{allocator}, + is_text_input{allocator}, + is_tab_input{allocator}, + is_esc_input{allocator}, + centers{allocator}, + extents{allocator}, + viewport_extents{allocator}, + viewport_transforms{allocator}, + is_fixed_positioned{allocator}, + fixed_positions{allocator}, + z_indices{allocator}, + layers{allocator}, + transforms{allocator}, + clips{allocator}, + z_ordering{allocator}, + focus_ordering{allocator} { } @@ -242,13 +246,13 @@ struct ViewSystem { // should never happen CHECK(next_id != U64_MAX); - view.inner.id = next_id++; + view.id_ = next_id++; events.mounted = true; } u64 const id = view.id(); - events.view_hit = (view.inner.last_rendered_frame + 1) == frame; + events.view_hit = (view.last_rendered_frame_ + 1) == frame; if (f1.pointed.contains(id)) [[unlikely]] { @@ -282,7 +286,7 @@ struct ViewSystem f1.focus.value().active) [[unlikely]] { events.focus_in = - (f0.focus.value().view != view.id()) || !f0.focus.value().active; + (f0.focus.value().view != view.id()) || !f0.focus.value().active; events.key_down = f1.key_down; events.key_up = f1.key_up; events.text_input = f1.text_input; @@ -300,12 +304,12 @@ struct ViewSystem { views.push(&view).unwrap(); nodes - .push(ViewNode{.depth = depth, - .breadth = breadth, - .parent = parent, - .first_child = 0, - .num_children = 0}) - .unwrap(); + .push(ViewNode{.depth = depth, + .breadth = breadth, + .parent = parent, + .first_child = 0, + .num_children = 0}) + .unwrap(); tab_indices.extend_uninit(1).unwrap(); viewports.extend_uninit(1).unwrap(); is_hidden.extend_uninit(1).unwrap(); @@ -331,8 +335,18 @@ struct ViewSystem push_view(child, depth + 1, num_children++, idx); }; - ViewState s = view.tick(ctx, view.inner.region, view.inner.zoom, - process_events(view), fn(builder)); + ViewState s = view.tick(ctx, view.region_, view.zoom_, process_events(view), + fn(builder)); + + bool const attr_text_input = s.text.is_some(); + bool attr_tab_input = false; + bool attr_esc_input = false; + + if (attr_text_input) + { + attr_tab_input = s.text.value().tab_input; + attr_esc_input = s.text.value().esc_input; + } tab_indices.set(idx, (s.tab == I32_MIN) ? tab_index : s.tab); viewports.set(idx, viewport); @@ -343,21 +357,19 @@ struct ViewSystem is_draggable.set(idx, s.draggable); is_droppable.set(idx, s.droppable); is_focusable.set(idx, s.focusable); - is_text_input.set(idx, s.text_input); - is_tab_input.set(idx, s.tab_input); - is_esc_input.set(idx, s.esc_input); + is_text_input.set(idx, attr_text_input); + is_tab_input.set(idx, attr_tab_input); + is_esc_input.set(idx, attr_esc_input); is_viewport.set(idx, s.viewport); if (!s.hidden && s.focusable && s.grab_focus) [[unlikely]] { - f1.focus = Some{ - Focus{.active = true, - .view = view.id(), - .focus_idx = view.inner.focus_idx, - .text_input = s.text_input, - .tab_input = s.tab_input, - .esc_input = s.esc_input} - }; + f1.focus = Focus{.active = true, + .view = view.id(), + .focus_idx = view.focus_idx_, + .text_input = attr_text_input, + .tab_input = attr_tab_input, + .esc_input = attr_esc_input}; } nodes[idx].first_child = first_child; @@ -389,7 +401,7 @@ struct ViewSystem for (u32 i = 0; i < views.size32(); i++) { - views[focus_ordering[i]]->inner.focus_idx = i; + views[focus_ordering[i]]->focus_idx_ = i; } } @@ -420,8 +432,8 @@ struct ViewSystem i--; ViewNode const & node = nodes[i]; ViewLayout layout = views[i]->fit( - extents[i], extents.view().slice(node.first_child, node.num_children), - centers.view().slice(node.first_child, node.num_children)); + extents[i], extents.view().slice(node.first_child, node.num_children), + centers.view().slice(node.first_child, node.num_children)); extents[i] = layout.extent; viewport_extents[i] = layout.viewport_extent; viewport_transforms[i] = layout.viewport_transform; @@ -448,12 +460,12 @@ struct ViewSystem c++) { transforms[c] = - // apply viewport-space transform - viewport_transform - // apply parent-space transform - * translate2d(centers[c]) - // first use accumulated ancestor transform - * ancestor_transform; + // apply viewport-space transform + viewport_transform + // apply parent-space transform + * translate2d(centers[c]) + // first use accumulated ancestor transform + * ancestor_transform; } } @@ -463,7 +475,7 @@ struct ViewSystem Affine3 const & transform = transforms[i]; f32 const zoom = transform[0][0]; centers[i] = - ash::transform(transform, Vec2{0, 0}) + viewport_extent * 0.5F; + ash::transform(transform, Vec2{0, 0}) + viewport_extent * 0.5F; extents[i] = extents[i] * zoom; viewport_extents[i] = viewport_extents[i] * zoom; } @@ -508,9 +520,9 @@ struct ViewSystem for (u32 i = 0; i < n; i++) { - View & view = *views[i]; - view.inner.region = CRect{.center = centers[i], .extent = extents[i]}; - view.inner.zoom = transforms[i][0][0]; + View & view = *views[i]; + view.region_ = CRect{.center = centers[i], .extent = extents[i]}; + view.zoom_ = transforms[i][0][0]; } } @@ -553,8 +565,8 @@ struct ViewSystem { ViewNode const & node = nodes[i]; z_indices[i] = views[i]->z_index( - z_indices[i], - z_indices.view().slice(node.first_child, node.num_children)); + z_indices[i], + z_indices.view().slice(node.first_child, node.num_children)); } layers[0] = 0; @@ -571,8 +583,9 @@ struct ViewSystem // sort layers indirect_sort(z_ordering.view(), [&](u32 a, u32 b) { - return z_order_cmp(layers[a], z_indices[a], nodes[a].depth, layers[b], - z_indices[b], nodes[b].depth) == Ordering::Less; + Ordering ord = z_order_cmp(layers[a], z_indices[a], nodes[a].depth, + layers[b], z_indices[b], nodes[b].depth); + return ord == Ordering::Less; }); } @@ -596,9 +609,9 @@ struct ViewSystem CRect region{.center = centers[i], .extent = extents[i]}; CRect const & clip = clips[i]; bool const hidden = - !overlaps(region, clip) || - !overlaps(region, CRect::from_offset({0, 0}, viewport_extent)) || - !region.is_visible() || !clip.is_visible(); + !overlaps(region, clip) || + !overlaps(region, CRect::from_offset({0, 0}, viewport_extent)) || + !region.is_visible() || !clip.is_visible(); is_hidden.set(i, hidden); } @@ -613,10 +626,18 @@ struct ViewSystem { View & view = *views[i]; canvas.clip(clips[i]); - view.render(canvas, view.inner.region, view.inner.zoom, clips[i]); - view.inner.last_rendered_frame = frame; + view.render(canvas, view.region_, view.zoom_, clips[i]); + view.last_rendered_frame_ = frame; } } + + // [ ] fix this + // canvas.blur( + // CRect{ + // .center = {1'920 / 2.0, 1'080 / 2.0}, + // .extent = {1920, 750 } + // }, + // 2); } void focus_view(u32 view) @@ -636,8 +657,8 @@ struct ViewSystem static constexpr bool hit_test(View & view, Vec2 position) { - return contains(view.inner.region, position) && - view.hit(view.inner.region, view.inner.zoom, position); + return contains(view.region_, position) && + view.hit(view.region_, view.zoom_, position); } Option hit_views(Vec2 mouse_position, ViewHitAttributes match) const @@ -682,13 +703,13 @@ struct ViewSystem } if (!is_hidden[i] && matches && hit_test(view, mouse_position)) - [[unlikely]] + [[unlikely]] { - return Some{i}; + return i; } } - return None; + return none; } Option navigate_focus(u32 from, bool forward) const @@ -697,7 +718,7 @@ struct ViewSystem if (n == 0) { - return None; + return none; } if (from > n) @@ -720,7 +741,7 @@ struct ViewSystem f = (f + 1) % n; } - return Some{f}; + return f; } else { @@ -738,7 +759,7 @@ struct ViewSystem f = (f + 1) % n; } - return Some{(n - 1) - f}; + return (n - 1) - f; } } @@ -761,39 +782,35 @@ struct ViewSystem // use grab focus request if any, otherwise persist previous frame's focus f1.focus = f0.grab_focus ? f0.grab_focus : f0.focus; - // [ ] start and end text input on mobile platforms - // mouse click & drag if (f1.mouse_down) { hit_views(ctx.mouse.position, ViewHitAttributes::Clickable | ViewHitAttributes::Draggable) - .match( - [&](u32 i) { - View & view = *views[i]; - - f1.pointed = Some{view.id()}; - - if (ctx.mouse_down(MouseButton::Primary) && is_draggable[i]) - { - f1.dragging = true; - f1.drag_src = Some{view.id()}; - f1.drag_start = true; - } - - f1.cursor = view.cursor(view.inner.region, view.inner.zoom, - ctx.mouse.position); - - f1.focus = Some{ - Focus{.active = false, - .view = view.id(), - .focus_idx = view.inner.focus_idx, - .text_input = is_text_input[i], - .tab_input = is_tab_input[i], - .esc_input = is_esc_input[i]} - }; - }, - [&]() { f1.focus = None; }); + .match( + [&](u32 i) { + View & view = *views[i]; + + f1.pointed = view.id(); + + if (ctx.mouse_down(MouseButton::Primary) && is_draggable[i]) + { + f1.dragging = true; + f1.drag_src = view.id(); + f1.drag_start = true; + } + + f1.cursor = + view.cursor(view.region_, view.zoom_, ctx.mouse.position); + + f1.focus = Focus{.active = false, + .view = view.id(), + .focus_idx = view.focus_idx_, + .text_input = is_text_input[i], + .tab_input = is_tab_input[i], + .esc_input = is_esc_input[i]}; + }, + [&]() { f1.focus = none; }); } // mouse drop event else if ((f0.dragging && ctx.mouse_up(MouseButton::Primary)) || ctx.dropped) @@ -803,12 +820,11 @@ struct ViewSystem f1.dragging = false; hit_views(ctx.mouse.position, ViewHitAttributes::Droppable) - .match([&](u32 i) { - View & view = *views[i]; - f1.pointed = Some{view.id()}; - f1.cursor = view.cursor(view.inner.region, view.inner.zoom, - ctx.mouse.position); - }); + .match([&](u32 i) { + View & view = *views[i]; + f1.pointed = view.id(); + f1.cursor = view.cursor(view.region_, view.zoom_, ctx.mouse.position); + }); } // mouse dragging update event else if (f0.dragging || ctx.drop_hovering) @@ -817,48 +833,43 @@ struct ViewSystem f1.dragging = true; hit_views(ctx.mouse.position, ViewHitAttributes::Droppable) - .match([&](u32 i) { - View & view = *views[i]; - f1.pointed = Some{view.id()}; - f1.cursor = view.cursor(view.inner.region, view.inner.zoom, - ctx.mouse.position); - }); + .match([&](u32 i) { + View & view = *views[i]; + f1.pointed = view.id(); + f1.cursor = view.cursor(view.region_, view.zoom_, ctx.mouse.position); + }); } // mouse release event else if (f1.mouse_up) { hit_views(ctx.mouse.position, ViewHitAttributes::Clickable) - .match([&](u32 i) { - View & view = *views[i]; - f1.pointed = Some{view.id()}; - f1.cursor = view.cursor(view.inner.region, view.inner.zoom, - ctx.mouse.position); - }); + .match([&](u32 i) { + View & view = *views[i]; + f1.pointed = view.id(); + f1.cursor = view.cursor(view.region_, view.zoom_, ctx.mouse.position); + }); } // mouse scroll event else if (ctx.mouse.wheel_scrolled) { hit_views(ctx.mouse.position, ViewHitAttributes::Scrollable) - .match([&](u32 i) { - View & view = *views[i]; - f1.pointed = Some{view.id()}; - f1.cursor = view.cursor(view.inner.region, view.inner.zoom, - ctx.mouse.position); - }); + .match([&](u32 i) { + View & view = *views[i]; + f1.pointed = view.id(); + f1.cursor = view.cursor(view.region_, view.zoom_, ctx.mouse.position); + }); } // mouse pointing event else { - hit_views(ctx.mouse.position, ViewHitAttributes::Pointable | - ViewHitAttributes::Clickable | - ViewHitAttributes::Draggable | - ViewHitAttributes::Scrollable) - .match([&](u32 i) { - View & view = *views[i]; - f1.pointed = Some{view.id()}; - f1.cursor = view.cursor(view.inner.region, view.inner.zoom, - ctx.mouse.position); - }); + hit_views(ctx.mouse.position, + ViewHitAttributes::Pointable | ViewHitAttributes::Clickable | + ViewHitAttributes::Draggable | ViewHitAttributes::Scrollable) + .match([&](u32 i) { + View & view = *views[i]; + f1.pointed = view.id(); + f1.cursor = view.cursor(view.region_, view.zoom_, ctx.mouse.position); + }); } // determine focus navigation direction @@ -885,7 +896,7 @@ struct ViewSystem { if (!(f1.focus.is_some() && f1.focus.value().esc_input)) { - f1.focus = None; + f1.focus = none; } } @@ -905,15 +916,15 @@ struct ViewSystem } f1.focus = navigate_focus(from, focus_action == FocusAction::Forward) - .map([&](u32 focus_idx) { - u32 const i = focus_ordering[focus_idx]; - return Focus{.active = true, - .view = views[i]->id(), - .focus_idx = focus_idx, - .text_input = is_text_input[i], - .tab_input = is_tab_input[i], - .esc_input = is_esc_input[i]}; - }); + .map([&](u32 focus_idx) { + u32 const i = focus_ordering[focus_idx]; + return Focus{.active = true, + .view = views[i]->id(), + .focus_idx = focus_idx, + .text_input = is_text_input[i], + .tab_input = is_tab_input[i], + .esc_input = is_esc_input[i]}; + }); } break; default: @@ -924,15 +935,12 @@ struct ViewSystem // [ ] call focus_view() once a new focus navigation has occured } - // bool should_show_text_input() const - // { - // return state[1].focus.text_input; - // } - - void tick(ViewContext const & ctx, View & root, Canvas & canvas) + void tick(InputState const & input, View & root, Canvas & canvas, + Fn loop) { clear(); - build(ctx, root); + + build(s1, root); u32 const n = views.size32(); centers.resize_uninit(n).unwrap(); extents.resize_uninit(n).unwrap(); @@ -947,15 +955,24 @@ struct ViewSystem z_ordering.resize_uninit(n).unwrap(); focus_ordering.resize_uninit(n).unwrap(); + loop(input); + // Option; + // [ ] start and end text input on mobile platforms + // [ ] change cursor + // [ ] text input + focus_order(); - layout(ctx.viewport_extent); + + layout(input.viewport_extent); stack(); - visibility(ctx.viewport_extent); - events(ctx); + visibility(input.viewport_extent); render(canvas); + events(input); + input.clone_to(s1); + frame++; } }; -} // namespace ash +} // namespace ash diff --git a/ashura/engine/views.cc b/ashura/engine/views.cc index 0b259e4ab..4953510df 100644 --- a/ashura/engine/views.cc +++ b/ashura/engine/views.cc @@ -5,43 +5,35 @@ namespace ash { -void ScalarDragBox::scalar_parse(Span text, ScalarState & styling) +void ScalarDragBox::scalar_parse(Span text, ScalarSpec const & spec, + Scalar & scalar) { if (text.is_empty()) { return; } - switch (styling.base.type) - { - case ScalarInputType::i32: - { - i32 value = 0; + spec.match( + [&](F32InputSpec const & spec) { + f32 value = 0; auto [ptr, ec] = - fast_float::from_chars(text.pbegin(), text.pend(), value); - if (ec != std::errc{} || value < styling.min.i32 || - value > styling.max.i32) + fast_float::from_chars(text.pbegin(), text.pend(), value); + if (ec != std::errc{} || value < spec.min || value > spec.max) { return; } - styling.current = ScalarInput{.i32 = value, .type = ScalarInputType::i32}; - } - break; - - case ScalarInputType::f32: - { - f32 value = 0; + scalar = value; + }, + [&](I32InputSpec const & spec) { + i32 value = 0; auto [ptr, ec] = - fast_float::from_chars(text.pbegin(), text.pend(), value); - if (ec != std::errc{} || value < styling.min.f32 || - value > styling.max.f32) + fast_float::from_chars(text.pbegin(), text.pend(), value); + if (ec != std::errc{} || value < spec.min || value > spec.max) { return; } - styling.current = ScalarInput{.f32 = value, .type = ScalarInputType::f32}; - } - break; - } + scalar = value; + }); } -} // namespace ash +} // namespace ash diff --git a/ashura/engine/views.h b/ashura/engine/views.h index e8e0f17ec..39b3263cb 100644 --- a/ashura/engine/views.h +++ b/ashura/engine/views.h @@ -40,6 +40,8 @@ struct FocusState } }; +// [ ] render hooks? +// [ ] debouncing struct PressState { bool in = false; @@ -142,65 +144,62 @@ struct FlexView : View MainAlign main_align = MainAlign::Start; f32 cross_align = 0; Frame frame = Frame{}.scale(1, 1); - } styling; + } style; - struct Inner - { - Vec items{default_allocator}; - } inner; + Vec items_{default_allocator}; FlexView & axis(Axis a) { - styling.axis = a; + style.axis = a; return *this; } FlexView & wrap(bool w) { - styling.wrap = w; + style.wrap = w; return *this; } FlexView & main_align(MainAlign align) { - styling.main_align = align; + style.main_align = align; return *this; } FlexView & cross_align(f32 align) { - styling.cross_align = align; + style.cross_align = align; return *this; } FlexView & frame(f32 width, f32 height, bool constrain = true) { - styling.frame = Frame{width, height, constrain}; + style.frame = Frame{width, height, constrain}; return *this; } FlexView & frame(Frame f) { - styling.frame = f; + style.frame = f; return *this; } FlexView & items(std::initializer_list list) { - inner.items.extend(span(list)).unwrap(); + items_.extend(span(list)).unwrap(); return *this; } FlexView & items(Span list) { - inner.items.extend(list).unwrap(); + items_.extend(list).unwrap(); return *this; } virtual ViewState tick(ViewContext const &, CRect const &, f32, ViewEvents const &, Fn build) override { - for (View * item : inner.items) + for (View * item : items_) { build(*item); } @@ -210,7 +209,7 @@ struct FlexView : View virtual void size(Vec2 allocated, Span sizes) override { - Vec2 const frame = styling.frame(allocated); + Vec2 const frame = style.frame(allocated); fill(sizes, frame); } @@ -218,9 +217,9 @@ struct FlexView : View Span centers) override { u32 const n = sizes.size32(); - Vec2 const frame = styling.frame(allocated); - u8 const main_axis = (styling.axis == Axis::X) ? 0 : 1; - u8 const cross_axis = (styling.axis == Axis::X) ? 1 : 0; + Vec2 const frame = style.frame(allocated); + u32 const main_axis = (style.axis == Axis::X) ? 0 : 1; + u32 const cross_axis = (style.axis == Axis::X) ? 1 : 0; Vec2 span = {}; f32 cross_cursor = 0; @@ -231,7 +230,7 @@ struct FlexView : View f32 cross_extent = sizes[first][cross_axis]; f32 main_spacing = 0; - while (i < n && !(styling.wrap && + while (i < n && !(style.wrap && (main_extent + sizes[i][main_axis]) > frame[main_axis])) { main_extent += sizes[i][main_axis]; @@ -241,19 +240,19 @@ struct FlexView : View u32 const count = i - first; - if (styling.main_align != MainAlign::Start) + if (style.main_align != MainAlign::Start) { main_spacing = max(frame[main_axis] - main_extent, 0.0F); } for (u32 b = first; b < first + count; b++) { - f32 const pos = space_align(cross_extent, sizes[b][cross_axis], - styling.cross_align); + f32 const pos = + space_align(cross_extent, sizes[b][cross_axis], style.cross_align); centers[b][cross_axis] = cross_cursor + cross_extent * 0.5F + pos; } - switch (styling.main_align) + switch (style.main_align) { case MainAlign::Start: { @@ -346,52 +345,49 @@ struct StackView : View bool reverse = false; Vec2 alignment = {0, 0}; Frame frame = Frame{}.scale(1, 1); - } styling; + } style; - struct Inner - { - Vec items{default_allocator}; - } inner; + Vec items_{default_allocator}; StackView & reverse(bool r) { - styling.reverse = r; + style.reverse = r; return *this; } StackView & align(f32 x, f32 y) { - styling.alignment = Vec2{x, y}; + style.alignment = Vec2{x, y}; return *this; } StackView & align(Vec2 a) { - styling.alignment = a; + style.alignment = a; return *this; } StackView & frame(f32 width, f32 height, bool constrain = true) { - styling.frame = Frame{width, height, constrain}; + style.frame = Frame{width, height, constrain}; return *this; } StackView & frame(Frame f) { - styling.frame = f; + style.frame = f; return *this; } StackView & items(std::initializer_list list) { - inner.items.extend(span(list)).unwrap(); + items_.extend(span(list)).unwrap(); return *this; } StackView & items(Span list) { - inner.items.extend(list).unwrap(); + items_.extend(list).unwrap(); return *this; } @@ -399,7 +395,7 @@ struct StackView : View { // sequential stacking i32 z = base; - if (!styling.reverse) + if (!style.reverse) { z += (i32) i; } @@ -413,7 +409,7 @@ struct StackView : View virtual ViewState tick(ViewContext const &, CRect const &, f32, ViewEvents const &, Fn build) override { - for (View * item : inner.items) + for (View * item : items_) { build(*item); } @@ -423,7 +419,7 @@ struct StackView : View virtual void size(Vec2 allocated, Span sizes) override { - fill(sizes, styling.frame(allocated)); + fill(sizes, style.frame(allocated)); } virtual ViewLayout fit(Vec2, Span sizes, @@ -432,15 +428,15 @@ struct StackView : View Vec2 span; u32 const n = sizes.size32(); - for (Vec2 styling : sizes) + for (Vec2 style : sizes) { - span.x = max(span.x, styling.x); - span.y = max(span.y, styling.y); + span.x = max(span.x, style.x); + span.y = max(span.y, style.y); } for (u32 i = 0; i < n; i++) { - centers[i] = space_align(span, sizes[i], styling.alignment); + centers[i] = space_align(span, sizes[i], style.alignment); } return {.extent = span}; @@ -467,20 +463,17 @@ struct TextView : View struct Style { TextHighlightStyle cursor_highlight; - } styling; + } style; - struct Inner - { - RenderText text{}; - TextCompositor compositor{}; - } inner; + RenderText text_{}; + TextCompositor compositor_{}; TextView() { - inner.text.style(TextStyle{.foreground = DEFAULT_THEME.on_surface}, - FontStyle{.font = engine->default_font, - .font_height = DEFAULT_THEME.body_font_height, - .line_height = DEFAULT_THEME.line_height}); + text_.run(TextStyle{.color = DEFAULT_THEME.on_surface}, + FontStyle{.font = engine->default_font, + .font_height = DEFAULT_THEME.body_font_height, + .line_height = DEFAULT_THEME.line_height}); } virtual ~TextView() override = default; @@ -493,44 +486,44 @@ struct TextView : View TextView & highlight(TextHighlight highlight) { - inner.text.highlight(highlight); + text_.highlight(highlight); return *this; } TextView & clear_highlights() { - inner.text.clear_highlights(); + text_.clear_highlights(); return *this; } TextView & cursor_highlight_style(TextHighlightStyle s) { - styling.cursor_highlight = s; + style.cursor_highlight = s; return *this; } - TextView & style(TextStyle const & style, FontStyle const & font, - u32 first = 0, u32 count = U32_MAX) + TextView & run(TextStyle const & style, FontStyle const & font, u32 first = 0, + u32 count = U32_MAX) { - inner.text.style(style, font, first, count); + text_.run(style, font, first, count); return *this; } TextView & text(Span t) { - inner.text.set_text(t); + text_.set_text(t); return *this; } TextView & text(Span t) { - inner.text.set_text(t); + text_.set_text(t); return *this; } Span text() { - return inner.text.get_text(); + return text_.get_text(); } virtual ViewState tick(ViewContext const & ctx, CRect const & region, @@ -547,28 +540,28 @@ struct TextView : View cmd = TextCommand::HitSelect; } - inner.compositor.command(inner.text.get_text(), inner.text.inner.layout, - region.extent.x, inner.text.inner.alignment, cmd, - noop, noop, {}, *ctx.clipboard, 1, - (ctx.mouse.position - region.center) * zoom); + compositor_.command(text_.get_text(), text_.layout_, region.extent.x, + text_.alignment_, cmd, noop, noop, {}, + *engine->clipboard, 1, + (ctx.mouse.position - region.center) * zoom); return ViewState{.draggable = state.copyable}; } virtual ViewLayout fit(Vec2 allocated, Span, Span) override { - inner.text.layout(allocated.x); - return {.extent = inner.text.inner.layout.extent}; + text_.layout(allocated.x); + return {.extent = text_.layout_.extent}; } virtual void render(Canvas & canvas, CRect const & region, f32 zoom, CRect const & clip) override { - highlight(TextHighlight{.slice = inner.compositor.get_cursor().as_slice( - inner.text.get_text().size32()), - .style = styling.cursor_highlight}); - inner.text.render(canvas, region, clip, zoom); - inner.text.inner.highlights.pop(); + highlight(TextHighlight{ + .slice = compositor_.get_cursor().as_slice(text_.get_text().size32()), + .style = style.cursor_highlight}); + text_.render(canvas, region, clip, zoom); + text_.highlights_.pop(); } virtual Cursor cursor(CRect const &, f32, Vec2) override @@ -581,6 +574,7 @@ struct TextView : View // [ ] viewport text with scrollable region, scroll direction // [ ] scroll on pagedown and cursor change // [ ] text input while in view, i.e. page down +// [ ] managing large text input!!! i.e editors struct TextInput : View { struct State @@ -599,7 +593,7 @@ struct TextInput : View { TextHighlightStyle highlight = {}; FocusStyle focus = {}; - } styling; + } style; struct Callbacks { @@ -609,23 +603,20 @@ struct TextInput : View Fn focus_out = noop; } cb; - struct Inner - { - RenderText content{}; - RenderText stub{}; - TextCompositor compositor{}; - } inner; + RenderText content_{}; + RenderText stub_{}; + TextCompositor compositor_{}; TextInput() { - inner.content.style(TextStyle{.foreground = DEFAULT_THEME.on_surface}, - FontStyle{.font = engine->default_font, - .font_height = DEFAULT_THEME.body_font_height, - .line_height = DEFAULT_THEME.line_height}); - inner.stub.style(TextStyle{.foreground = DEFAULT_THEME.on_surface}, - FontStyle{.font = engine->default_font, - .font_height = DEFAULT_THEME.body_font_height, - .line_height = DEFAULT_THEME.line_height}); + content_.run(TextStyle{.color = DEFAULT_THEME.on_surface}, + FontStyle{.font = engine->default_font, + .font_height = DEFAULT_THEME.body_font_height, + .line_height = DEFAULT_THEME.line_height}); + stub_.run(TextStyle{.color = DEFAULT_THEME.on_surface}, + FontStyle{.font = engine->default_font, + .font_height = DEFAULT_THEME.body_font_height, + .line_height = DEFAULT_THEME.line_height}); } virtual ~TextInput() override = default; @@ -651,13 +642,13 @@ struct TextInput : View TextInput & highlight(TextHighlight const & highlight) { - inner.content.inner.highlights.push(highlight).unwrap(); + content_.highlights_.push(highlight).unwrap(); return *this; } TextInput & clear_highlights() { - inner.content.inner.highlights.clear(); + content_.highlights_.clear(); return *this; } @@ -687,39 +678,39 @@ struct TextInput : View TextInput & content(Span t) { - inner.content.set_text(t); + content_.set_text(t); return *this; } TextInput & content(Span t) { - inner.content.set_text(t); + content_.set_text(t); return *this; } - TextInput & content_style(TextStyle const & style, FontStyle const & font, - u32 first = 0, u32 count = U32_MAX) + TextInput & content_run(TextStyle const & style, FontStyle const & font, + u32 first = 0, u32 count = U32_MAX) { - inner.content.style(style, font, first, count); + content_.run(style, font, first, count); return *this; } TextInput & stub(Span t) { - inner.stub.set_text(t); + stub_.set_text(t); return *this; } TextInput & stub(Span t) { - inner.stub.set_text(t); + stub_.set_text(t); return *this; } - TextInput & stub_style(TextStyle const & style, FontStyle const & font, - u32 first = 0, u32 count = U32_MAX) + TextInput & stub_run(TextStyle const & style, FontStyle const & font, + u32 first = 0, u32 count = U32_MAX) { - inner.stub.style(style, font, first, count); + stub_.run(style, font, first, count); return *this; } @@ -851,16 +842,16 @@ struct TextInput : View Fn) override { bool edited = false; - auto erase = [this, &edited](Slice32 styling) { - this->inner.content.inner.text.erase(styling); - edited |= styling.is_empty(); - this->inner.content.flush_text(); + auto erase = [this, &edited](Slice32 style) { + this->content_.text_.erase(style); + edited |= style.is_empty(); + this->content_.flush_text(); }; auto insert = [this, &edited](u32 pos, Span t) { - CHECK(this->inner.content.inner.text.insert_span(pos, t)); + CHECK(this->content_.text_.insert_span(pos, t)); edited |= t.is_empty(); - this->inner.content.flush_text(); + this->content_.flush_text(); }; state.editing = false; @@ -886,26 +877,25 @@ struct TextInput : View cmd = command(ctx); } - if (inner.content.inner.layout.lines.is_empty()) + if (content_.layout_.lines.is_empty()) { state.lines_per_page = 1; } else { state.lines_per_page = - (u32) (region.extent.y / - (inner.content.inner.layout.lines[0].metrics.height * zoom)); + (u32) (region.extent.y / + (content_.layout_.lines[0].metrics.height * zoom)); } Vec text_input_utf32{default_allocator}; utf8_decode(ctx.text, text_input_utf32).unwrap(); - inner.compositor.command( - inner.content.inner.text, inner.content.inner.layout, region.extent.x, - inner.content.inner.alignment, cmd, fn(insert), fn(erase), - text_input_utf32, *ctx.clipboard, state.lines_per_page, - (ctx.mouse.position - region.center) * zoom); + compositor_.command( + content_.text_, content_.layout_, region.extent.x, content_.alignment_, + cmd, fn(insert), fn(erase), text_input_utf32, *engine->clipboard, + state.lines_per_page, (ctx.mouse.position - region.center) * zoom); if (edited) { @@ -914,7 +904,7 @@ struct TextInput : View if (events.focus_out) { - inner.compositor.unselect(); + compositor_.unselect(); } if (events.key_down && ctx.key_state(KeyCode::Return) && @@ -943,38 +933,40 @@ struct TextInput : View cb.edit(); } - return ViewState{.draggable = !state.disabled, - .focusable = !state.disabled, - .text_input = !state.disabled, - .tab_input = state.tab_input, - .grab_focus = events.mouse_down}; + return ViewState{ + .draggable = !state.disabled, + .focusable = !state.disabled, + .text = TextInputInfo{.multiline = state.multiline, + .tab_input = state.tab_input}, + .grab_focus = events.mouse_down + }; } virtual ViewLayout fit(Vec2 allocated, Span, Span) override { - if (inner.content.inner.text.is_empty()) + if (content_.text_.is_empty()) { - inner.stub.layout(allocated.x); - return {.extent = inner.stub.inner.layout.extent}; + stub_.layout(allocated.x); + return {.extent = stub_.layout_.extent}; } - inner.content.layout(allocated.x); - return {.extent = inner.content.inner.layout.extent}; + content_.layout(allocated.x); + return {.extent = content_.layout_.extent}; } virtual void render(Canvas & canvas, CRect const & region, f32 zoom, CRect const & clip) override { - if (inner.content.inner.text.is_empty()) + if (content_.text_.is_empty()) { - inner.stub.render(canvas, region, clip, zoom); + stub_.render(canvas, region, clip, zoom); } else { - highlight(TextHighlight{.slice = inner.compositor.get_cursor().as_slice( - inner.content.inner.text.size32()), - .style = styling.highlight}); - inner.content.render(canvas, region, clip, zoom); - inner.content.inner.highlights.pop(); + highlight(TextHighlight{ + .slice = compositor_.get_cursor().as_slice(content_.text_.size32()), + .style = style.highlight}); + content_.render(canvas, region, clip, zoom); + content_.highlights_.pop(); } if (state.focus.focused) @@ -982,8 +974,8 @@ struct TextInput : View canvas.rect({.center = region.center, .extent = region.extent, .stroke = 1, - .thickness = styling.focus.border_thickness, - .tint = styling.focus.border_color}); + .thickness = style.focus.border_thickness, + .tint = style.focus.border_color}); } } @@ -1012,7 +1004,7 @@ struct Button : View Frame frame = {}; Frame padding = {}; FocusStyle focus = {}; - } styling; + } style; struct Callbacks { @@ -1042,7 +1034,7 @@ struct Button : View virtual void size(Vec2 allocated, Span sizes) override { - Vec2 size = allocated - 2 * styling.padding(allocated); + Vec2 size = allocated - 2 * style.padding(allocated); size.x = max(size.x, 0.0F); size.y = max(size.y, 0.0F); fill(sizes, size); @@ -1053,44 +1045,42 @@ struct Button : View { fill(centers, Vec2{0, 0}); return {.extent = (sizes.is_empty() ? Vec2{0, 0} : sizes[0]) + - 2 * styling.padding(allocated)}; + 2 * style.padding(allocated)}; } virtual void render(Canvas & canvas, CRect const & region, f32, CRect const &) override { ColorGradient tint = (state.press.hovered && !state.press.held) ? - styling.hovered_color : - styling.color; - tint = state.disabled ? styling.disabled_color : tint; - canvas.rrect({.center = region.center, - .extent = region.extent, - .corner_radii = styling.corner_radii(region.extent.y), - .stroke = styling.stroke, - .thickness = styling.thickness, - .tint = tint}); + style.hovered_color : + style.color; + tint = state.disabled ? style.disabled_color : tint; + canvas.squircle({.center = region.center, + .extent = region.extent, + .corner_radii = style.corner_radii(region.extent.y), + .stroke = style.stroke, + .thickness = style.thickness, + .tint = tint}, + 1, 256); if (state.press.focus.focused) { canvas.rect({.center = region.center, .extent = region.extent, .stroke = 1, - .thickness = styling.focus.border_thickness, - .tint = styling.focus.border_color}); + .thickness = style.focus.border_thickness, + .tint = style.focus.border_color}); } } }; struct TextButton : Button { - struct Inner - { - TextView text{}; - } inner; + TextView text_{}; TextButton() { - inner.text.state.copyable = false; + text_.state.copyable = false; } virtual ~TextButton() override = default; @@ -1101,70 +1091,70 @@ struct TextButton : Button return *this; } - TextButton & style(TextStyle const & style, FontStyle const & font, - u32 first = 0, u32 count = U32_MAX) + TextButton & run(TextStyle const & style, FontStyle const & font, + u32 first = 0, u32 count = U32_MAX) { - inner.text.style(style, font, first, count); + text_.run(style, font, first, count); return *this; } TextButton & text(Span t) { - inner.text.text(t); + text_.text(t); return *this; } TextButton & text(Span t) { - inner.text.text(t); + text_.text(t); return *this; } TextButton & color(ColorGradient c) { - styling.color = c; + style.color = c; return *this; } TextButton & hovered_color(ColorGradient c) { - styling.hovered_color = c; + style.hovered_color = c; return *this; } TextButton & disabled_color(ColorGradient c) { - styling.disabled_color = c; + style.disabled_color = c; return *this; } TextButton & corner_radii(CornerRadii c) { - styling.corner_radii = c; + style.corner_radii = c; return *this; } TextButton & frame(f32 width, f32 height, bool constrain = true) { - styling.frame = Frame{width, height, constrain}; + style.frame = Frame{width, height, constrain}; return *this; } TextButton & frame(Frame f) { - styling.frame = f; + style.frame = f; return *this; } TextButton & padding(f32 x, f32 y) { - styling.padding = Frame{x, y}; + style.padding = Frame{x, y}; return *this; } TextButton & padding(Frame p) { - styling.padding = p; + style.padding = p; return *this; } @@ -1185,7 +1175,7 @@ struct TextButton : Button Fn build) override { ViewState state = Button::tick(ctx, region, zoom, events, build); - build(inner.text); + build(text_); return state; } }; @@ -1210,7 +1200,7 @@ struct CheckBox : View CornerRadii corner_radii = Size{.scale = 0.125F}; Frame frame{20, 20}; FocusStyle focus = {}; - } styling; + } style; struct Callbacks { @@ -1225,55 +1215,55 @@ struct CheckBox : View CheckBox & box_color(ColorGradient c) { - styling.box_color = c; + style.box_color = c; return *this; } CheckBox & box_hovered_color(ColorGradient c) { - styling.box_hovered_color = c; + style.box_hovered_color = c; return *this; } CheckBox & tick_color(ColorGradient c) { - styling.tick_color = c; + style.tick_color = c; return *this; } CheckBox & stroke(f32 s) { - styling.stroke = s; + style.stroke = s; return *this; } CheckBox & thickness(f32 t) { - styling.thickness = t; + style.thickness = t; return *this; } CheckBox & tick_thickness(f32 t) { - styling.tick_thickness = t; + style.tick_thickness = t; return *this; } CheckBox & corner_radii(CornerRadii r) { - styling.corner_radii = r; + style.corner_radii = r; return *this; } CheckBox & frame(f32 width, f32 height, bool constrain = true) { - styling.frame = Frame{width, height, constrain}; + style.frame = Frame{width, height, constrain}; return *this; } CheckBox & frame(Frame f) { - styling.frame = f; + style.frame = f; return *this; } @@ -1301,7 +1291,7 @@ struct CheckBox : View virtual ViewLayout fit(Vec2 allocated, Span, Span) override { - Vec2 extent = styling.frame(allocated); + Vec2 extent = style.frame(allocated); return {.extent = Vec2::splat(min(extent.x, extent.y))}; } @@ -1309,12 +1299,12 @@ struct CheckBox : View CRect const &) override { ColorGradient tint = - (state.press.hovered && !state.press.held && !state.disabled) ? - styling.box_hovered_color : - styling.box_color; + (state.press.hovered && !state.press.held && !state.disabled) ? + style.box_hovered_color : + style.box_color; canvas.rrect({.center = region.center, .extent = region.extent, - .corner_radii = styling.corner_radii(region.extent.y), + .corner_radii = style.corner_radii(region.extent.y), .stroke = 1, .thickness = 2, .tint = tint}); @@ -1322,14 +1312,14 @@ struct CheckBox : View if (state.value) { canvas.line( - { - .center = region.center, - .extent = region.extent, - .stroke = 0, - .thickness = styling.tick_thickness, - .tint = styling.tick_color + { + .center = region.center, + .extent = region.extent, + .stroke = 0, + .thickness = style.tick_thickness, + .tint = style.tick_color }, - span({{0.125F, 0.5F}, {0.374F, 0.75F}, {0.775F, 0.25F}})); + span({{0.125F, 0.5F}, {0.374F, 0.75F}, {0.775F, 0.25F}})); } if (state.press.focus.focused) @@ -1337,13 +1327,14 @@ struct CheckBox : View canvas.rect({.center = region.center, .extent = region.extent, .stroke = 1, - .thickness = styling.focus.border_thickness, - .tint = styling.focus.border_color}); + .thickness = style.focus.border_thickness, + .tint = style.focus.border_color}); } } }; /// @brief Multi-directional Slider +// [ ] make it easy so that we can just switch the direction without switching the axis values struct Slider : View { struct State @@ -1369,7 +1360,7 @@ struct Slider : View CornerRadii track_corner_radii = Size{.scale = 0.125F}; FocusStyle focus = {}; f32 delta = 0.1F; - } styling; + } style; struct Callbacks { @@ -1391,79 +1382,79 @@ struct Slider : View Slider & axis(Axis a) { - styling.axis = a; + style.axis = a; return *this; } Slider & frame(f32 width, f32 height, bool constrain = true) { - styling.frame = Frame{width, height, constrain}; + style.frame = Frame{width, height, constrain}; return *this; } Slider & frame(Frame f) { - styling.frame = f; + style.frame = f; return *this; } Slider & thumb_frame(f32 width, f32 height, bool constrain = true) { - styling.thumb_frame = Frame{width, height, constrain}; + style.thumb_frame = Frame{width, height, constrain}; return *this; } Slider & thumb_frame(Frame f) { - styling.thumb_frame = f; + style.thumb_frame = f; return *this; } Slider & track_frame(f32 width, f32 height, bool constrain = true) { - styling.track_frame = Frame{width, height, constrain}; + style.track_frame = Frame{width, height, constrain}; return *this; } Slider & track_frame(Frame f) { - styling.track_frame = f; + style.track_frame = f; return *this; } Slider & thumb_color(ColorGradient c) { - styling.thumb_color = c; + style.thumb_color = c; return *this; } Slider & thumb_hovered_color(ColorGradient c) { - styling.thumb_hovered_color = c; + style.thumb_hovered_color = c; return *this; } Slider & thumb_dragging_color(ColorGradient c) { - styling.thumb_dragging_color = c; + style.thumb_dragging_color = c; return *this; } Slider & thumb_corner_radii(CornerRadii c) { - styling.thumb_corner_radii = c; + style.thumb_corner_radii = c; return *this; } Slider & track_color(ColorGradient c) { - styling.track_color = c; + style.track_color = c; return *this; } Slider & track_corner_radii(CornerRadii c) { - styling.track_corner_radii = c; + style.track_corner_radii = c; return *this; } @@ -1476,34 +1467,34 @@ struct Slider : View virtual ViewState tick(ViewContext const & ctx, CRect const & region, f32, ViewEvents const & events, Fn) override { - u8 const main_axis = (styling.axis == Axis::X) ? 0 : 1; + u32 const main_axis = (style.axis == Axis::X) ? 0 : 1; state.drag.tick(events); if (state.drag.dragging) { - Vec2 const track_extent = styling.track_frame(region.extent); + Vec2 const track_extent = style.track_frame(region.extent); Vec2 const track_begin = region.center - track_extent * 0.5F; Vec2 const track_end = region.center + track_extent * 0.5F; state.t = clamp(unlerp(track_begin[main_axis], track_end[main_axis], ctx.mouse.position[main_axis]), 0.0F, 1.0F); f32 const value = - clamp(lerp(state.low, state.high, state.t), state.low, state.high); + clamp(lerp(state.low, state.high, state.t), state.low, state.high); cb.changed(value); } if (state.drag.focus.focused) { - if ((styling.axis == Axis::X && ctx.key_state(KeyCode::Left)) || - (styling.axis == Axis::Y && ctx.key_state(KeyCode::Up))) + if ((style.axis == Axis::X && ctx.key_state(KeyCode::Left)) || + (style.axis == Axis::Y && ctx.key_state(KeyCode::Up))) { - state.t = max(state.t - styling.delta, 0.0F); + state.t = max(state.t - style.delta, 0.0F); } - else if ((styling.axis == Axis::X && ctx.key_state(KeyCode::Right)) || - (styling.axis == Axis::Y && ctx.key_state(KeyCode::Down))) + else if ((style.axis == Axis::X && ctx.key_state(KeyCode::Right)) || + (style.axis == Axis::Y && ctx.key_state(KeyCode::Down))) { - state.t = min(state.t + styling.delta, 1.0F); + state.t = min(state.t + style.delta, 1.0F); } } @@ -1514,29 +1505,29 @@ struct Slider : View virtual ViewLayout fit(Vec2 allocated, Span, Span) override { - return {.extent = styling.frame(allocated)}; + return {.extent = style.frame(allocated)}; } virtual void render(Canvas & canvas, CRect const & region, f32, CRect const &) override { - u8 const main_axis = (styling.axis == Axis::X) ? 0 : 1; - u8 const cross_axis = (styling.axis == Axis::X) ? 1 : 0; + u32 const main_axis = (style.axis == Axis::X) ? 0 : 1; + u32 const cross_axis = (style.axis == Axis::X) ? 1 : 0; Vec2 const frame = region.extent; - Vec2 const track_frame = styling.track_frame(frame); + Vec2 const track_frame = style.track_frame(frame); ColorGradient thumb_color; if (state.drag.dragging) { - thumb_color = styling.thumb_dragging_color; + thumb_color = style.thumb_dragging_color; } else if (state.drag.hovered) { - thumb_color = styling.thumb_hovered_color; + thumb_color = style.thumb_hovered_color; } else { - thumb_color = styling.thumb_color; + thumb_color = style.thumb_color; } f32 dilation = 1.0F; @@ -1552,30 +1543,32 @@ struct Slider : View Vec2 const track_begin = region.center - track_frame * 0.5F; + // [ ] offset calculation is off + Vec2 thumb_center; thumb_center[main_axis] = - track_begin[main_axis] + track_frame[main_axis] * state.t; + track_begin[main_axis] + track_frame[main_axis] * state.t; thumb_center[cross_axis] = region.center[cross_axis]; - Vec2 const thumb_extent = styling.thumb_frame(region.extent) * dilation; + Vec2 const thumb_extent = style.thumb_frame(region.extent) * dilation; canvas - .rrect({.center = region.center, - .extent = track_frame, - .corner_radii = styling.track_corner_radii(region.extent.y), - .tint = styling.track_color}) - .rrect({.center = thumb_center, - .extent = thumb_extent, - .corner_radii = styling.thumb_corner_radii(thumb_extent.y), - .tint = thumb_color}); + .rrect({.center = region.center, + .extent = track_frame, + .corner_radii = style.track_corner_radii(region.extent.y), + .tint = style.track_color}) + .rrect({.center = thumb_center, + .extent = thumb_extent, + .corner_radii = style.thumb_corner_radii(thumb_extent.y), + .tint = thumb_color}); if (state.drag.focus.focused) { canvas.rect({.center = region.center, .extent = region.extent, .stroke = 1, - .thickness = styling.focus.border_thickness, - .tint = styling.focus.border_color}); + .thickness = style.focus.border_thickness, + .tint = style.focus.border_color}); } } }; @@ -1600,7 +1593,7 @@ struct Switch : View Frame frame{40, 20}; Frame thumb_frame{17.5, 17.5}; FocusStyle focus = {}; - } styling; + } style; struct Callbacks { @@ -1636,61 +1629,61 @@ struct Switch : View Switch & on_color(ColorGradient c) { - styling.on_color = c; + style.on_color = c; return *this; } Switch & on_hovered_color(ColorGradient c) { - styling.on_hovered_color = c; + style.on_hovered_color = c; return *this; } Switch & off_color(ColorGradient c) { - styling.off_color = c; + style.off_color = c; return *this; } Switch & off_hovered_color(ColorGradient c) { - styling.off_hovered_color = c; + style.off_hovered_color = c; return *this; } Switch & track_color(ColorGradient c) { - styling.track_color = c; + style.track_color = c; return *this; } Switch & corner_radii(CornerRadii r) { - styling.corner_radii = r; + style.corner_radii = r; return *this; } Switch & frame(f32 width, f32 height, bool constrain = true) { - styling.frame = Frame{width, height, constrain}; + style.frame = Frame{width, height, constrain}; return *this; } Switch & frame(Frame f) { - styling.frame = f; + style.frame = f; return *this; } Switch & thumb_frame(f32 width, f32 height, bool constrain = true) { - styling.thumb_frame = Frame{width, height, constrain}; + style.thumb_frame = Frame{width, height, constrain}; return *this; } Switch & thumb_frame(Frame f) { - styling.thumb_frame = f; + style.thumb_frame = f; return *this; } @@ -1712,45 +1705,45 @@ struct Switch : View virtual ViewLayout fit(Vec2 allocated, Span, Span) override { - return {.extent = styling.frame(allocated)}; + return {.extent = style.frame(allocated)}; } virtual void render(Canvas & canvas, CRect const & region, f32, CRect const &) override { - Vec2 const thumb_extent = styling.thumb_frame(region.extent); + Vec2 const thumb_extent = style.thumb_frame(region.extent); Vec2 const thumb_center = - region.center + space_align(region.extent, thumb_extent, - Vec2{state.value ? 1.0F : -1.0F, 0}); + region.center + space_align(region.extent, thumb_extent, + Vec2{state.value ? 1.0F : -1.0F, 0}); ColorGradient thumb_color; if (state.press.hovered) { thumb_color = - state.value ? styling.on_hovered_color : styling.off_hovered_color; + state.value ? style.on_hovered_color : style.off_hovered_color; } else { - thumb_color = state.value ? styling.on_color : styling.off_color; + thumb_color = state.value ? style.on_color : style.off_color; } canvas - .rrect({.center = region.center, - .extent = region.extent, - .corner_radii = styling.corner_radii(region.extent.y), - .tint = styling.track_color}) - .rrect({.center = thumb_center, - .extent = thumb_extent, - .corner_radii = styling.corner_radii(region.extent.y), - .tint = thumb_color}); + .rrect({.center = region.center, + .extent = region.extent, + .corner_radii = style.corner_radii(region.extent.y), + .tint = style.track_color}) + .rrect({.center = thumb_center, + .extent = thumb_extent, + .corner_radii = style.corner_radii(region.extent.y), + .tint = thumb_color}); if (state.press.focus.focused) { canvas.rect({.center = region.center, .extent = region.extent, .stroke = 1, - .thickness = styling.focus.border_thickness, - .tint = styling.focus.border_color}); + .thickness = style.focus.border_thickness, + .tint = style.focus.border_color}); } } }; @@ -1773,7 +1766,7 @@ struct RadioBox : View ColorGradient inner_color = DEFAULT_THEME.primary; ColorGradient inner_hovered_color = DEFAULT_THEME.primary_variant; FocusStyle focus = {}; - } styling; + } style; struct Callbacks { @@ -1782,43 +1775,43 @@ struct RadioBox : View RadioBox & corner_radii(CornerRadii c) { - styling.corner_radii = c; + style.corner_radii = c; return *this; } RadioBox & thickness(f32 t) { - styling.thickness = t; + style.thickness = t; return *this; } RadioBox & color(ColorGradient c) { - styling.color = c; + style.color = c; return *this; } RadioBox & inner_color(ColorGradient c) { - styling.inner_color = c; + style.inner_color = c; return *this; } RadioBox & inner_hovered_color(ColorGradient c) { - styling.inner_hovered_color = c; + style.inner_hovered_color = c; return *this; } RadioBox & frame(f32 width, f32 height, bool constrain = true) { - styling.frame = Frame{width, height, constrain}; + style.frame = Frame{width, height, constrain}; return *this; } RadioBox & frame(Frame f) { - styling.frame = f; + style.frame = f; return *this; } @@ -1846,7 +1839,7 @@ struct RadioBox : View virtual ViewLayout fit(Vec2 allocated, Span, Span) override { - return {.extent = styling.frame(allocated)}; + return {.extent = style.frame(allocated)}; } virtual void render(Canvas & canvas, CRect const & region, f32, @@ -1854,21 +1847,19 @@ struct RadioBox : View { canvas.rrect({.center = region.center, .extent = region.extent, - .corner_radii = styling.corner_radii(region.extent.y), + .corner_radii = style.corner_radii(region.extent.y), .stroke = 1, - .thickness = styling.thickness, - .tint = styling.color}); + .thickness = style.thickness, + .tint = style.color}); if (state.value) { Vec2 inner_extent = region.extent * (state.press.hovered ? 0.75F : 0.5F); - ColorGradient inner_color = state.press.hovered ? - styling.inner_hovered_color : - styling.inner_color; + ColorGradient inner_color = + state.press.hovered ? style.inner_hovered_color : style.inner_color; - canvas.circle({.center = region.center, - .extent = inner_extent, - .tint = inner_color}); + canvas.circle( + {.center = region.center, .extent = inner_extent, .tint = inner_color}); } if (state.press.focus.focused) @@ -1876,48 +1867,54 @@ struct RadioBox : View canvas.rect({.center = region.center, .extent = region.extent, .stroke = 1, - .thickness = styling.focus.border_thickness, - .tint = styling.focus.border_color}); + .thickness = style.focus.border_thickness, + .tint = style.focus.border_color}); } } }; -enum class ScalarInputType : u8 +using Scalar = Enum; + +namespace fmt { - i32 = 0, - f32 = 1 -}; -/// @brief Numeric Scalar UI Input. -struct ScalarInput +inline bool push(Context const & ctx, Spec const & spec, Scalar const & value) { - union - { - i32 i32 = 0; - f32 f32; - }; + return value.match([&](f32 f) { return push(ctx, spec, f); }, + [&](i32 i) { return push(ctx, spec, i); }); +} - ScalarInputType type = ScalarInputType::i32; -}; +} // namespace fmt -namespace fmt +/// @param start starting value, this is the value to be reset to when cancel is +/// requested +/// @param min minimum value of the scalar +/// @param max maximum value of the scalar +/// @param step step in either direction that should be taken. i.e. when `+` or +/// `-` is pressed. +/// @param current current value of the scalar, mutated by the GUI system +struct F32InputSpec { + f32 base = 0; + f32 min = 0; + f32 max = 1; + f32 step = 0.01; -inline bool push(Context const & ctx, Spec const & spec, - ScalarInput const & value) -{ - switch (value.type) + constexpr f32 step_value(f32 current, i32 direction) const { - case ScalarInputType::i32: - return push(ctx, spec, value.i32); - case ScalarInputType::f32: - return push(ctx, spec, value.f32); - default: - return true; + return clamp(current + ((direction > 0) ? step : -step), min, max); + } + + constexpr f32 uninterp(f32 current) const + { + return clamp(unlerp(min, max, current), 0.0F, 1.0F); } -} -} // namespace fmt + constexpr f32 interp(f32 t) const + { + return clamp(lerp(min, max, t), min, max); + } +}; /// @param start starting value, this is the value to be reset to when cancel is /// requested @@ -1926,101 +1923,52 @@ inline bool push(Context const & ctx, Spec const & spec, /// @param step step in either direction that should be taken. i.e. when `+` or /// `-` is pressed. /// @param current current value of the scalar, mutated by the GUI system -struct ScalarState +struct I32InputSpec { - ScalarInput base = {}; - ScalarInput min = {}; - ScalarInput max = {.i32 = 200}; - ScalarInput step = {}; - ScalarInput current = {.i32 = 100}; + i32 base = 0; + i32 min = 0; + i32 max = 1'000; + i32 step = 100; - constexpr void step_value(i32 direction) + constexpr i32 step_value(i32 current, i32 direction) const { - switch (base.type) - { - case ScalarInputType::i32: - current.i32 = - clamp(sat_add(current.i32, (direction > 0) ? step.i32 : -step.i32), - min.i32, max.i32); - return; - case ScalarInputType::f32: - current.f32 = - clamp(current.f32 + ((direction > 0) ? step.f32 : -step.f32), - min.f32, max.f32); - return; - default: - return; - } + return clamp(sat_add(current, (direction > 0) ? step : -step), min, max); } - constexpr f32 uninterp() const + constexpr f32 uninterp(i32 current) const { - switch (base.type) - { - case ScalarInputType::i32: - return clamp(unlerp((f32) min.i32, (f32) max.i32, (f32) current.i32), - 0.0F, 1.0F); - case ScalarInputType::f32: - return clamp(unlerp(min.f32, max.f32, current.f32), 0.0F, 1.0F); - default: - return 0; - } + return clamp(unlerp((f32) min, (f32) max, (f32) current), 0.0F, 1.0F); } - constexpr void interp(f32 t) + constexpr i32 interp(f32 t) const { - switch (base.type) - { - case ScalarInputType::i32: - current.i32 = clamp((i32) lerp((f32) min.i32, (f32) max.i32, t), - min.i32, max.i32); - return; - case ScalarInputType::f32: - current.f32 = clamp(lerp(min.f32, max.f32, t), min.f32, max.f32); - return; - default: - break; - } + return clamp((i32) lerp((f32) min, (f32) max, t), min, max); } }; -constexpr ScalarState scalar(f32 base, f32 min, f32 max, f32 step) -{ - return ScalarState{ - .base = {.f32 = base, .type = ScalarInputType::i32}, - .min = {.f32 = min, .type = ScalarInputType::i32}, - .max = {.f32 = max, .type = ScalarInputType::i32}, - .step = {.f32 = step, .type = ScalarInputType::i32} - }; -} - -constexpr ScalarState scalar(i32 base, i32 min, i32 max, i32 step) -{ - return ScalarState{ - .base = {.i32 = base, .type = ScalarInputType::i32}, - .min = {.i32 = min, .type = ScalarInputType::i32}, - .max = {.i32 = max, .type = ScalarInputType::i32}, - .step = {.i32 = step, .type = ScalarInputType::i32} - }; -} +using ScalarSpec = Enum; struct ScalarDragBox : View { - typedef Fn Fmt; - typedef Fn, ScalarState &)> Parse; + typedef Fn Fmt; + typedef Fn, ScalarSpec const &, Scalar &)> Parse; struct State { - bool disabled = false; - bool input_mode = false; - bool dragging = false; - FocusState focus = {}; - ScalarState value = {}; + bool disabled = false; + bool input_mode = false; + bool dragging = false; + FocusState focus = {}; + ScalarSpec spec = F32InputSpec{}; + Scalar scalar = 0.0F; } state; + // TODO: calculate minimum frame extent based on font size + struct Style { - Frame frame = Frame{}.min(100, DEFAULT_THEME.body_font_height + 10); + Frame frame = Frame{}.min(DEFAULT_THEME.body_font_height * 10, + DEFAULT_THEME.body_font_height + 10); Frame padding{5, 5}; Size thumb_width = {5}; CornerRadii corner_radii = Size{.scale = 0.125F}; @@ -2031,33 +1979,31 @@ struct ScalarDragBox : View FocusStyle focus = {}; Fmt fmt = fn(scalar_fmt); Parse parse = fn(scalar_parse); - } styling; + } style; - struct Inner - { - TextInput input{}; - } inner; + TextInput input_{}; struct Callbacks { - Fn update = noop; + Fn update = noop; } cb; ScalarDragBox() { - inner.input.multiline(false).tab_input(false).enter_submits(false); + input_.multiline(false).tab_input(false).enter_submits(false); } virtual ~ScalarDragBox() override { } - static void scalar_fmt(fmt::Context const & ctx, ScalarInput v) + static void scalar_fmt(fmt::Context const & ctx, Scalar s) { - fmt::format(ctx, v); + fmt::format(ctx, fmt::Spec{.precision = 2}, s); } - static void scalar_parse(Span text, ScalarState & styling); + static void scalar_parse(Span text, ScalarSpec const & spec, + Scalar &); virtual ViewState tick(ViewContext const & ctx, CRect const & region, f32, ViewEvents const & events, @@ -2074,14 +2020,16 @@ struct ScalarDragBox : View if (state.dragging && !state.input_mode) { f32 const t = - clamp(unlerp(region.begin().x, region.end().x, ctx.mouse.position.x), - 0.0F, 1.0F); - state.value.interp(t); + clamp(unlerp(region.begin().x, region.end().x, ctx.mouse.position.x), + 0.0F, 1.0F); + state.scalar = state.spec.match( + [t](F32InputSpec & v) -> Scalar { return v.interp(t); }, + [t](I32InputSpec & v) -> Scalar { return v.interp(t); }); } - if (inner.input.state.editing) + if (input_.state.editing) { - styling.parse(inner.input.inner.content.get_text(), state.value); + style.parse(input_.content_.get_text(), state.spec, state.scalar); } else { @@ -2089,20 +2037,20 @@ struct ScalarDragBox : View c8 text[128]; Buffer buffer = ash::buffer(span(text).as_char()); fmt::Context ctx = fmt::buffer(buffer, scratch); - styling.fmt(ctx, state.value.current); - inner.input.inner.content.set_text(span(text).slice(0, buffer.size())); + style.fmt(ctx, state.scalar); + input_.content_.set_text(span(text).slice(0, buffer.size())); } - inner.input.state.disabled = !state.input_mode; + input_.state.disabled = !state.input_mode; - if (inner.input.state.editing || state.dragging) + if (input_.state.editing || state.dragging) { - cb.update(state.value.current); + cb.update(state.scalar); } state.focus.tick(events); - build(inner.input); + build(input_); return ViewState{.pointable = !state.disabled, .draggable = !state.disabled, @@ -2111,7 +2059,7 @@ struct ScalarDragBox : View virtual void size(Vec2 allocated, Span sizes) override { - Vec2 child = styling.frame(allocated) - 2 * styling.padding(allocated); + Vec2 child = style.frame(allocated) - 2 * style.padding(allocated); child.x = max(child.x, 0.0F); child.y = max(child.y, 0.0F); fill(sizes, child); @@ -2121,7 +2069,7 @@ struct ScalarDragBox : View Span centers) override { fill(centers, Vec2{0, 0}); - return {.extent = sizes[0] + 2 * styling.padding(allocated)}; + return {.extent = sizes[0] + 2 * style.padding(allocated)}; } virtual void render(Canvas & canvas, CRect const & region, f32, @@ -2129,24 +2077,26 @@ struct ScalarDragBox : View { canvas.rrect({.center = region.center, .extent = region.extent, - .corner_radii = styling.corner_radii(region.extent.y), - .stroke = styling.stroke, - .thickness = styling.thickness, - .tint = styling.color}); + .corner_radii = style.corner_radii(region.extent.y), + .stroke = style.stroke, + .thickness = style.thickness, + .tint = style.color}); if (!state.input_mode) { - f32 const t = state.value.uninterp(); - Vec2 const thumb_extent{styling.thumb_width(region.extent.x), + f32 const t = state.spec.match( + [this](F32InputSpec & v) { return v.uninterp(state.scalar[v0]); }, + [this](I32InputSpec & v) { return v.uninterp(state.scalar[v1]); }); + Vec2 const thumb_extent{style.thumb_width(region.extent.x), region.extent.y}; Vec2 thumb_center = region.center; thumb_center.x += - space_align(region.extent.x, thumb_extent.x, norm_to_axis(t)); + space_align(region.extent.x, thumb_extent.x, norm_to_axis(t)); canvas.rrect({.center = thumb_center, .extent = thumb_extent, .corner_radii = Vec4::splat(region.extent.y * 0.125F), - .tint = styling.thumb_color}); + .tint = style.thumb_color}); } if (state.focus.focused) @@ -2154,8 +2104,8 @@ struct ScalarDragBox : View canvas.rect({.center = region.center, .extent = region.extent, .stroke = 1, - .thickness = styling.focus.border_thickness, - .tint = styling.focus.border_color}); + .thickness = style.focus.border_thickness, + .tint = style.focus.border_color}); } } @@ -2167,66 +2117,76 @@ struct ScalarDragBox : View } }; -// [ ] infinite per-pixel drag struct ScalarBox : FlexView { struct Callbacks { - Fn update = noop; + Fn update = noop; } cb; - struct Inner - { - TextButton dec{}; - TextButton inc{}; - ScalarDragBox drag{}; - } inner; + TextButton dec_{}; + TextButton inc_{}; + ScalarDragBox drag_{}; ScalarBox() { FlexView::axis(Axis::X) - .wrap(false) - .main_align(MainAlign::Start) - .cross_align(0) - .frame(Frame{}.scale(1, 1)); - - inner.dec.text(U"-"_str) - .style( - TextStyle{ - .shadow_scale = 1, - .shadow_offset = {1, 1}, - .foreground = DEFAULT_THEME.on_primary, - .shadow = colors::BLACK + .wrap(false) + .main_align(MainAlign::Start) + .cross_align(0) + .frame(Frame{}.scale(1, 1)); + + dec_.text(U"-"_str) + .run( + TextStyle{ + .shadow_scale = 1, + .shadow_offset = {1, 1}, + .color = DEFAULT_THEME.on_primary, + .shadow = colors::BLACK }, - FontStyle{.font = engine->default_font, - .font_height = DEFAULT_THEME.body_font_height, - .line_height = 1}) - .on_pressed(fn( - this, - +[](ScalarBox * b) { - b->inner.drag.state.value.step_value(-1); - b->cb.update(b->inner.drag.state.value.current); - })) - .padding(5, 5); - - inner.inc.text(U"+"_str) - .style( - TextStyle{ - .shadow_scale = 1, - .shadow_offset = {1, 1}, - .foreground = DEFAULT_THEME.on_primary, - .shadow = colors::BLACK + FontStyle{.font = engine->default_font, + .font_height = DEFAULT_THEME.body_font_height, + .line_height = 1}) + .on_pressed(fn( + this, + +[](ScalarBox * b) { + auto & state = b->drag_.state; + state.scalar = state.spec.match( + [&state](F32InputSpec const & spec) -> Scalar { + return spec.step_value(state.scalar[v0], -1); + }, + [&state](I32InputSpec const & spec) -> Scalar { + return spec.step_value(state.scalar[v1], -1); + }); + b->cb.update(state.scalar); + })) + .padding(5, 5); + + inc_.text(U"+"_str) + .run( + TextStyle{ + .shadow_scale = 1, + .shadow_offset = {1, 1}, + .color = DEFAULT_THEME.on_primary, + .shadow = colors::BLACK }, - FontStyle{.font = engine->default_font, - .font_height = DEFAULT_THEME.body_font_height, - .line_height = 1}) - .on_pressed(fn( - this, - +[](ScalarBox * b) { - b->inner.drag.state.value.step_value(1); - b->cb.update(b->inner.drag.state.value.current); - })) - .padding(5, 5); + FontStyle{.font = engine->default_font, + .font_height = DEFAULT_THEME.body_font_height, + .line_height = 1}) + .on_pressed(fn( + this, + +[](ScalarBox * b) { + auto & state = b->drag_.state; + state.scalar = state.spec.match( + [&state](F32InputSpec const & spec) -> Scalar { + return spec.step_value(state.scalar[v0], 1); + }, + [&state](I32InputSpec const & spec) -> Scalar { + return spec.step_value(state.scalar[v1], 1); + }); + b->cb.update(state.scalar); + })) + .padding(5, 5); // [ ] color, stroke color, etc. the rectangles at small sizes seem to have // bad border radii. @@ -2236,71 +2196,72 @@ struct ScalarBox : FlexView // [ ] fix interpolation of slider // - inner.drag.cb.update = - fn(this, +[](ScalarBox * b, ScalarInput in) { b->cb.update(in); }); + drag_.cb.update = + fn(this, +[](ScalarBox * b, Scalar in) { b->cb.update(in); }); // [ ] set drag box style: create similar methods for it // [ ] all views must have these methods - // inner.drag. + // drag_. } virtual ~ScalarBox() override = default; // [ ] placeholder + // [ ] spec ScalarBox & stroke(f32 s) { - inner.drag.styling.stroke = s; + drag_.style.stroke = s; return *this; } ScalarBox & thickness(f32 t) { - inner.drag.styling.thickness = t; + drag_.style.thickness = t; return *this; } ScalarBox & padding(f32 x, f32 y) { - inner.dec.padding(x, y); - inner.inc.padding(x, y); - inner.drag.styling.padding = Frame{x, y}; + dec_.padding(x, y); + inc_.padding(x, y); + drag_.style.padding = Frame{x, y}; return *this; } ScalarBox & padding(Frame p) { - inner.dec.padding(p); - inner.inc.padding(p); - inner.drag.styling.padding = p; + dec_.padding(p); + inc_.padding(p); + drag_.style.padding = p; return *this; } ScalarBox & frame(f32 width, f32 height, bool constrain = true) { - inner.dec.frame(width, height, constrain); - inner.inc.frame(width, height, constrain); - inner.drag.styling.frame = Frame{width, height, constrain}; + dec_.frame(width, height, constrain); + inc_.frame(width, height, constrain); + drag_.style.frame = Frame{width, height, constrain}; return *this; } ScalarBox & frame(Frame f) { - inner.dec.frame(f); - inner.inc.frame(f); - inner.drag.styling.frame = f; + dec_.frame(f); + inc_.frame(f); + drag_.style.frame = f; return *this; } ScalarBox & corner_radii(CornerRadii r) { - inner.dec.corner_radii(r); - inner.inc.corner_radii(r); - inner.drag.styling.corner_radii = r; + dec_.corner_radii(r); + inc_.corner_radii(r); + drag_.style.corner_radii = r; return *this; } - ScalarBox & on_update(Fn f) + ScalarBox & on_update(Fn f) { cb.update = f; return *this; @@ -2309,19 +2270,19 @@ struct ScalarBox : FlexView ScalarBox & text_style(TextStyle const & style, FontStyle const & font, u32 first, u32 count) { - inner.dec.style(style, font, first, count); - inner.inc.style(style, font, first, count); - inner.drag.inner.input.content_style(style, font, first, count) - .stub_style(style, font, first, count); + dec_.run(style, font, first, count); + inc_.run(style, font, first, count); + drag_.input_.content_run(style, font, first, count) + .stub_run(style, font, first, count); return *this; } virtual ViewState tick(ViewContext const &, CRect const &, f32, ViewEvents const &, Fn build) override { - build(inner.dec); - build(inner.drag); - build(inner.inc); + build(dec_); + build(drag_); + build(inc_); return ViewState{}; } }; @@ -2348,7 +2309,7 @@ struct ScrollBar : View CornerRadii track_corner_radii = Size{}; FocusStyle focus = {}; f32 delta = 0.1F; - } styling; + } style; struct Callbacks { @@ -2358,30 +2319,30 @@ struct ScrollBar : View virtual ViewState tick(ViewContext const & ctx, CRect const & region, f32, ViewEvents const & events, Fn) override { - u8 const main_axis = (styling.axis == Axis::X) ? 0 : 1; + u32 const main_axis = (style.axis == Axis::X) ? 0 : 1; state.drag.tick(events); if (state.drag.dragging) { - state.t = - clamp((ctx.mouse.position[main_axis] - region.extent[main_axis] / 2) / - region.extent[main_axis], - 0.0F, 1.0F); + state.t = clamp( + (ctx.mouse.position[main_axis] - region.extent[main_axis] * 0.5F) / + region.extent[main_axis], + 0.0F, 1.0F); cb.scrolled(state.t); } if (state.drag.focus.focused) { - if ((styling.axis == Axis::X && ctx.key_state(KeyCode::Left)) || - (styling.axis == Axis::Y && ctx.key_state(KeyCode::Up))) + if ((style.axis == Axis::X && ctx.key_state(KeyCode::Left)) || + (style.axis == Axis::Y && ctx.key_state(KeyCode::Up))) { - state.t = max(state.t - styling.delta, 0.0F); + state.t = max(state.t - style.delta, 0.0F); } - else if ((styling.axis == Axis::X && ctx.key_state(KeyCode::Right)) || - (styling.axis == Axis::Y && ctx.key_state(KeyCode::Down))) + else if ((style.axis == Axis::X && ctx.key_state(KeyCode::Right)) || + (style.axis == Axis::Y && ctx.key_state(KeyCode::Down))) { - state.t = min(state.t + styling.delta, 1.0F); + state.t = min(state.t + style.delta, 1.0F); } } @@ -2406,13 +2367,13 @@ struct ScrollBar : View virtual void render(Canvas & canvas, CRect const & region, f32, CRect const &) override { - u8 const main_axis = (styling.axis == Axis::X) ? 0 : 1; - u8 const cross_axis = (styling.axis == Axis::X) ? 1 : 0; - Vec4 const thumb_corner_radii = styling.thumb_corner_radii(region.extent.y); - Vec4 const track_corner_radii = styling.track_corner_radii(region.extent.y); + u32 const main_axis = (style.axis == Axis::X) ? 0 : 1; + u32 const cross_axis = (style.axis == Axis::X) ? 1 : 0; + Vec4 const thumb_corner_radii = style.thumb_corner_radii(region.extent.y); + Vec4 const track_corner_radii = style.track_corner_radii(region.extent.y); // calculate thumb main axis extent - f32 const thumb_scale = region.extent[main_axis] / styling.content_extent; + f32 const thumb_scale = region.extent[main_axis] / style.content_extent; Vec2 thumb_extent; thumb_extent[cross_axis] = region.extent[cross_axis]; thumb_extent[main_axis] = thumb_scale * region.extent[main_axis]; @@ -2428,36 +2389,36 @@ struct ScrollBar : View ColorGradient thumb_color; if (state.drag.dragging) { - thumb_color = styling.thumb_dragging_color; + thumb_color = style.thumb_dragging_color; } else if (state.drag.hovered) { - thumb_color = styling.thumb_hovered_color; + thumb_color = style.thumb_hovered_color; } else { - thumb_color = styling.thumb_color; + thumb_color = style.thumb_color; } canvas - .rrect({.center = region.center, - .extent = region.extent, - .corner_radii = track_corner_radii, - .stroke = 0, - .tint = styling.track_color}) - .rrect({.center = thumb_center, - .extent = thumb_extent, - .corner_radii = thumb_corner_radii, - .stroke = 0, - .tint = thumb_color}); + .rrect({.center = region.center, + .extent = region.extent, + .corner_radii = track_corner_radii, + .stroke = 0, + .tint = style.track_color}) + .rrect({.center = thumb_center, + .extent = thumb_extent, + .corner_radii = thumb_corner_radii, + .stroke = 0, + .tint = thumb_color}); if (state.drag.focus.focused) { canvas.rect({.center = region.center, .extent = region.extent, .stroke = 1, - .thickness = styling.focus.border_thickness, - .tint = styling.focus.border_color}); + .thickness = style.focus.border_thickness, + .tint = style.focus.border_color}); } } }; @@ -2471,19 +2432,13 @@ struct ScrollViewFrame : View Vec2 content_extent = {}; } state; - struct Inner - { - Option child = None; - } inner; + OptionRef child_ = none; virtual ViewState tick(ViewContext const & ctx, CRect const & region, f32, ViewEvents const & events, Fn build) override { - if (inner.child) - { - build(*inner.child.value()); - } + child_.match(build); if (events.mouse_scroll) { @@ -2516,7 +2471,7 @@ struct ScrollViewFrame : View return {.extent = allocated, .viewport_extent = state.content_extent, .viewport_transform = - scroll_transform(content_size, allocated, state.t, state.zoom)}; + scroll_transform(content_size, allocated, state.t, state.zoom)}; } }; @@ -2533,139 +2488,136 @@ struct ScrollView : View Frame frame{200, 200}; Size x_bar_size = {.offset = 10}; Size y_bar_size = {.offset = 10}; - } styling; + } style; - struct Inner - { - ScrollViewFrame view_frame{}; - ScrollBar x_bar{}; - ScrollBar y_bar{}; - } inner; + ScrollViewFrame view_frame_{}; + ScrollBar x_bar_{}; + ScrollBar y_bar_{}; ScrollView() { - inner.x_bar.styling.axis = Axis::X; - inner.y_bar.styling.axis = Axis::Y; + x_bar_.style.axis = Axis::X; + y_bar_.style.axis = Axis::Y; } virtual ~ScrollView() override = default; ScrollView & disable(bool d) { - state.disabled = d; - inner.x_bar.state.disabled = d; - inner.y_bar.state.disabled = d; + state.disabled = d; + x_bar_.state.disabled = d; + y_bar_.state.disabled = d; return *this; } ScrollView & item(View & v) { - inner.view_frame.inner.child = Some{&v}; + view_frame_.child_ = v; return *this; } - ScrollView & item(NoneType) + ScrollView & item(None) { - inner.view_frame.inner.child = None; + view_frame_.child_ = none; return *this; } ScrollView & thumb_color(ColorGradient c) { - inner.x_bar.styling.thumb_color = c; - inner.y_bar.styling.thumb_color = c; + x_bar_.style.thumb_color = c; + y_bar_.style.thumb_color = c; return *this; } ScrollView & thumb_hovered_color(ColorGradient c) { - inner.x_bar.styling.thumb_hovered_color = c; - inner.y_bar.styling.thumb_hovered_color = c; + x_bar_.style.thumb_hovered_color = c; + y_bar_.style.thumb_hovered_color = c; return *this; } ScrollView & thumb_dragging_color(ColorGradient c) { - inner.x_bar.styling.thumb_dragging_color = c; - inner.y_bar.styling.thumb_dragging_color = c; + x_bar_.style.thumb_dragging_color = c; + y_bar_.style.thumb_dragging_color = c; return *this; } ScrollView & thumb_corner_radii(CornerRadii c) { - inner.x_bar.styling.thumb_corner_radii = c; - inner.y_bar.styling.thumb_corner_radii = c; + x_bar_.style.thumb_corner_radii = c; + y_bar_.style.thumb_corner_radii = c; return *this; } ScrollView & track_color(ColorGradient c) { - inner.x_bar.styling.track_color = c; - inner.y_bar.styling.track_color = c; + x_bar_.style.track_color = c; + y_bar_.style.track_color = c; return *this; } ScrollView & track_corner_radii(CornerRadii c) { - inner.x_bar.styling.track_corner_radii = c; - inner.y_bar.styling.track_corner_radii = c; + x_bar_.style.track_corner_radii = c; + y_bar_.style.track_corner_radii = c; return *this; } ScrollView & axes(Axes a) { - styling.axes = a; - inner.x_bar.state.hidden = has_bits(a, Axes::X); - inner.y_bar.state.hidden = has_bits(a, Axes::Y); + style.axes = a; + x_bar_.state.hidden = has_bits(a, Axes::X); + y_bar_.state.hidden = has_bits(a, Axes::Y); return *this; } ScrollView & frame(f32 width, f32 height, bool constrain) { - styling.frame = Frame{width, height, constrain}; + style.frame = Frame{width, height, constrain}; return *this; } ScrollView & frame(Frame f) { - styling.frame = f; + style.frame = f; return *this; } ScrollView & bar_size(f32 x, f32 y) { - styling.x_bar_size = Size{.offset = x}; - styling.y_bar_size = Size{.offset = y}; + style.x_bar_size = Size{.offset = x}; + style.y_bar_size = Size{.offset = y}; return *this; } ScrollView & bar_size(Size x, Size y) { - styling.x_bar_size = x; - styling.y_bar_size = y; + style.x_bar_size = x; + style.y_bar_size = y; return *this; } virtual ViewState tick(ViewContext const &, CRect const &, f32, ViewEvents const &, Fn build) override { - inner.view_frame.state.t = {inner.x_bar.state.t, inner.y_bar.state.t}; - build(inner.view_frame); - build(inner.x_bar); - build(inner.y_bar); + view_frame_.state.t = {x_bar_.state.t, y_bar_.state.t}; + build(view_frame_); + build(x_bar_); + build(y_bar_); return ViewState{}; } virtual void size(Vec2 allocated, Span sizes) override { - Vec2 const frame = styling.frame(allocated); - f32 const x_bar_size = styling.x_bar_size(allocated.x); - f32 const y_bar_size = styling.y_bar_size(allocated.y); + Vec2 const frame = style.frame(allocated); + f32 const x_bar_size = style.x_bar_size(allocated.x); + f32 const y_bar_size = style.y_bar_size(allocated.y); sizes[0] = frame; sizes[1] = {frame.x, x_bar_size}; - if (has_bits(styling.axes, Axes::X | Axes::Y)) + if (has_bits(style.axes, Axes::X | Axes::Y)) { sizes[1].x -= y_bar_size; } @@ -2676,14 +2628,12 @@ struct ScrollView : View virtual ViewLayout fit(Vec2 allocated, Span sizes, Span centers) override { - Vec2 const frame = styling.frame(allocated); + Vec2 const frame = style.frame(allocated); centers[0] = {0, 0}; centers[1] = space_align(frame, sizes[1], ALIGNMENT_BOTTOM_LEFT); centers[2] = space_align(frame, sizes[2], ALIGNMENT_TOP_RIGHT); - inner.x_bar.styling.content_extent = - inner.view_frame.state.content_extent.x; - inner.y_bar.styling.content_extent = - inner.view_frame.state.content_extent.y; + x_bar_.style.content_extent = view_frame_.state.content_extent.x; + y_bar_.style.content_extent = view_frame_.state.content_extent.y; return {.extent = frame}; } @@ -2693,10 +2643,10 @@ struct ComboBoxItem : View { struct State { - bool disabled = false; - PressState press = {}; - Option const *> selected = None; - u32 index = 0; + bool disabled = false; + PressState press = {}; + OptionRef const> selected = none; + u32 index = 0; } state; struct Style @@ -2715,17 +2665,14 @@ struct ComboBoxItem : View f32 alignment = 0; }; - Option