diff --git a/.clang-format b/.clang-format index bbbefa97..e5daeb49 100644 --- a/.clang-format +++ b/.clang-format @@ -7,43 +7,45 @@ AlignConsecutiveDeclarations: false AlignEscapedNewlines: Left AlignOperands: Align AlignTrailingComments: - Kind: Always - OverEmptyLines: 2 + Kind: Always + OverEmptyLines: 2 AllowAllArgumentsOnNextLine: true AllowAllParametersOfDeclarationOnNextLine: true AllowShortBlocksOnASingleLine: Empty AllowShortCaseLabelsOnASingleLine: false -AllowShortFunctionsOnASingleLine: All +AllowShortFunctionsOnASingleLine: Empty AllowShortIfStatementsOnASingleLine: WithoutElse -AllowShortLambdasOnASingleLine: Inline +AllowShortLambdasOnASingleLine: Empty AllowShortLoopsOnASingleLine: false AlwaysBreakBeforeMultilineStrings: true BinPackArguments: false BinPackParameters: false BreakBeforeBraces: Allman -BraceWrapping: - AfterCaseLabel: true - AfterClass: true - AfterControlStatement: true - AfterEnum: true - AfterFunction: true - AfterNamespace: true - AfterStruct: true - AfterUnion: true - AfterExternBlock: true - BeforeCatch: true - BeforeElse: true - BeforeLambdaBody: true - BeforeWhile: true - IndentBraces: false - SplitEmptyFunction: true - SplitEmptyRecord: true - SplitEmptyNamespace: true +BraceWrapping: + AfterCaseLabel: true + AfterClass: true + AfterControlStatement: true + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: true + BeforeElse: true + BeforeLambdaBody: true + BeforeWhile: true + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BracedInitializerIndentWidth: 4 BreakBeforeBinaryOperators: None BreakBeforeTernaryOperators: true BreakConstructorInitializers: BeforeColon BreakInheritanceList: BeforeColon BreakStringLiterals: true +BreakTemplateDeclarations: Yes ColumnLimit: 200 CompactNamespaces: false ConstructorInitializerIndentWidth: 4 @@ -51,15 +53,15 @@ ContinuationIndentWidth: 8 Cpp11BracedListStyle: true FixNamespaceComments: true IncludeCategories: - - Regex: '^<.*' - Priority: 1 - - Regex: '^".*' - Priority: 2 - - Regex: '.*' - Priority: 3 + - Regex: '^<.*' + Priority: 1 + - Regex: '^".*' + Priority: 2 + - Regex: '.*' + Priority: 3 IncludeIsMainRegex: '([-_](test|unittest))?$' IndentCaseLabels: true -IndentPPDirectives: BeforeHash +IndentPPDirectives: None IndentWidth: 4 IndentWrappedFunctionNames: false InsertNewlineAtEOF: true @@ -69,6 +71,7 @@ MacroBlockBegin: '' MacroBlockEnd: '' MaxEmptyLinesToKeep: 2 NamespaceIndentation: All +PackConstructorInitializers: Never PointerAlignment: Left ReflowComments: false SpaceAfterCStyleCast: false diff --git a/scripts/library.cmake b/scripts/library.cmake index 6eeccc7b..e94fe97d 100644 --- a/scripts/library.cmake +++ b/scripts/library.cmake @@ -8,7 +8,7 @@ add_library(gal::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) # COMPILE FLAGS if (${PROJECT_NAME_PREFIX}COMPILER_MSVC) - set(${PROJECT_NAME_PREFIX}COMPILE_FLAGS "/D_CRT_SECURE_NO_WARNINGS") + set(${PROJECT_NAME_PREFIX}COMPILE_FLAGS "/D_CRT_SECURE_NO_WARNINGS;/utf-8;/DNOMINMAX") # ==================== # PEDANTIC if (${PROJECT_NAME_PREFIX}PEDANTIC) @@ -257,6 +257,8 @@ set( # ========================= ${PROJECT_SOURCE_DIR}/src/memory/rw.hpp + ${PROJECT_SOURCE_DIR}/src/memory/unique_ptr.hpp + ${PROJECT_SOURCE_DIR}/src/memory/reference_wrapper.hpp ${PROJECT_SOURCE_DIR}/src/memory/memory.hpp @@ -356,20 +358,47 @@ set( ${PROJECT_SOURCE_DIR}/src/unit_test/unit_test.hpp # ========================= - # DRAW + # IO # ========================= - ${PROJECT_SOURCE_DIR}/src/draw/flag.hpp - ${PROJECT_SOURCE_DIR}/src/draw/def.hpp - ${PROJECT_SOURCE_DIR}/src/draw/font.hpp - ${PROJECT_SOURCE_DIR}/src/draw/shared_data.hpp - ${PROJECT_SOURCE_DIR}/src/draw/theme.hpp - ${PROJECT_SOURCE_DIR}/src/draw/mouse.hpp - ${PROJECT_SOURCE_DIR}/src/draw/draw_list.hpp - ${PROJECT_SOURCE_DIR}/src/draw/window.hpp - ${PROJECT_SOURCE_DIR}/src/draw/context.hpp + ${PROJECT_SOURCE_DIR}/src/io/inputs.hpp + + ${PROJECT_SOURCE_DIR}/src/io/io.hpp - ${PROJECT_SOURCE_DIR}/src/draw/draw.hpp + # ========================= + # GFX + # ========================= + + ${PROJECT_SOURCE_DIR}/src/gfx/type.hpp + ${PROJECT_SOURCE_DIR}/src/gfx/texture.hpp + ${PROJECT_SOURCE_DIR}/src/gfx/glyph.hpp + ${PROJECT_SOURCE_DIR}/src/gfx/render_list.hpp + ${PROJECT_SOURCE_DIR}/src/gfx/renderer.hpp + ${PROJECT_SOURCE_DIR}/src/gfx/context.hpp + + ${PROJECT_SOURCE_DIR}/src/gfx/internal/rect_pack.hpp + ${PROJECT_SOURCE_DIR}/src/gfx/internal/texture.hpp + ${PROJECT_SOURCE_DIR}/src/gfx/internal/glyph.hpp + ${PROJECT_SOURCE_DIR}/src/gfx/internal/context.hpp + + ${PROJECT_SOURCE_DIR}/src/gfx/extension/glyph_parser_freetype.hpp + ${PROJECT_SOURCE_DIR}/src/gfx/extension/renderer_d3d11.hpp + ${PROJECT_SOURCE_DIR}/src/gfx/extension/renderer_d3d12.hpp + + ${PROJECT_SOURCE_DIR}/src/gfx/gfx.hpp + + # ========================= + # GUI + # ========================= + + ${PROJECT_SOURCE_DIR}/src/gui/gui.hpp + ${PROJECT_SOURCE_DIR}/src/gui/internal/gui.inl + ${PROJECT_SOURCE_DIR}/src/gui/internal/common.hpp + ${PROJECT_SOURCE_DIR}/src/gui/internal/font.hpp + ${PROJECT_SOURCE_DIR}/src/gui/internal/draw_list.hpp + ${PROJECT_SOURCE_DIR}/src/gui/internal/mouse.hpp + ${PROJECT_SOURCE_DIR}/src/gui/internal/window.hpp + ${PROJECT_SOURCE_DIR}/src/gui/internal/context.hpp ) set( @@ -404,16 +433,39 @@ set( ${PROJECT_SOURCE_DIR}/src/chars/icelake.cpp # ========================= - # DRAW + # IO + # ========================= + + ${PROJECT_SOURCE_DIR}/src/io/inputs.cpp + + # ========================= + # GFX + # ========================= + + ${PROJECT_SOURCE_DIR}/src/gfx/texture.cpp + ${PROJECT_SOURCE_DIR}/src/gfx/glyph.cpp + ${PROJECT_SOURCE_DIR}/src/gfx/render_list.cpp + ${PROJECT_SOURCE_DIR}/src/gfx/renderer.cpp + + ${PROJECT_SOURCE_DIR}/src/gfx/internal/rect_pack.cpp + ${PROJECT_SOURCE_DIR}/src/gfx/internal/texture.cpp + ${PROJECT_SOURCE_DIR}/src/gfx/internal/glyph.cpp + ${PROJECT_SOURCE_DIR}/src/gfx/internal/context.cpp + + ${PROJECT_SOURCE_DIR}/src/gfx/extension/glyph_parser_freetype.cpp + ${PROJECT_SOURCE_DIR}/src/gfx/extension/renderer_d3d11.cpp + ${PROJECT_SOURCE_DIR}/src/gfx/extension/renderer_d3d12.cpp + + # ========================= + # GUI # ========================= - ${PROJECT_SOURCE_DIR}/src/draw/font.cpp - ${PROJECT_SOURCE_DIR}/src/draw/shared_data.cpp - ${PROJECT_SOURCE_DIR}/src/draw/theme.cpp - ${PROJECT_SOURCE_DIR}/src/draw/mouse.cpp - ${PROJECT_SOURCE_DIR}/src/draw/draw_list.cpp - ${PROJECT_SOURCE_DIR}/src/draw/window.cpp - ${PROJECT_SOURCE_DIR}/src/draw/context.cpp + ${PROJECT_SOURCE_DIR}/src/gui/internal/common.cpp + ${PROJECT_SOURCE_DIR}/src/gui/internal/font.cpp + ${PROJECT_SOURCE_DIR}/src/gui/internal/draw_list.cpp + ${PROJECT_SOURCE_DIR}/src/gui/internal/mouse.cpp + ${PROJECT_SOURCE_DIR}/src/gui/internal/window.cpp + ${PROJECT_SOURCE_DIR}/src/gui/internal/context.cpp ) set_source_files_properties( diff --git a/src/draw/context.cpp b/src/draw/context.cpp deleted file mode 100644 index e55d3411..00000000 --- a/src/draw/context.cpp +++ /dev/null @@ -1,352 +0,0 @@ -// This file is part of prometheus -// Copyright (C) 2022-2025 Life4gal -// This file is subject to the license terms in the LICENSE file -// found in the top-level directory of this distribution. - -#include - -namespace {} - -namespace gal::prometheus::draw -{ - auto Context::find_window(const Window& window) const noexcept -> index_type - { - const auto it = std::ranges::find( - windows_ | std::views::reverse, - std::addressof(window), - [](const auto& w) noexcept -> const auto* { - return std::addressof(w); - } - ); - - const auto it_base = it.base(); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(it_base != std::ranges::end(windows_)); - return std::ranges::distance(std::ranges::begin(windows_), it_base); - } - - Context::Context() noexcept - : - // default_draw_list_shared_data_{}, - // font_default_{}, - theme_default_{Theme::default_theme()}, - draw_list_shared_data_stack_{}, - font_stack_{}, - theme_stack_{}, - draw_list_shared_data_current_{stack_use_default}, - font_current_{stack_use_default}, - theme_current_{stack_use_default}, - pad_{0}, - mouse_{.3f, 36}, - window_current_{nullptr}, - window_hovered_{nullptr}, - widget_id_hovered_{invalid_id}, - widget_id_activated_{invalid_id} - { - std::ignore = pad_; - } - - auto Context::instance() noexcept -> Context& - { - static Context context{}; - return context; - } - - auto Context::draw_list_shared_data() const noexcept -> const DrawListSharedData& - { - if (draw_list_shared_data_current_ == stack_use_default) - { - return draw_list_shared_data_default_; - } - - const auto* p = draw_list_shared_data_stack_[draw_list_shared_data_current_]; - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(p != nullptr); - - return *p; - } - - auto Context::push_draw_list_shared_data(DrawListSharedData& shared_data) noexcept -> void - { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(draw_list_shared_data_current_ == stack_use_default or draw_list_shared_data_current_ < draw_list_shared_data_stack_size); - - draw_list_shared_data_current_ += 1; - draw_list_shared_data_stack_[draw_list_shared_data_current_] = std::addressof(shared_data); - } - - auto Context::pop_draw_list_shared_data() noexcept -> void - { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(draw_list_shared_data_current_ != stack_use_default, "Unable to popup the default DrawListSharedData!"); - - draw_list_shared_data_stack_[draw_list_shared_data_current_] = nullptr; - if (draw_list_shared_data_current_ == 0) - { - draw_list_shared_data_current_ = stack_use_default; - } - else - { - draw_list_shared_data_current_ -= 1; - } - } - - auto Context::set_default_font(font_type font) noexcept -> void - { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(font != nullptr); - - font_default_ = std::move(font); - } - - auto Context::font() const noexcept -> const Font& - { - if (font_current_ == stack_use_default) - { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(font_default_ != nullptr); - - return *font_default_; - } - - const auto* p = font_stack_[font_current_]; - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(p != nullptr); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(p->get() != nullptr); - - return **p; - } - - auto Context::push_font(font_type& font) noexcept -> void - { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(font != nullptr); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(font_current_ == stack_use_default or font_current_ < font_stack_size); - - font_current_ += 1; - font_stack_[font_current_] = std::addressof(font); - } - - auto Context::pop_font() noexcept -> void - { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(font_current_ != stack_use_default, "Unable to popup the default Font!"); - - font_stack_[font_current_] = nullptr; - if (font_current_ == 0) - { - font_current_ = stack_use_default; - } - else - { - font_current_ -= 1; - } - } - - auto Context::theme() const noexcept -> const Theme& - { - if (theme_current_ == stack_use_default) - { - return theme_default_; - } - - const auto* p = theme_stack_[theme_current_]; - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(p != nullptr); - - return *p; - } - - auto Context::push_theme(Theme& theme) noexcept -> void - { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(theme_current_== stack_use_default or theme_current_ < theme_stack_size); - - theme_current_ += 1; - theme_stack_[theme_current_] = std::addressof(theme); - } - - auto Context::pop_theme() noexcept -> void - { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(theme_current_ != stack_use_default, "Unable to popup the default Theme!"); - - theme_stack_[theme_current_] = nullptr; - if (theme_current_ == 0) - { - theme_current_ = stack_use_default; - } - else - { - theme_current_ -= 1; - } - } - - auto Context::tooltip() const noexcept -> std::string_view - { - return tooltip_; - } - - auto Context::mouse() const noexcept -> const Mouse& - { - return mouse_; - } - - auto Context::is_widget_hovered(const id_type id) const noexcept -> bool - { - return widget_id_hovered_ == id; - } - - auto Context::is_widget_activated(const id_type id) const noexcept -> bool - { - return widget_id_activated_ == id; - } - - auto Context::invalidate_widget_hovered( - #if defined(GAL_PROMETHEUS_DRAW_CONTEXT_DEBUG) - const std::string& reason, - const std::source_location& location - #endif - ) noexcept -> void - { - #if defined(GAL_PROMETHEUS_DRAW_CONTEXT_DEBUG) - (void)reason; - (void)location; - #endif - - widget_id_hovered_ = Window::invalid_id; - } - - auto Context::invalidate_widget_activated( - #if defined(GAL_PROMETHEUS_DRAW_CONTEXT_DEBUG) - const std::string& reason, - const std::source_location& location - #endif - ) noexcept -> void - { - #if defined(GAL_PROMETHEUS_DRAW_CONTEXT_DEBUG) - (void)reason; - (void)location; - #endif - - widget_id_activated_ = Window::invalid_id; - } - - auto Context::test_widget_status( - const id_type id, - const rect_type& widget_rect, - const bool repeat - #if defined(GAL_PROMETHEUS_DRAW_CONTEXT_DEBUG) - , - const std::string& reason - #endif - ) noexcept -> widget_status_type - { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(window_current_ != nullptr); - - auto& window = *window_current_; - const rect_type rect{widget_rect.point + window.rect().left_top(), widget_rect.extent}; - const auto hovered = window_hovered_ == std::addressof(window) and rect.includes(mouse_.position()); - - widget_status_type widget_status{.hovered = hovered, .pressed = false, .keeping = false}; - if (hovered) - { - widget_id_hovered_ = id; - if (mouse_.clicked()) - { - widget_id_activated_ = id; - } - else if (repeat and widget_id_activated_ != Window::invalid_id) - { - widget_status.pressed = true; - } - } - - if (widget_id_activated_ == id) - { - if (mouse_.down()) - { - widget_status.keeping = true; - } - else - { - if (hovered) - { - widget_status.pressed = true; - } - - invalidate_widget_activated( - #if defined(GAL_PROMETHEUS_DRAW_CONTEXT_DEBUG) - std::format("Release mouse on widget #{}.({})", id, reason) - #endif - ); - } - } - - return widget_status; - } - - auto Context::find_window(const std::string_view name) const noexcept -> std::optional> - { - const auto it = std::ranges::find_if( - windows_, - [name](const auto& window) noexcept -> bool - { - return name == window.name(); - } - ); - - if (it == std::ranges::end(windows_)) - { - return std::nullopt; - } - - return std::make_optional(std::cref(*it)); - } - - auto Context::new_frame() noexcept -> void - { - tooltip_[0] = '\0'; - - // todo - mouse_.tick(1 / 60.f); - - std::ranges::for_each( - windows_ | std::views::reverse, - [this](auto& window) noexcept -> void - { - if (window.hovered(mouse_.position())) - { - window_hovered_ = std::addressof(window); - } - } - ); - - if (mouse_.clicked_) - { - if (window_hovered_ != nullptr) - { - const auto index = find_window(*window_hovered_); - const auto it = windows_.begin() + index; - - auto&& window = std::move(windows_[index]); - windows_.erase(it); - windows_.emplace_back(std::move(window)); - } - } - } - - auto Context::render() noexcept -> void - { - window_draw_lists_.clear(); - std::ranges::transform( - windows_, - std::back_inserter(window_draw_lists_), - [](auto& window) noexcept -> auto - { - return std::ref(window.render()); - } - ); - - if (window_current_ != nullptr and tooltip_[0] != '\0') - { - const auto index = find_window(*window_current_); - - auto& draw_list = window_draw_lists_[index].get(); - const auto& theme = this->theme(); - - // todo - const rect_type tooltip_rect{mouse_.position(), extent_type{100, 100}}; - draw_list.rect_filled(tooltip_rect, theme.color()); - draw_list.text(theme.font_size, mouse_.position(), theme.color(), tooltip_); - } - } -} diff --git a/src/draw/context.hpp b/src/draw/context.hpp deleted file mode 100644 index 429e804f..00000000 --- a/src/draw/context.hpp +++ /dev/null @@ -1,185 +0,0 @@ -// This file is part of prometheus -// Copyright (C) 2022-2025 Life4gal -// This file is subject to the license terms in the LICENSE file -// found in the top-level directory of this distribution. - -#pragma once - -#include -#include -#include -#include -#include -#include - -namespace gal::prometheus::draw -{ - class Context final - { - public: - template - using container_type = DrawListDef::container_type; - - using rect_type = DrawListDef::rect_type; - using point_type = DrawListDef::point_type; - using extent_type = DrawListDef::extent_type; - - using font_type = std::shared_ptr; - - constexpr static std::size_t draw_list_shared_data_stack_size = 8; - constexpr static std::size_t font_stack_size = 8; - constexpr static std::size_t theme_stack_size = 8; - - using id_type = Window::id_type; - constexpr static auto invalid_id = Window::invalid_id; - - using windows_type = container_type; - using index_type = windows_type::difference_type; - - private: - using stack_pointer_type = std::uint8_t; - constexpr static auto stack_use_default = std::numeric_limits::max(); - - // ---------------------------------------------------------------------- - // DrawListSharedData + Font + Theme - - DrawListSharedData draw_list_shared_data_default_; - font_type font_default_; - Theme theme_default_; - - DrawListSharedData* draw_list_shared_data_stack_[draw_list_shared_data_stack_size]; - font_type* font_stack_[font_stack_size]; - Theme* theme_stack_[theme_stack_size]; - - stack_pointer_type draw_list_shared_data_current_; - stack_pointer_type font_current_; - stack_pointer_type theme_current_; - stack_pointer_type pad_; - - // ---------------------------------------------------------------------- - // TOOLTIP - - std::string tooltip_; - - // ---------------------------------------------------------------------- - // MOUSE - - Mouse mouse_; - - // ---------------------------------------------------------------------- - // WINDOW - - windows_type windows_; - Window* window_current_; - Window* window_hovered_; - - id_type widget_id_hovered_; - id_type widget_id_activated_; - - container_type> window_draw_lists_; - - [[nodiscard]] auto find_window(const Window& window) const noexcept -> index_type; - - Context() noexcept; - - public: - // --------------------------------------------- - // SINGLETON - - [[nodiscard]] static auto instance() noexcept -> Context&; - - // --------------------------------------------- - // DRAW LIST SHARED DATA - - [[nodiscard]] auto draw_list_shared_data() const noexcept -> const DrawListSharedData&; - - auto push_draw_list_shared_data(DrawListSharedData& shared_data) noexcept -> void; - - auto pop_draw_list_shared_data() noexcept -> void; - - // --------------------------------------------- - // FONT - - auto set_default_font(font_type font) noexcept -> void; - - [[nodiscard]] auto font() const noexcept -> const Font&; - - auto push_font(font_type& font) noexcept -> void; - - auto pop_font() noexcept -> void; - - // --------------------------------------------- - // THEME - - [[nodiscard]] auto theme() const noexcept -> const Theme&; - - auto push_theme(Theme& theme) noexcept -> void; - - auto pop_theme() noexcept -> void; - - // --------------------------------------------- - // TOOLTIP - - [[nodiscard]] auto tooltip() const noexcept -> std::string_view; - - // --------------------------------------------- - // MOUSE - - [[nodiscard]] auto mouse() const noexcept -> const Mouse&; - - // --------------------------------------------- - // WINDOW & WIDGET - - [[nodiscard]] auto is_widget_hovered(id_type id) const noexcept -> bool; - - [[nodiscard]] auto is_widget_activated(id_type id) const noexcept -> bool; - - auto invalidate_widget_hovered( - #if defined(GAL_PROMETHEUS_DRAW_CONTEXT_DEBUG) - const std::string& reason, - const std::source_location& location = std::source_location::current() - #endif - ) noexcept -> void; - - auto invalidate_widget_activated( - #if defined(GAL_PROMETHEUS_DRAW_CONTEXT_DEBUG) - const std::string& reason, - const std::source_location& location = std::source_location::current() - #endif - ) noexcept -> void; - - struct widget_status_type - { - bool hovered; - bool pressed; - bool keeping; - }; - - [[nodiscard]] auto test_widget_status( - id_type id, - const rect_type& widget_rect, - bool repeat - #if defined(GAL_PROMETHEUS_DRAW_CONTEXT_DEBUG) - , - const std::string& reason - #endif - ) noexcept -> widget_status_type; - - [[nodiscard]] auto find_window(std::string_view name) const noexcept -> std::optional>; - - // --------------------------------------------- - // RENDER - - auto new_frame() noexcept -> void; - - auto render() noexcept -> void; - - // --------------------------------------------- - // for test only - - auto test_set_window(Window& window) noexcept -> void - { - window_current_ = &window; - } - }; -} diff --git a/src/draw/def.hpp b/src/draw/def.hpp deleted file mode 100644 index 11c615d8..00000000 --- a/src/draw/def.hpp +++ /dev/null @@ -1,125 +0,0 @@ -// This file is part of prometheus -// Copyright (C) 2022-2025 Life4gal -// This file is subject to the license terms in the LICENSE file -// found in the top-level directory of this distribution. - -#pragma once - -#include -#include -#include -#include -#include - -namespace gal::prometheus::draw -{ - class DrawListDef - { - public: - template - using container_type = std::vector; - - // ---------------------------------------------------- - - using rect_type = primitive::basic_rect_2d; - using point_type = rect_type::point_type; - using extent_type = rect_type::extent_type; - - using circle_type = primitive::basic_circle_2d; - using ellipse_type = primitive::basic_ellipse_2d; - - // ---------------------------------------------------- - - using uv_type = primitive::basic_point_2d; - using color_type = primitive::basic_color; - using vertex_type = primitive::basic_vertex; - using index_type = std::uint16_t; - - // ---------------------------------------------------- - - using path_list_type = container_type; - using vertex_list_type = container_type; - using index_list_type = container_type; - - // ---------------------------------------------------- - - using texture_id_type = std::uintptr_t; - using size_type = vertex_list_type::size_type; - - struct command_type - { - rect_type clip_rect; - texture_id_type texture_id; - - // ======================= - - // set by DrawList::index_list.size() - // start offset in `DrawList::index_list` - size_type index_offset; - // set by subsequent `DrawList::draw_xxx` - // number of indices (multiple of 3) to be rendered as triangles - size_type element_count; - }; - - using command_list_type = container_type; - - // ---------------------------------------------------- - - class Accessor - { - // command_type::element_count - std::reference_wrapper element_count_; - // DrawList::vertex_list - std::reference_wrapper vertex_list_; - // DrawList::index_list_; - std::reference_wrapper index_list_; - - public: - constexpr Accessor(command_type& command, vertex_list_type& vertex_list, index_list_type& index_list) noexcept - : element_count_{command.element_count}, - vertex_list_{vertex_list}, - index_list_{index_list} {} - - Accessor(const Accessor& other) = delete; - Accessor(Accessor&& other) noexcept = delete; - auto operator=(const Accessor& other) -> Accessor& = delete; - auto operator=(Accessor&& other) noexcept -> Accessor& = delete; - - ~Accessor() noexcept = default; - - constexpr auto reserve(const size_type vertex_count, const size_type index_count) const noexcept -> void - { - auto& element_count = element_count_.get(); - auto& vertex = vertex_list_.get(); - auto& index = index_list_.get(); - - element_count += index_count; - vertex.reserve(vertex.size() + vertex_count); - index.reserve(index.size() + index_count); - } - - [[nodiscard]] constexpr auto size() const noexcept -> size_type - { - const auto& list = vertex_list_.get(); - - return list.size(); - } - - constexpr auto add_vertex(const point_type& position, const uv_type& uv, const color_type& color) const noexcept -> void - { - auto& list = vertex_list_.get(); - - list.emplace_back(position, uv, color); - } - - constexpr auto add_index(const index_type a, const index_type b, const index_type c) const noexcept -> void - { - auto& list = index_list_.get(); - - list.push_back(a); - list.push_back(b); - list.push_back(c); - } - }; - }; -} diff --git a/src/draw/draw_list.cpp b/src/draw/draw_list.cpp deleted file mode 100644 index b8290bf1..00000000 --- a/src/draw/draw_list.cpp +++ /dev/null @@ -1,1857 +0,0 @@ -// This file is part of prometheus -// Copyright (C) 2022-2025 Life4gal -// This file is subject to the license terms in the LICENSE file -// found in the top-level directory of this distribution. -#include - -#include -#include - -#include -#include -#include - -namespace -{ - using namespace gal::prometheus; - - [[nodiscard]] constexpr auto to_fixed_rect_corner_flag(const draw::DrawFlag flag) noexcept -> draw::DrawFlag - { - using enum draw::DrawFlag; - - if ((flag & ROUND_CORNER_MASK) == NONE) - { - return ROUND_CORNER_ALL | flag; - } - - return flag; - } - - [[nodiscard]] constexpr auto to_fixed_normal(const float x, const float y) noexcept -> std::pair - { - if (const auto d = math::pow(x, 2) + math::pow(y, 2); - d > 1e-6f) - { - // fixme - const auto inv_len = [d] - { - // #if defined(__AVX512F__) - // __m512 d_v = _mm512_set1_ps(d); - // __m512 inv_len_v = _mm512_rcp14_ps(d_v); - // return _mm512_cvtss_f32(inv_len_v); - // #elif defined(__AVX__) - // __m256 d_v = _mm256_set1_ps(d); - // __m256 inv_len_v = _mm256_rcp_ps(d_v); - // return _mm256_cvtss_f32(inv_len_v); - // #elif defined(__SSE4_1__) or defined(__SSE3__) or defined(__SSE__) - // __m128 d_v = _mm_set_ss(d); - // __m128 inv_len_v = _mm_rcp_ss(d_v); - // return _mm_cvtss_f32(inv_len_v); - // #else - return 1.0f / d; - // #endif - }(); - - return {x * inv_len, y * inv_len}; - } - - return {x, y}; - } - - // fixme - constexpr std::size_t bezier_curve_casteljau_max_level = 10; - - using point_type = draw::DrawList::point_type; - - constexpr auto bezier_cubic_calc = [](const point_type& p1, const point_type& p2, const point_type& p3, const point_type& p4, const float tolerance) noexcept -> point_type - { - const auto u = 1.f - tolerance; - - const auto w1 = math::pow(u, 3); - const auto w2 = 3 * math::pow(u, 2) * tolerance; - const auto w3 = 3 * u * math::pow(tolerance, 2); - const auto w4 = math::pow(tolerance, 3); - - return - { - p1.x * w1 + p2.x * w2 + p3.x * w3 + p4.x * w4, - p1.y * w1 + p2.y * w2 + p3.y * w3 + p4.y * w4 - }; - }; - - constexpr auto bezier_quadratic_calc = [](const point_type& p1, const point_type& p2, const point_type& p3, const float tolerance) noexcept -> point_type - { - const auto u = 1.f - tolerance; - - const auto w1 = math::pow(u, 2); - const auto w2 = 2 * u * tolerance; - const auto w3 = math::pow(tolerance, 2); - - return - { - p1.x * w1 + p2.x * w2 + p3.x * w3, - p1.y * w1 + p2.y * w2 + p3.y * w3 - }; - }; -} - -namespace gal::prometheus::draw -{ - auto DrawList::make_accessor() noexcept -> DrawListDef::Accessor - { - auto& [command_list, vertex_list, index_list] = private_data_; - - return - { - command_list.back(), - vertex_list, - index_list - }; - } - - auto DrawList::push_command() noexcept -> void - { - // fixme: If the window boundary is smaller than the rect boundary, the rect will no longer be valid. - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not this_command_clip_rect_.empty() and this_command_clip_rect_.valid()); - - auto& [command_list, _, index_list] = private_data_; - - command_list.emplace_back( - command_type - { - .clip_rect = this_command_clip_rect_, - .texture_id = this_command_texture_id_, - .index_offset = index_list.size(), - // set by subsequent draw_xxx - .element_count = 0 - } - ); - } - - auto DrawList::on_element_changed(const ChangedElement element) noexcept -> void - { - auto& [command_list, vertex_list, index_list] = private_data_; - - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not command_list.empty()); - - const auto command_count = command_list.size(); - const auto& [current_clip_rect, current_texture_id, current_index_offset, current_element_count] = command_list.back(); - - if (current_element_count != 0) - { - if (element == ChangedElement::CLIP_RECT) - { - if (current_clip_rect != this_command_clip_rect_) - { - push_command(); - - return; - } - } - else if (element == ChangedElement::TEXTURE_ID) - { - if (current_texture_id != this_command_texture_id_) - { - push_command(); - - return; - } - } - else - { - GAL_PROMETHEUS_ERROR_DEBUG_UNREACHABLE(); - } - } - - // try to merge with previous command if it matches, else use current command - if (command_count > 1) - { - if ( - const auto& [previous_clip_rect, previous_texture, previous_index_offset, previous_element_count] = command_list[command_count - 2]; - current_element_count == 0 and - ( - this_command_clip_rect_ == previous_clip_rect and - this_command_texture_id_ == previous_texture - ) and - // sequential - current_index_offset == previous_index_offset + previous_element_count - ) - { - command_list.pop_back(); - return; - } - } - - if (element == ChangedElement::CLIP_RECT) - { - command_list.back().clip_rect = this_command_clip_rect_; - } - else if (element == ChangedElement::TEXTURE_ID) - { - command_list.back().texture_id = this_command_texture_id_; - } - else - { - GAL_PROMETHEUS_ERROR_DEBUG_UNREACHABLE(); - } - } - - auto DrawList::draw_polygon_line(const color_type& color, const DrawFlag draw_flag, const float thickness) noexcept -> void - { - const auto& font = Context::instance().font(); - - const auto accessor = make_accessor(); - - const auto path_point_count = path_list_.size(); - const auto& path_point = path_list_; - - if (path_point_count < 2 or color.alpha == 0) - { - return; - } - - const auto is_closed = (draw_flag & DrawFlag::CLOSED) != DrawFlag::NONE; - const auto segments_count = is_closed ? path_point_count : path_point_count - 1; - - const auto vertex_count = segments_count * 4; - const auto index_count = segments_count * 6; - accessor.reserve(vertex_count, index_count); - - for (std::decay_t i = 0; i < segments_count; ++i) - { - const auto n = (i + 1) % path_point_count; - - const auto& p1 = path_point[i]; - const auto& p2 = path_point[n]; - - auto [normalized_x, normalized_y] = math::normalize(p2.x - p1.x, p2.y - p1.y); - normalized_x *= (thickness * .5f); - normalized_y *= (thickness * .5f); - - const auto current_vertex_index = static_cast(accessor.size()); - const auto& opaque_uv = font.white_pixel_uv(); - - accessor.add_vertex(p1 + point_type{normalized_y, -normalized_x}, opaque_uv, color); - accessor.add_vertex(p2 + point_type{normalized_y, -normalized_x}, opaque_uv, color); - accessor.add_vertex(p2 + point_type{-normalized_y, normalized_x}, opaque_uv, color); - accessor.add_vertex(p1 + point_type{-normalized_y, normalized_x}, opaque_uv, color); - - accessor.add_index(current_vertex_index + 0, current_vertex_index + 1, current_vertex_index + 2); - accessor.add_index(current_vertex_index + 0, current_vertex_index + 2, current_vertex_index + 3); - } - } - - auto DrawList::draw_polygon_line_aa(const color_type& color, const DrawFlag draw_flag, float thickness) noexcept -> void - { - const auto& font = Context::instance().font(); - - const auto accessor = make_accessor(); - - const auto path_point_count = path_list_.size(); - const auto& path_point = path_list_; - - if (path_point_count < 2 or color.alpha == 0) - { - return; - } - - const auto& opaque_uv = font.white_pixel_uv(); - const auto transparent_color = color.transparent(); - - const auto is_closed = (draw_flag & DrawFlag::CLOSED) != DrawFlag::NONE; - const auto segments_count = is_closed ? path_point_count : path_point_count - 1; - const auto is_thick_line = thickness > 1.f; - - thickness = std::ranges::max(thickness, 1.f); - const auto thickness_integer = static_cast(thickness); - const auto thickness_fractional = thickness - static_cast(thickness_integer); - - const auto is_use_texture = - ( - (draw_list_flag_ & DrawListFlag::ANTI_ALIASED_LINE_USE_TEXTURE) == DrawListFlag::ANTI_ALIASED_LINE_USE_TEXTURE and - (thickness_integer < font.baked_line_max_width()) and - (thickness_fractional <= .00001f)); - - const auto vertex_cont = is_use_texture ? (path_point_count * 2) : (is_thick_line ? path_point_count * 4 : path_point_count * 3); - const auto index_count = is_use_texture ? (segments_count * 6) : (is_thick_line ? segments_count * 18 : segments_count * 12); - accessor.reserve(vertex_cont, index_count); - - // The first items are normals at each line point, then after that there are either 2 or 4 temp points for each line point - container_type temp_buffer{}; - temp_buffer.resize(path_point_count * ((is_use_texture or not is_thick_line) ? 3 : 5)); - auto temp_buffer_normals = std::span{temp_buffer.begin(), path_point_count}; - auto temp_buffer_points = std::span{temp_buffer.begin() + static_cast(path_point_count), temp_buffer.end()}; - - // Calculate normals (tangents) for each line segment - for (std::decay_t i = 0; i < segments_count; ++i) - { - const auto n = (i + 1) % path_point_count; - const auto d = path_point[n] - path_point[i]; - - const auto [normalized_x, normalized_y] = math::normalize(d.x, d.y); - temp_buffer_normals[i].x = normalized_y; - temp_buffer_normals[i].y = -normalized_x; - } - - if (not is_closed) - { - temp_buffer_normals[temp_buffer_normals.size() - 1] = temp_buffer_normals[temp_buffer_normals.size() - 2]; - } - - // If we are drawing a one-pixel-wide line without a texture, or a textured line of any width, we only need 2 or 3 vertices per point - if (is_use_texture or not is_thick_line) - { - // [PATH 1] Texture-based lines (thick or non-thick) - - // The width of the geometry we need to draw - this is essentially pixels for the line itself, plus "one pixel" for AA - const auto half_draw_size = is_use_texture ? ((thickness * .5f) + 1.f) : 1.f; - - // If line is not closed, the first and last points need to be generated differently as there are no normals to blend - if (not is_closed) - { - temp_buffer_points[0] = path_point[0] + temp_buffer_normals[0] * half_draw_size; - temp_buffer_points[1] = path_point[0] - temp_buffer_normals[0] * half_draw_size; - temp_buffer_points[(path_point_count - 1) * 2 + 0] = path_point[path_point_count - 1] + temp_buffer_normals[path_point_count - 1] * half_draw_size; - temp_buffer_points[(path_point_count - 1) * 2 + 1] = path_point[path_point_count - 1] - temp_buffer_normals[path_point_count - 1] * half_draw_size; - } - - const auto current_vertex_index = static_cast(accessor.size()); - - // Generate the indices to form a number of triangles for each line segment, and the vertices for the line edges - // This takes points n and n+1 and writes into n+1, with the first point in a closed line being generated from the final one (as n+1 wraps) - auto vertex_index_for_start = current_vertex_index; - for (std::decay_t first_point_of_segment = 0; first_point_of_segment < segments_count; ++first_point_of_segment) - { - const auto second_point_of_segment = (first_point_of_segment + 1) % path_point_count; - const auto vertex_index_for_end = static_cast( - // closed - (first_point_of_segment + 1) == path_point_count - ? current_vertex_index - : (vertex_index_for_start + (is_use_texture ? 2 : 3)) - ); - - // Average normals - const auto d = (temp_buffer_normals[first_point_of_segment] + temp_buffer_normals[second_point_of_segment]) * .5f; - // dm_x, dm_y are offset to the outer edge of the AA area - auto [dm_x, dm_y] = to_fixed_normal(d.x, d.y); - dm_x *= half_draw_size; - dm_y *= half_draw_size; - - // Add temporary vertexes for the outer edges - temp_buffer_points[second_point_of_segment * 2 + 0] = path_point[second_point_of_segment] + point_type{dm_x, dm_y}; - temp_buffer_points[second_point_of_segment * 2 + 1] = path_point[second_point_of_segment] - point_type{dm_x, dm_y}; - - if (is_use_texture) - { - // Add indices for two triangles - - // right - accessor.add_index(vertex_index_for_end + 0, vertex_index_for_start + 0, vertex_index_for_start + 1); - // left - accessor.add_index(vertex_index_for_end + 1, vertex_index_for_start + 1, vertex_index_for_end + 0); - } - else - { - // Add indexes for four triangles - - // right 1 - accessor.add_index(vertex_index_for_end + 0, vertex_index_for_start + 0, vertex_index_for_start + 2); - // right 2 - accessor.add_index(vertex_index_for_start + 2, vertex_index_for_end + 2, vertex_index_for_end + 0); - // left 1 - accessor.add_index(vertex_index_for_end + 1, vertex_index_for_start + 1, vertex_index_for_start + 0); - // left 2 - accessor.add_index(vertex_index_for_start + 0, vertex_index_for_end + 0, vertex_index_for_end + 1); - } - - vertex_index_for_start = vertex_index_for_end; - } - - // Add vertexes for each point on the line - if (is_use_texture) - { - GAL_PROMETHEUS_ERROR_ASSUME(not font.baked_line_uv().empty(), "draw::FontAtlasFlag::NO_BAKED_LINE"); - - const auto& uv = font.baked_line_uv()[thickness_integer]; - - const auto uv0 = uv.left_top(); - const auto uv1 = uv.right_bottom(); - for (std::decay_t i = 0; i < path_point_count; ++i) - { - // left-side outer edge - accessor.add_vertex(temp_buffer_points[i * 2 + 0], uv0, color); - // right-side outer edge - accessor.add_vertex(temp_buffer_points[i * 2 + 1], uv1, color); - } - } - else - { - // If we're not using a texture, we need the center vertex as well - for (std::decay_t i = 0; i < path_point_count; ++i) - { - // center of line - accessor.add_vertex(path_point[i], opaque_uv, color); - // left-side outer edge - accessor.add_vertex(temp_buffer_points[i * 2 + 0], opaque_uv, transparent_color); - // right-side outer edge - accessor.add_vertex(temp_buffer_points[i * 2 + 1], opaque_uv, transparent_color); - } - } - } - else - { - // [PATH 2] Non-texture-based lines (non-thick) - - // we need to draw the solid line core and thus require four vertices per point - const auto half_inner_thickness = (thickness - 1.f) * .5f; - - // If line is not closed, the first and last points need to be generated differently as there are no normals to blend - if (not is_closed) - { - const auto point_last = path_point_count - 1; - temp_buffer_points[0] = path_point[0] + temp_buffer_normals[0] * (half_inner_thickness + 1.f); - temp_buffer_points[1] = path_point[0] + temp_buffer_normals[0] * (half_inner_thickness + 0.f); - temp_buffer_points[2] = path_point[0] - temp_buffer_normals[0] * (half_inner_thickness + 0.f); - temp_buffer_points[3] = path_point[0] - temp_buffer_normals[0] * (half_inner_thickness + 1.f); - temp_buffer_points[point_last * 4 + 0] = path_point[point_last] + temp_buffer_normals[point_last] * (half_inner_thickness + 1.f); - temp_buffer_points[point_last * 4 + 1] = path_point[point_last] + temp_buffer_normals[point_last] * (half_inner_thickness + 0.f); - temp_buffer_points[point_last * 4 + 2] = path_point[point_last] - temp_buffer_normals[point_last] * (half_inner_thickness + 0.f); - temp_buffer_points[point_last * 4 + 3] = path_point[point_last] - temp_buffer_normals[point_last] * (half_inner_thickness + 1.f); - } - - const auto current_vertex_index = static_cast(accessor.size()); - - // Generate the indices to form a number of triangles for each line segment, and the vertices for the line edges - // This takes points n and n+1 and writes into n+1, with the first point in a closed line being generated from the final one (as n+1 wraps) - auto vertex_index_for_start = current_vertex_index; - for (std::decay_t first_point_of_segment = 0; first_point_of_segment < segments_count; ++first_point_of_segment) - { - const auto second_point_of_segment = (first_point_of_segment + 1) % path_point_count; - const auto vertex_index_for_end = static_cast( - (first_point_of_segment + 1) == path_point_count - ? current_vertex_index - : (vertex_index_for_start + 4) - ); - - // Average normals - const auto d = (temp_buffer_normals[first_point_of_segment] + temp_buffer_normals[second_point_of_segment]) * .5f; - const auto [dm_x, dm_y] = to_fixed_normal(d.x, d.y); - const auto dm_out_x = dm_x * (half_inner_thickness + 1.f); - const auto dm_out_y = dm_y * (half_inner_thickness + 1.f); - const auto dm_in_x = dm_x * (half_inner_thickness + 0.f); - const auto dm_in_y = dm_y * (half_inner_thickness + 0.f); - - // Add temporary vertices - temp_buffer_points[second_point_of_segment * 4 + 0] = path_point[second_point_of_segment] + point_type{dm_out_x, dm_out_y}; - temp_buffer_points[second_point_of_segment * 4 + 1] = path_point[second_point_of_segment] + point_type{dm_in_x, dm_in_y}; - temp_buffer_points[second_point_of_segment * 4 + 2] = path_point[second_point_of_segment] - point_type{dm_in_x, dm_in_y}; - temp_buffer_points[second_point_of_segment * 4 + 3] = path_point[second_point_of_segment] - point_type{dm_out_x, dm_out_y}; - - // Add indexes - accessor.add_index(vertex_index_for_end + 1, vertex_index_for_end + 1, vertex_index_for_start + 2); - accessor.add_index(vertex_index_for_start + 2, vertex_index_for_end + 2, vertex_index_for_end + 1); - accessor.add_index(vertex_index_for_end + 1, vertex_index_for_start + 1, vertex_index_for_start + 0); - accessor.add_index(vertex_index_for_start + 0, vertex_index_for_end + 0, vertex_index_for_end + 1); - accessor.add_index(vertex_index_for_end + 2, vertex_index_for_start + 2, vertex_index_for_start + 3); - accessor.add_index(vertex_index_for_start + 3, vertex_index_for_end + 3, vertex_index_for_end + 2); - - vertex_index_for_start = vertex_index_for_end; - } - - // Add vertices - for (std::decay_t i = 0; i < path_point_count; ++i) - { - accessor.add_vertex(temp_buffer_points[i * 4 + 0], opaque_uv, transparent_color); - accessor.add_vertex(temp_buffer_points[i * 4 + 1], opaque_uv, color); - accessor.add_vertex(temp_buffer_points[i * 4 + 2], opaque_uv, color); - accessor.add_vertex(temp_buffer_points[i * 4 + 2], opaque_uv, transparent_color); - } - } - } - - auto DrawList::draw_convex_polygon_line_filled(const color_type& color) noexcept -> void - { - const auto& font = Context::instance().font(); - - const auto accessor = make_accessor(); - - const auto path_point_count = path_list_.size(); - const auto& path_point = path_list_; - - if (path_point_count < 3 or color.alpha == 0) - { - return; - } - - const auto vertex_count = path_point_count; - const auto index_count = (path_point_count - 2) * 3; - accessor.reserve(vertex_count, index_count); - - const auto current_vertex_index = static_cast(accessor.size()); - const auto& opaque_uv = font.white_pixel_uv(); - - std::ranges::for_each( - path_point, - [&](const point_type& point) noexcept -> void - { - accessor.add_vertex(point, opaque_uv, color); - } - ); - for (index_type i = 2; std::cmp_less(i, path_point_count); ++i) - { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_index + i - 1 >= current_vertex_index); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_index + i >= current_vertex_index); - - accessor.add_index(current_vertex_index + 0, current_vertex_index + i - 1, current_vertex_index + i); - } - } - - auto DrawList::draw_convex_polygon_line_filled_aa(const color_type& color) noexcept -> void - { - const auto& font = Context::instance().font(); - - const auto accessor = make_accessor(); - - const auto path_point_count = path_list_.size(); - const auto& path_point = path_list_; - - if (path_point_count < 3 or color.alpha == 0) - { - return; - } - - const auto& opaque_uv = font.white_pixel_uv(); - const auto transparent_color = color.transparent(); - - const auto vertex_count = path_point_count * 2; - const auto index_count = (path_point_count - 2) * 3 + path_point_count * 6; - accessor.reserve(vertex_count, index_count); - - const auto current_vertex_inner_index = static_cast(accessor.size()); - const auto current_vertex_outer_index = static_cast(accessor.size() + 1); - - // Add indexes for fill - for (index_type i = 2; std::cmp_less(i, path_point_count); ++i) - { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_inner_index + static_cast((i - 1) << 1) >= current_vertex_inner_index); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_inner_index + static_cast(i << 1) >= current_vertex_inner_index); - - accessor.add_index( - current_vertex_inner_index + 0, - current_vertex_inner_index + static_cast((i - 1) << 1), - current_vertex_inner_index + static_cast(i << 1) - ); - } - - container_type temp_buffer{}; - temp_buffer.resize(path_point_count); - auto temp_buffer_normals = std::span{temp_buffer.begin(), path_point_count}; - - for (auto i = path_point_count - 1, n = static_cast(0); n < path_point_count; i = n++) - { - const auto d = path_point[n] - path_point[i]; - - const auto [normalized_x, normalized_y] = math::normalize(d.x, d.y); - temp_buffer_normals[i].x = normalized_y; - temp_buffer_normals[i].y = -normalized_x; - } - for (auto i = path_point_count - 1, n = static_cast(0); n < path_point_count; i = n++) - { - // Average normals - const auto d = (temp_buffer_normals[n] + temp_buffer_normals[i]) * .5f; - auto [dm_x, dm_y] = to_fixed_normal(d.x, d.y); - dm_x *= .5f; - dm_y *= .5f; - - // inner - accessor.add_vertex(path_point[n] - point_type{dm_x, dm_y}, opaque_uv, color); - // outer - accessor.add_vertex(path_point[n] + point_type{dm_x, dm_y}, opaque_uv, transparent_color); - - // Add indexes for fringes - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_inner_index + static_cast(n << 1) >= current_vertex_inner_index); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_inner_index + static_cast(i << 1) >= current_vertex_inner_index); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_outer_index + static_cast(i << 1) >= current_vertex_outer_index); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_outer_index + static_cast(i << 1) >= current_vertex_outer_index); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_outer_index + static_cast(n << 1) >= current_vertex_outer_index); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_inner_index + static_cast(n << 1) >= current_vertex_inner_index); - - accessor.add_index( - current_vertex_inner_index + static_cast(n << 1), - current_vertex_inner_index + static_cast(i << 1), - current_vertex_outer_index + static_cast(i << 1) - ); - accessor.add_index( - current_vertex_outer_index + static_cast(i << 1), - current_vertex_outer_index + static_cast(n << 1), - current_vertex_inner_index + static_cast(n << 1) - ); - } - } - - auto DrawList::draw_rect_filled( - const rect_type& rect, - const color_type& color_left_top, - const color_type& color_right_top, - const color_type& color_left_bottom, - const color_type& color_right_bottom - ) noexcept -> void - { - const auto& font = Context::instance().font(); - - const auto accessor = make_accessor(); - - // two triangle without path - constexpr auto vertex_count = 4; - constexpr auto index_count = 6; - accessor.reserve(vertex_count, index_count); - - const auto& opaque_uv = font.white_pixel_uv(); - - const auto current_vertex_index = static_cast(accessor.size()); - - accessor.add_vertex(rect.left_top(), opaque_uv, color_left_top); - accessor.add_vertex(rect.right_top(), opaque_uv, color_right_top); - accessor.add_vertex(rect.right_bottom(), opaque_uv, color_right_bottom); - accessor.add_vertex(rect.left_bottom(), opaque_uv, color_left_bottom); - - accessor.add_index(current_vertex_index + 0, current_vertex_index + 1, current_vertex_index + 2); - accessor.add_index(current_vertex_index + 0, current_vertex_index + 2, current_vertex_index + 3); - } - - auto DrawList::draw_text( - const Font& font, - const float font_size, - const point_type& p, - const color_type& color, - const std::string_view utf8_text, - const float wrap_width - ) noexcept -> void - { - const auto new_texture = this_command_texture_id_ != font.texture_id(); - - if (new_texture) - { - push_texture_id(font.texture_id()); - } - - font.text_draw(utf8_text, font_size, wrap_width, p, color, make_accessor()); - - if (new_texture) - { - pop_texture_id(); - } - } - - auto DrawList::draw_image( - const texture_id_type texture_id, - const point_type& display_p1, - const point_type& display_p2, - const point_type& display_p3, - const point_type& display_p4, - const uv_type& uv_p1, - const uv_type& uv_p2, - const uv_type& uv_p3, - const uv_type& uv_p4, - const color_type& color - ) noexcept -> void - { - const auto new_texture = this_command_texture_id_ != texture_id; - - if (new_texture) - { - push_texture_id(texture_id); - } - - const auto accessor = make_accessor(); - - // two triangle without path - constexpr auto vertex_count = 4; - constexpr auto index_count = 6; - accessor.reserve(vertex_count, index_count); - - const auto current_vertex_index = static_cast(accessor.size()); - - accessor.add_vertex(display_p1, uv_p1, color); - accessor.add_vertex(display_p2, uv_p2, color); - accessor.add_vertex(display_p3, uv_p3, color); - accessor.add_vertex(display_p4, uv_p4, color); - - accessor.add_index(current_vertex_index + 0, current_vertex_index + 1, current_vertex_index + 2); - accessor.add_index(current_vertex_index + 0, current_vertex_index + 2, current_vertex_index + 3); - - if (new_texture) - { - pop_texture_id(); - } - } - - auto DrawList::draw_image_rounded( - const texture_id_type texture_id, - const rect_type& display_rect, - const rect_type& uv_rect, - const color_type& color, - float rounding, - DrawFlag flag - ) noexcept -> void - { - // @see `path_rect` - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(display_rect.valid() and not display_rect.empty()); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(uv_rect.valid() and not uv_rect.empty()); - - if (rounding >= .5f) - { - flag = to_fixed_rect_corner_flag(flag); - - const auto v = - (flag & DrawFlag::ROUND_CORNER_TOP) == DrawFlag::ROUND_CORNER_TOP or - (flag & DrawFlag::ROUND_CORNER_BOTTOM) == DrawFlag::ROUND_CORNER_BOTTOM; - const auto h = - (flag & DrawFlag::ROUND_CORNER_LEFT) == DrawFlag::ROUND_CORNER_LEFT or - (flag & DrawFlag::ROUND_CORNER_RIGHT) == DrawFlag::ROUND_CORNER_RIGHT; - - rounding = std::ranges::min(rounding, display_rect.width() * (v ? .5f : 1.f) - 1.f); - rounding = std::ranges::min(rounding, display_rect.height() * (h ? .5f : 1.f) - 1.f); - } - - if (rounding < .5f or (DrawFlag::ROUND_CORNER_MASK & flag) == DrawFlag::ROUND_CORNER_NONE) - { - draw_image( - texture_id, - display_rect.left_top(), - display_rect.right_top(), - display_rect.right_bottom(), - display_rect.left_bottom(), - uv_rect.left_top(), - uv_rect.right_top(), - uv_rect.right_bottom(), - uv_rect.left_bottom(), - color - ); - } - else - { - const auto new_texture = this_command_texture_id_ != texture_id; - - if (new_texture) - { - push_texture_id(texture_id); - } - - const auto rounding_left_top = (flag & DrawFlag::ROUND_CORNER_LEFT_TOP) != DrawFlag::NONE ? rounding : 0; - const auto rounding_right_top = (flag & DrawFlag::ROUND_CORNER_RIGHT_TOP) != DrawFlag::NONE ? rounding : 0; - const auto rounding_left_bottom = (flag & DrawFlag::ROUND_CORNER_LEFT_BOTTOM) != DrawFlag::NONE ? rounding : 0; - const auto rounding_right_bottom = (flag & DrawFlag::ROUND_CORNER_RIGHT_BOTTOM) != DrawFlag::NONE ? rounding : 0; - - path_arc_fast({display_rect.left_top() + point_type{rounding_left_top, rounding_left_top}, rounding_left_top}, DrawArcFlag::Q2_CLOCK_WISH); - path_arc_fast({display_rect.right_top() + point_type{-rounding_right_top, rounding_right_top}, rounding_right_top}, DrawArcFlag::Q1_CLOCK_WISH); - path_arc_fast({display_rect.right_bottom() + point_type{-rounding_right_bottom, -rounding_right_bottom}, rounding_right_bottom}, DrawArcFlag::Q4_CLOCK_WISH); - path_arc_fast({display_rect.left_bottom() + point_type{rounding_left_bottom, -rounding_left_bottom}, rounding_left_bottom}, DrawArcFlag::Q3_CLOCK_WISH); - - auto& vertex_list = private_data_.vertex_list; - - const auto before_vertex_count = vertex_list.size(); - // draw - path_stroke(color); - const auto after_vertex_count = vertex_list.size(); - - // set uv manually - - const auto display_size = display_rect.size(); - const auto uv_size = uv_rect.size(); - const auto scale = uv_size / display_size; - - auto it = vertex_list.begin() + static_cast(before_vertex_count); - const auto end = vertex_list.begin() + static_cast(after_vertex_count); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(it < end); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(end == vertex_list.end()); - - // note: linear uv - const auto uv_min = uv_rect.left_top(); - // const auto uv_max = uv_rect.right_bottom(); - while (it != end) - { - const auto v = uv_min + (it->position - display_rect.left_top()) * scale; - - it->uv = - { - // std::ranges::clamp(v.x, uv_min.x, uv_max.x), - v.x, - // std::ranges::clamp(v.y, uv_min.y, uv_max.y) - v.y - }; - it += 1; - } - - if (new_texture) - { - pop_texture_id(); - } - } - } - - auto DrawList::path_stroke(const color_type& color, const DrawFlag flag, const float thickness) noexcept -> void - { - if ((draw_list_flag_ & DrawListFlag::ANTI_ALIASED_LINE) != DrawListFlag::NONE) - { - draw_polygon_line_aa(color, flag, thickness); - } - else - { - draw_polygon_line(color, flag, thickness); - } - - path_clear(); - } - - auto DrawList::path_stroke(const color_type& color) noexcept -> void - { - if ((draw_list_flag_ & DrawListFlag::ANTI_ALIASED_FILL) != DrawListFlag::NONE) - { - draw_convex_polygon_line_filled_aa(color); - } - else - { - draw_convex_polygon_line_filled(color); - } - - path_clear(); - } - - auto DrawList::path_arc_fast(const circle_type& circle, const int from, const int to) noexcept -> void - { - const auto& draw_list_shared_data = Context::instance().draw_list_shared_data(); - - const auto& [center, radius] = circle; - - if (radius < .5f) - { - path_pin(center); - return; - } - - // Calculate arc auto segment step size - auto step = DrawListSharedData::vertex_sample_points_count / draw_list_shared_data.get_circle_auto_segment_count(radius); - // Make sure we never do steps larger than one quarter of the circle - step = std::clamp(step, static_cast(1), DrawListSharedData::vertex_sample_points_count / 4); - - const auto sample_range = math::abs(to - from); - const auto next_step = step; - - auto extra_max_sample = false; - if (step > 1) - { - const auto overstep = sample_range % step; - if (overstep > 0) - { - extra_max_sample = true; - - // When we have overstepped to avoid awkwardly looking one long line and one tiny one at the end, - // distribute first step range evenly between them by reducing first step size. - step -= (step - overstep) / 2; - } - - path_reserve_extra(sample_range / step + 1 + (overstep > 0)); - } - else - { - path_reserve_extra(sample_range + 1); - } - - auto sample_index = from; - if (sample_index < 0 or std::cmp_greater_equal(sample_index, DrawListSharedData::vertex_sample_points_count)) - { - sample_index = sample_index % static_cast(DrawListSharedData::vertex_sample_points_count); - if (sample_index < 0) - { - sample_index += static_cast(DrawListSharedData::vertex_sample_points_count); - } - } - - if (to >= from) - { - for (int i = from; i <= to; i += static_cast(step), sample_index += static_cast(step), step = next_step) - { - // a_step is clamped to vertex_sample_points_count, so we have guaranteed that it will not wrap over range twice or more - if (std::cmp_greater_equal(sample_index, DrawListSharedData::vertex_sample_points_count)) - { - sample_index -= static_cast(DrawListSharedData::vertex_sample_points_count); - } - - const auto& sample_point = draw_list_shared_data.get_vertex_sample_point(sample_index); - - path_pin({center + sample_point * radius}); - } - } - else - { - for (int i = from; i >= to; i -= static_cast(step), sample_index -= static_cast(step), step = next_step) - { - // a_step is clamped to vertex_sample_points_count, so we have guaranteed that it will not wrap over range twice or more - if (sample_index < 0) - { - sample_index += static_cast(DrawListSharedData::vertex_sample_points_count); - } - - const auto& sample_point = draw_list_shared_data.get_vertex_sample_point(sample_index); - - path_pin({center + sample_point * radius}); - } - } - - if (extra_max_sample) - { - auto normalized_max_sample_index = to % static_cast(DrawListSharedData::vertex_sample_points_count); - if (normalized_max_sample_index < 0) - { - normalized_max_sample_index += DrawListSharedData::vertex_sample_points_count; - } - - const auto& sample_point = draw_list_shared_data.get_vertex_sample_point(normalized_max_sample_index); - - path_pin({center + sample_point * radius}); - } - } - - auto DrawList::path_arc_fast(const circle_type& circle, const DrawArcFlag flag) noexcept -> void - { - const auto [from, to] = range_of_arc_quadrant(flag); - - return path_arc_fast(circle, from, to); - } - - auto DrawList::path_arc_n(const circle_type& circle, const float from, const float to, const std::uint32_t segments) noexcept -> void - { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(to > from); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(from >= 0); - - const auto& [center, radius] = circle; - - if (radius < .5f) - { - path_pin(center); - return; - } - - path_reserve_extra(segments); - for (std::uint32_t i = 0; i < segments; ++i) - { - const auto a = from + static_cast(i) / static_cast(segments) * (to - from); - path_pin({center + point_type{math::cos(a), math::sin(a)} * radius}); - } - } - - auto DrawList::path_arc(const circle_type& circle, const float from, const float to) noexcept -> void - { - const auto& draw_list_shared_data = Context::instance().draw_list_shared_data(); - - const auto& [center, radius] = circle; - - if (radius < .5f) - { - path_pin(center); - return; - } - - // Automatic segment count - if (radius <= draw_list_shared_data.get_arc_fast_radius_cutoff()) - { - const auto is_reversed = to < from; - - // We are going to use precomputed values for mid-samples. - // Determine first and last sample in lookup table that belong to the arc - const auto sample_from_f = DrawListSharedData::vertex_sample_points_count * from / (std::numbers::pi_v * 2); - const auto sample_to_f = DrawListSharedData::vertex_sample_points_count * to / (std::numbers::pi_v * 2); - - const auto sample_from = is_reversed ? static_cast(math::floor(sample_from_f)) : static_cast(math::ceil(sample_from_f)); - const auto sample_to = is_reversed ? static_cast(math::ceil(sample_to_f)) : static_cast(math::floor(sample_to_f)); - const auto sample_mid = is_reversed ? static_cast(std::ranges::max(sample_from - sample_to, 0)) : static_cast(std::ranges::max(sample_to - sample_from, 0)); - - const auto segment_from_angle = static_cast(sample_from) * std::numbers::pi_v * 2 / DrawListSharedData::vertex_sample_points_count; - const auto segment_to_angle = static_cast(sample_to) * std::numbers::pi_v * 2 / DrawListSharedData::vertex_sample_points_count; - - const auto emit_start = math::abs(segment_from_angle - from) >= 1e-5f; - const auto emit_end = math::abs(to - segment_to_angle) >= 1e-5f; - - if (emit_start) - { - // The quadrant must be the same, otherwise it is not continuous with the path drawn by `path_arc_fast`. - path_pin({center + point_type{math::cos(from), -math::sin(from)} * radius}); - } - if (sample_mid > 0) - { - path_arc_fast(circle, sample_from, sample_to); - } - if (emit_end) - { - // The quadrant must be the same, otherwise it is not continuous with the path drawn by `path_arc_fast`. - path_pin({center + point_type{math::cos(to), -math::sin(to)} * radius}); - } - } - else - { - const auto arc_length = to - from; - const auto circle_segment_count = draw_list_shared_data.get_circle_auto_segment_count(radius); - const auto arc_segment_count = std::ranges::max( - static_cast(math::ceil(static_cast(circle_segment_count) * arc_length / (std::numbers::pi_v * 2))), - static_cast(std::numbers::pi_v * 2 / arc_length) - ); - path_arc_n(circle, from, to, arc_segment_count); - } - } - - auto DrawList::path_arc_elliptical_n(const ellipse_type& ellipse, const float from, const float to, const std::uint32_t segments) noexcept -> void - { - const auto& [center, radius, rotation] = ellipse; - const auto cos_theta = math::cos(rotation); - const auto sin_theta = math::sin(rotation); - - path_reserve_extra(segments); - for (std::uint32_t i = 0; i < segments; ++i) - { - const auto a = from + static_cast(i) / static_cast(segments) * (to - from); - const auto offset = point_type{math::cos(a), math::sin(a)} * radius; - const auto prime_x = offset.x * cos_theta - offset.y * sin_theta; - const auto prime_y = offset.x * sin_theta + offset.y * cos_theta; - path_pin({center + point_type{prime_x, prime_y}}); - } - } - - auto DrawList::path_quadrilateral(const point_type& p1, const point_type& p2, const point_type& p3, const point_type& p4) noexcept -> void - { - path_pin(p1); - path_pin(p2); - path_pin(p3); - path_pin(p4); - } - - auto DrawList::path_rect(const rect_type& rect, float rounding, DrawFlag flag) noexcept -> void - { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(rect.valid() and not rect.empty()); - - if (rounding >= .5f) - { - flag = to_fixed_rect_corner_flag(flag); - - const auto v = - (flag & DrawFlag::ROUND_CORNER_TOP) == DrawFlag::ROUND_CORNER_TOP or - (flag & DrawFlag::ROUND_CORNER_BOTTOM) == DrawFlag::ROUND_CORNER_BOTTOM; - const auto h = - (flag & DrawFlag::ROUND_CORNER_LEFT) == DrawFlag::ROUND_CORNER_LEFT or - (flag & DrawFlag::ROUND_CORNER_RIGHT) == DrawFlag::ROUND_CORNER_RIGHT; - - rounding = std::ranges::min(rounding, rect.width() * (v ? .5f : 1.f) - 1.f); - rounding = std::ranges::min(rounding, rect.height() * (h ? .5f : 1.f) - 1.f); - } - - if (rounding < .5f or (DrawFlag::ROUND_CORNER_MASK & flag) == DrawFlag::ROUND_CORNER_NONE) - { - path_quadrilateral(rect.left_top(), rect.right_top(), rect.right_bottom(), rect.left_bottom()); - } - else - { - const auto rounding_left_top = (flag & DrawFlag::ROUND_CORNER_LEFT_TOP) != DrawFlag::NONE ? rounding : 0; - const auto rounding_right_top = (flag & DrawFlag::ROUND_CORNER_RIGHT_TOP) != DrawFlag::NONE ? rounding : 0; - const auto rounding_left_bottom = (flag & DrawFlag::ROUND_CORNER_LEFT_BOTTOM) != DrawFlag::NONE ? rounding : 0; - const auto rounding_right_bottom = (flag & DrawFlag::ROUND_CORNER_RIGHT_BOTTOM) != DrawFlag::NONE ? rounding : 0; - - path_arc_fast({rect.left_top() + point_type{rounding_left_top, rounding_left_top}, rounding_left_top}, DrawArcFlag::Q2_CLOCK_WISH); - path_arc_fast({rect.right_top() + point_type{-rounding_right_top, rounding_right_top}, rounding_right_top}, DrawArcFlag::Q1_CLOCK_WISH); - path_arc_fast({rect.right_bottom() + point_type{-rounding_right_bottom, -rounding_right_bottom}, rounding_right_bottom}, DrawArcFlag::Q4_CLOCK_WISH); - path_arc_fast({rect.left_bottom() + point_type{rounding_left_bottom, -rounding_left_bottom}, rounding_left_bottom}, DrawArcFlag::Q3_CLOCK_WISH); - } - } - - auto DrawList::path_bezier_cubic_curve_casteljau( - const point_type& p1, - const point_type& p2, - const point_type& p3, - const point_type& p4, - const float tessellation_tolerance, - const std::size_t level - ) noexcept -> void - { - const auto dx = p4.x - p1.x; - const auto dy = p4.y - p1.y; - const auto d2 = math::abs((p2.x - p4.x) * dy - (p2.y - p4.y) * dx); - const auto d3 = math::abs((p3.x - p4.x) * dy - (p3.y - p4.y) * dx); - - if (math::pow(d2 + d3, 2) < tessellation_tolerance * (math::pow(dx, 2) + math::pow(dy, 2))) - { - path_pin(p4); - } - else if (level < bezier_curve_casteljau_max_level) - { - const auto p_12 = (p1 + p2) * .5f; - const auto p_23 = (p2 + p3) * .5f; - const auto p_34 = (p3 + p4) * .5f; - const auto p_123 = (p_12 + p_23) * .5f; - const auto p_234 = (p_23 + p_34) * .5f; - const auto p_1234 = (p_123 + p_234) * .5f; - - path_bezier_cubic_curve_casteljau(p1, p_12, p_123, p_1234, tessellation_tolerance, level + 1); - path_bezier_cubic_curve_casteljau(p_1234, p_234, p_34, p4, tessellation_tolerance, level + 1); - } - } - - auto DrawList::path_bezier_quadratic_curve_casteljau(const point_type& p1, const point_type& p2, const point_type& p3, const float tessellation_tolerance, const std::size_t level) noexcept -> void - { - const auto dx = p3.x - p1.x; - const auto dy = p3.y - p1.y; - const auto det = (p2.x - p3.x) * dy - (p2.y - p3.y) * dx; - - if (math::pow(det, 2) * 4.f < tessellation_tolerance * (math::pow(dx, 2) + math::pow(dy, 2))) - { - path_pin(p3); - } - else if (level < bezier_curve_casteljau_max_level) - { - const auto p_12 = (p1 + p2) * .5f; - const auto p_23 = (p2 + p3) * .5f; - const auto p_123 = (p_12 + p_23) * .5f; - - path_bezier_quadratic_curve_casteljau(p1, p_12, p_123, tessellation_tolerance, level + 1); - path_bezier_quadratic_curve_casteljau(p_123, p_23, p3, tessellation_tolerance, level + 1); - } - } - - auto DrawList::path_bezier_curve(const point_type& p1, const point_type& p2, const point_type& p3, const point_type& p4, const std::uint32_t segments) noexcept -> void - { - const auto& draw_list_shared_data = Context::instance().draw_list_shared_data(); - - path_pin(p1); - if (segments == 0) - { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(draw_list_shared_data.get_curve_tessellation_tolerance() > 0); - - path_reserve_extra(bezier_curve_casteljau_max_level * 2); - // auto-tessellated - path_bezier_cubic_curve_casteljau(p1, p2, p3, p4, draw_list_shared_data.get_curve_tessellation_tolerance(), 0); - } - else - { - path_reserve_extra(segments); - const auto step = 1.f / static_cast(segments); - for (std::uint32_t i = 1; i <= segments; ++i) - { - path_pin(bezier_cubic_calc(p1, p2, p3, p4, step * static_cast(i))); - } - } - } - - auto DrawList::path_bezier_quadratic_curve(const point_type& p1, const point_type& p2, const point_type& p3, const std::uint32_t segments) noexcept -> void - { - const auto& draw_list_shared_data = Context::instance().draw_list_shared_data(); - - path_pin(p1); - if (segments == 0) - { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(draw_list_shared_data.get_curve_tessellation_tolerance() > 0); - - path_reserve_extra(bezier_curve_casteljau_max_level * 2); - // auto-tessellated - path_bezier_quadratic_curve_casteljau(p1, p2, p3, draw_list_shared_data.get_curve_tessellation_tolerance(), 0); - } - else - { - path_reserve_extra(segments); - const auto step = 1.f / static_cast(segments); - for (std::uint32_t i = 1; i <= segments; ++i) - { - path_pin(bezier_quadratic_calc(p1, p2, p3, step * static_cast(i))); - } - } - } - - DrawList::DrawList() noexcept - : draw_list_flag_{DrawListFlag::NONE}, - this_command_clip_rect_{0, 0, 0, 0}, - this_command_texture_id_{0} - { - // reset(); - } - - auto DrawList::draw_list_flag(const DrawListFlag flag) noexcept -> void - { - draw_list_flag_ = flag; - } - - auto DrawList::draw_list_flag(const std::underlying_type_t flag) noexcept -> void - { - draw_list_flag(static_cast(flag)); - } - - auto DrawList::reset() noexcept -> void - { - const auto& font = Context::instance().font(); - - auto& [command_list, vertex_list, index_list] = private_data_; - - command_list.clear(); - vertex_list.resize(0); - index_list.resize(0); - - // we don't know the size of the clip rect, so we need the user to set it - this_command_clip_rect_ = {}; - // the first texture is always the (default) font texture - this_command_texture_id_ = font.texture_id(); - - path_list_.clear(); - - // we always have a command ready in the buffer - command_list.emplace_back( - command_type - { - .clip_rect = this_command_clip_rect_, - .texture_id = this_command_texture_id_, - .index_offset = index_list.size(), - // set by subsequent draw_xxx - .element_count = 0 - } - ); - } - - auto DrawList::push_clip_rect(const rect_type& rect, const bool intersect_with_current_clip_rect) noexcept -> rect_type& - { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not rect.empty() and rect.valid()); - - auto& [command_list, _1, _2] = private_data_; - - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not command_list.empty()); - - const auto& [current_clip_rect, current_texture, current_index_offset, current_element_count] = command_list.back(); - - this_command_clip_rect_ = intersect_with_current_clip_rect ? rect.combine_min(current_clip_rect) : rect; - - on_element_changed(ChangedElement::CLIP_RECT); - return command_list.back().clip_rect; - } - - auto DrawList::push_clip_rect(const point_type& left_top, const point_type& right_bottom, const bool intersect_with_current_clip_rect) noexcept -> rect_type& - { - return push_clip_rect({left_top, right_bottom}, intersect_with_current_clip_rect); - } - - auto DrawList::pop_clip_rect() noexcept -> void - { - const auto& [command_list, _1, _2] = private_data_; - - // at least one command - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(command_list.size() > 1); - this_command_clip_rect_ = command_list[command_list.size() - 2].clip_rect; - - on_element_changed(ChangedElement::CLIP_RECT); - } - - auto DrawList::push_texture_id(const texture_id_type texture) noexcept -> void - { - this_command_texture_id_ = texture; - - on_element_changed(ChangedElement::TEXTURE_ID); - } - - auto DrawList::pop_texture_id() noexcept -> void - { - const auto& [command_list, _1, _2] = private_data_; - - // at least one command - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(command_list.size() > 1); - this_command_texture_id_ = command_list[command_list.size() - 2].texture_id; - - on_element_changed(ChangedElement::TEXTURE_ID); - } - - auto DrawList::line( - const point_type& from, - const point_type& to, - const color_type& color, - const float thickness - ) noexcept -> void - { - if (color.alpha == 0) - { - return; - } - - // path_pin(from + point_type{.5f, .5f}); - // path_pin(to + point_type{.5f, .5f}); - path_pin(from); - path_pin(to); - - path_stroke(color, DrawFlag::NONE, thickness); - } - - - auto DrawList::triangle( - const point_type& a, - const point_type& b, - const point_type& c, - const color_type& color, - const float thickness - ) noexcept -> void - { - if (color.alpha == 0) - { - return; - } - - path_pin(a); - path_pin(b); - path_pin(c); - - path_stroke(color, DrawFlag::CLOSED, thickness); - } - - auto DrawList::triangle_filled( - const point_type& a, - const point_type& b, - const point_type& c, - const color_type& color - ) noexcept -> void - { - if (color.alpha == 0) - { - return; - } - - path_pin(a); - path_pin(b); - path_pin(c); - - path_stroke(color); - } - - auto DrawList::rect( - const rect_type& rect, - const color_type& color, - const float rounding, - const DrawFlag flag, - const float thickness - ) noexcept -> void - { - if (color.alpha == 0) - { - return; - } - - // path_rect(rect_type{rect.left_top() + point_type{.5f, .5f}, rect.right_bottom() - point_type{.5f, .5f}}, rounding, flag); - path_rect(rect, rounding, flag); - - path_stroke(color, DrawFlag::CLOSED, thickness); - } - - auto DrawList::rect( - const point_type& left_top, - const point_type& right_bottom, - const color_type& color, - const float rounding, - const DrawFlag flag, - const float thickness - ) noexcept -> void - { - return rect({left_top, right_bottom}, color, rounding, flag, thickness); - } - - auto DrawList::rect_filled( - const rect_type& rect, - const color_type& color, - const float rounding, - const DrawFlag flag - ) noexcept -> void - { - if (color.alpha == 0) - { - return; - } - - if (rounding < .5f or (DrawFlag::ROUND_CORNER_MASK & flag) == DrawFlag::ROUND_CORNER_NONE) - { - draw_rect_filled(rect, color, color, color, color); - } - else - { - path_rect(rect, rounding, flag); - path_stroke(color); - } - } - - auto DrawList::rect_filled( - const point_type& left_top, - const point_type& right_bottom, - const color_type& color, - const float rounding, - const DrawFlag flag - ) noexcept -> void - { - return rect_filled({left_top, right_bottom}, color, rounding, flag); - } - - auto DrawList::rect_filled( - const rect_type& rect, - const color_type& color_left_top, - const color_type& color_right_top, - const color_type& color_left_bottom, - const color_type& color_right_bottom - ) noexcept -> void - { - if (color_left_top.alpha == 0 or color_right_top.alpha == 0 or color_left_bottom.alpha == 0 or color_right_bottom.alpha == 0) - { - return; - } - - draw_rect_filled(rect, color_left_top, color_right_top, color_left_bottom, color_right_bottom); - } - - auto DrawList::rect_filled( - const point_type& left_top, - const point_type& right_bottom, - const color_type& color_left_top, - const color_type& color_right_top, - const color_type& color_left_bottom, - const color_type& color_right_bottom - ) noexcept -> void - { - return rect_filled({left_top, right_bottom}, color_left_top, color_right_top, color_left_bottom, color_right_bottom); - } - - auto DrawList::quadrilateral( - const point_type& p1, - const point_type& p2, - const point_type& p3, - const point_type& p4, - const color_type& color, - const float thickness - ) noexcept -> void - { - if (color.alpha == 0) - { - return; - } - - path_quadrilateral(p1, p2, p3, p4); - - path_stroke(color, DrawFlag::CLOSED, thickness); - } - - auto DrawList::quadrilateral_filled( - const point_type& p1, - const point_type& p2, - const point_type& p3, - const point_type& p4, - const color_type& color - ) noexcept -> void - { - if (color.alpha == 0) - { - return; - } - - path_quadrilateral(p1, p2, p3, p4); - - path_stroke(color); - } - - auto DrawList::circle_n( - const circle_type& circle, - const color_type& color, - const std::uint32_t segments, - const float thickness - ) noexcept -> void - { - if (color.alpha == 0 or circle.radius < .5f or segments < 3) - { - return; - } - - path_arc_n(circle, 0, std::numbers::pi_v * 2, segments); - - path_stroke(color, DrawFlag::CLOSED, thickness); - } - - auto DrawList::circle_n( - const point_type& center, - const float radius, - const color_type& color, - const std::uint32_t segments, - const float thickness - ) noexcept -> void - { - return circle_n({center, radius}, color, segments, thickness); - } - - auto DrawList::ellipse_n( - const ellipse_type& ellipse, - const color_type& color, - const std::uint32_t segments, - const float thickness - ) noexcept -> void - { - if (color.alpha == 0 or ellipse.radius.width < .5f or ellipse.radius.height < .5f or segments < 3) - { - return; - } - - path_arc_elliptical_n(ellipse, 0, std::numbers::pi_v * 2, segments); - - path_stroke(color, DrawFlag::CLOSED, thickness); - } - - auto DrawList::ellipse_n( - const point_type& center, - const extent_type& radius, - const float rotation, - const color_type& color, - const std::uint32_t segments, - const float thickness - ) noexcept -> void - { - return ellipse_n({center, radius, rotation}, color, segments, thickness); - } - - auto DrawList::circle_n_filled( - const circle_type& circle, - const color_type& color, - const std::uint32_t segments - ) noexcept -> void - { - if (color.alpha == 0 or circle.radius < .5f or segments < 3) - { - return; - } - - path_arc_n(circle, 0, std::numbers::pi_v * 2, segments); - - path_stroke(color); - } - - auto DrawList::circle_n_filled( - const point_type& center, - const float radius, - const color_type& color, - const std::uint32_t segments - ) noexcept -> void - { - return circle_n_filled({center, radius}, color, segments); - } - - auto DrawList::ellipse_n_filled( - const ellipse_type& ellipse, - const color_type& color, - const std::uint32_t segments - ) noexcept -> void - { - if (color.alpha == 0 or ellipse.radius.width < .5f or ellipse.radius.height < .5f or segments < 3) - { - return; - } - - path_arc_elliptical_n(ellipse, 0, std::numbers::pi_v * 2, segments); - - path_stroke(color); - } - - auto DrawList::ellipse_n_filled( - const point_type& center, - const extent_type& radius, - const float rotation, - const color_type& color, - const std::uint32_t segments - ) noexcept -> void - { - return ellipse_n_filled({center, radius, rotation}, color, segments); - } - - auto DrawList::circle( - const circle_type& circle, - const color_type& color, - const std::uint32_t segments, - const float thickness - ) noexcept -> void - { - if (color.alpha == 0 or circle.radius < .5f) - { - return; - } - - if (segments == 0) - { - path_arc_fast(circle, 0, DrawListSharedData::vertex_sample_points_count - 1); - - path_stroke(color, DrawFlag::CLOSED, thickness); - } - else - { - const auto clamped_segments = std::ranges::clamp(segments, DrawListSharedData::circle_segments_min, DrawListSharedData::circle_segments_max); - - circle_n(circle, color, clamped_segments, thickness); - } - } - - auto DrawList::circle( - const point_type& center, - const float radius, - const color_type& color, - const std::uint32_t segments, - const float thickness - ) noexcept -> void - { - return circle({center, radius}, color, segments, thickness); - } - - auto DrawList::circle_filled( - const circle_type& circle, - const color_type& color, - const std::uint32_t segments - ) noexcept -> void - { - if (color.alpha == 0 or circle.radius < .5f) - { - return; - } - - if (segments == 0) - { - path_arc_fast(circle, 0, DrawListSharedData::vertex_sample_points_count - 1); - - path_stroke(color); - } - else - { - const auto clamped_segments = std::ranges::clamp(segments, DrawListSharedData::circle_segments_min, DrawListSharedData::circle_segments_max); - - circle_n_filled(circle, color, clamped_segments); - } - } - - auto DrawList::circle_filled( - const point_type& center, - const float radius, - const color_type& color, - const std::uint32_t segments - ) noexcept -> void - { - return circle_filled({center, radius}, color, segments); - } - - auto DrawList::ellipse( - const ellipse_type& ellipse, - const color_type& color, - std::uint32_t segments, - const float thickness - ) noexcept -> void - { - const auto& draw_list_shared_data = Context::instance().draw_list_shared_data(); - - if (color.alpha == 0 or ellipse.radius.width < .5f or ellipse.radius.height < .5f) - { - return; - } - - if (segments == 0) - { - // fixme: maybe there's a better computation to do here - segments = draw_list_shared_data.get_circle_auto_segment_count(std::ranges::max(ellipse.radius.width, ellipse.radius.height)); - } - - ellipse_n(ellipse, color, segments, thickness); - } - - auto DrawList::ellipse( - const point_type& center, - const extent_type& radius, - const float rotation, - const color_type& color, - const std::uint32_t segments, - const float thickness - ) noexcept -> void - { - return ellipse({center, radius, rotation}, color, segments, thickness); - } - - auto DrawList::ellipse_filled( - const ellipse_type& ellipse, - const color_type& color, - std::uint32_t segments - ) noexcept -> void - { - const auto& draw_list_shared_data = Context::instance().draw_list_shared_data(); - - if (color.alpha == 0 or ellipse.radius.width < .5f or ellipse.radius.height < .5f) - { - return; - } - - if (segments == 0) - { - // fixme: maybe there's a better computation to do here - segments = draw_list_shared_data.get_circle_auto_segment_count(std::ranges::max(ellipse.radius.width, ellipse.radius.height)); - } - - ellipse_n_filled(ellipse, color, segments); - } - - auto DrawList::ellipse_filled( - const point_type& center, - const extent_type& radius, - const float rotation, - const color_type& color, - const std::uint32_t segments - ) noexcept -> void - { - return ellipse_filled({center, radius, rotation}, color, segments); - } - - auto DrawList::bezier_cubic( - const point_type& p1, - const point_type& p2, - const point_type& p3, - const point_type& p4, - const color_type& color, - const std::uint32_t segments, - const float thickness - ) noexcept -> void - { - if (color.alpha == 0) - { - return; - } - - path_bezier_curve(p1, p2, p3, p4, segments); - - path_stroke(color, DrawFlag::NONE, thickness); - } - - auto DrawList::bezier_quadratic( - const point_type& p1, - const point_type& p2, - const point_type& p3, - const color_type& color, - const std::uint32_t segments, - const float thickness - ) noexcept -> void - { - if (color.alpha == 0) - { - return; - } - - path_bezier_quadratic_curve(p1, p2, p3, segments); - - path_stroke(color, DrawFlag::NONE, thickness); - } - - auto DrawList::text( - const Font& font, - const float font_size, - const point_type& p, - const color_type& color, - const std::string_view utf8_text, - const float wrap_width - ) noexcept -> void - { - if (color.alpha == 0) - { - return; - } - - draw_text(font, font_size, p, color, utf8_text, wrap_width); - } - - auto DrawList::text( - const float font_size, - const point_type& p, - const color_type& color, - const std::string_view utf8_text, - const float wrap_width - ) noexcept -> void - { - const auto& font = Context::instance().font(); - - this->text(font, font_size, p, color, utf8_text, wrap_width); - } - - auto DrawList::image( - const texture_id_type texture_id, - const point_type& display_p1, - const point_type& display_p2, - const point_type& display_p3, - const point_type& display_p4, - const uv_type& uv_p1, - const uv_type& uv_p2, - const uv_type& uv_p3, - const uv_type& uv_p4, - const color_type& color - ) noexcept -> void - { - if (color.alpha == 0) - { - return; - } - - draw_image(texture_id, display_p1, display_p2, display_p3, display_p4, uv_p1, uv_p2, uv_p3, uv_p4, color); - } - - auto DrawList::image( - const texture_id_type texture_id, - const rect_type& display_rect, - const rect_type& uv_rect, - const color_type& color - ) noexcept -> void - { - image( - texture_id, - display_rect.left_top(), - display_rect.right_top(), - display_rect.right_bottom(), - display_rect.left_bottom(), - uv_rect.left_top(), - uv_rect.right_top(), - uv_rect.right_bottom(), - uv_rect.left_bottom(), - color - ); - } - - auto DrawList::image( - const texture_id_type texture_id, - const point_type& display_left_top, - const point_type& display_right_bottom, - const uv_type& uv_left_top, - const uv_type& uv_right_bottom, - const color_type& color - ) noexcept -> void - { - image(texture_id, {display_left_top, display_right_bottom}, {uv_left_top, uv_right_bottom}, color); - } - - auto DrawList::image_rounded( - const texture_id_type texture_id, - const rect_type& display_rect, - const float rounding, - const DrawFlag flag, - const rect_type& uv_rect, - const color_type& color - ) noexcept -> void - { - if (color.alpha == 0) - { - return; - } - - draw_image_rounded(texture_id, display_rect, uv_rect, color, rounding, flag); - } - - auto DrawList::image_rounded( - const texture_id_type texture_id, - const point_type& display_left_top, - const point_type& display_right_bottom, - const float rounding, - const DrawFlag flag, - const uv_type& uv_left_top, - const uv_type& uv_right_bottom, - const color_type& color - ) noexcept -> void - { - image_rounded(texture_id, {display_left_top, display_right_bottom}, rounding, flag, {uv_left_top, uv_right_bottom}, color); - } -} diff --git a/src/draw/draw_list.hpp b/src/draw/draw_list.hpp deleted file mode 100644 index fe3454b7..00000000 --- a/src/draw/draw_list.hpp +++ /dev/null @@ -1,569 +0,0 @@ -// This file is part of prometheus -// Copyright (C) 2022-2025 Life4gal -// This file is subject to the license terms in the LICENSE file -// found in the top-level directory of this distribution. - -#pragma once - -#include -#include - -namespace gal::prometheus::draw -{ - class Font; - - class DrawList final - { - public: - template - using container_type = DrawListDef::container_type; - - // ---------------------------------------------------- - - using rect_type = DrawListDef::rect_type; - using point_type = DrawListDef::point_type; - using extent_type = DrawListDef::extent_type; - - using circle_type = DrawListDef::circle_type; - using ellipse_type = DrawListDef::ellipse_type; - - // ---------------------------------------------------- - - using uv_type = DrawListDef::uv_type; - using color_type = DrawListDef::color_type; - using vertex_type = DrawListDef::vertex_type; - using index_type = DrawListDef::index_type; - - // ---------------------------------------------------- - - using path_list_type = DrawListDef::path_list_type; - using vertex_list_type = DrawListDef::vertex_list_type; - using index_list_type = DrawListDef::index_list_type; - - // ---------------------------------------------------- - - using texture_id_type = DrawListDef::texture_id_type; - using size_type = DrawListDef::size_type; - - using command_type = DrawListDef::command_type; - using command_list_type = DrawListDef::command_list_type; - - private: - DrawListFlag draw_list_flag_; - - /** - * @brief This wrapper structure expects to limit the data writes to a limited number of functions to avoid unintended data writes. - * @see make_accessor - * @see push_command - * @see on_element_changed - * @see reset - * @see push_clip_rect - * @see draw_image_rounded (fill image uv) - */ - struct private_data_type - { - // vertex_list: v1-v2-v3-v4 + v5-v6-v7-v8 + v9-v10-v11 => rect0 + rect1(clipped by rect0) + triangle0(clipped by rect1) - // index_list: 0/1/2-0/2/3 + 4/5/6-4/6/7 + 8/9/10 - // command_list: - // 0: .clip_rect = {0, 0, root_window_width, root_window_height}, .index_offset = 0, .element_count = root_window_element_count + 6 (two triangles => 0/1/2-0/2/3) - // 1: .clip_rect = {max(rect0.left, rect1.left), max(rect0.top, rect1.top), min(rect0.right, rect1.right), min(rect0.bottom, rect1.bottom)}, .index_offset = root_window_element_count + 6, .element_count = 6 (two triangles => 4/5/6-4/6/7) - // 2: .clip_rect = {...}, .index_offset = root_window_element_count + 12, .element_count = 3 (one triangle => 8/9/10) - command_list_type command_list; - vertex_list_type vertex_list; - index_list_type index_list; - }; - - private_data_type private_data_; - - rect_type this_command_clip_rect_; - texture_id_type this_command_texture_id_; - - path_list_type path_list_; - - [[nodiscard]] auto make_accessor() noexcept -> DrawListDef::Accessor; - - auto push_command() noexcept -> void; - - enum class ChangedElement : std::uint8_t - { - CLIP_RECT, - TEXTURE_ID, - }; - - auto on_element_changed(ChangedElement element) noexcept -> void; - - // ---------------------------------------------------------------------------- - // DRAW - - auto draw_polygon_line(const color_type& color, DrawFlag draw_flag, float thickness) noexcept -> void; - - auto draw_polygon_line_aa(const color_type& color, DrawFlag draw_flag, float thickness) noexcept -> void; - - auto draw_convex_polygon_line_filled(const color_type& color) noexcept -> void; - - auto draw_convex_polygon_line_filled_aa(const color_type& color) noexcept -> void; - - auto draw_rect_filled( - const rect_type& rect, - const color_type& color_left_top, - const color_type& color_right_top, - const color_type& color_left_bottom, - const color_type& color_right_bottom - ) noexcept -> void; - - auto draw_text( - const Font& font, - float font_size, - const point_type& p, - const color_type& color, - std::string_view utf8_text, - float wrap_width - ) noexcept -> void; - - auto draw_image( - texture_id_type texture_id, - const point_type& display_p1, - const point_type& display_p2, - const point_type& display_p3, - const point_type& display_p4, - const uv_type& uv_p1, - const uv_type& uv_p2, - const uv_type& uv_p3, - const uv_type& uv_p4, - const color_type& color - ) noexcept -> void; - - auto draw_image_rounded( - texture_id_type texture_id, - const rect_type& display_rect, - const rect_type& uv_rect, - const color_type& color, - float rounding, - DrawFlag flag - ) noexcept -> void; - - // ---------------------------------------------------------------------------- - // PATH - - constexpr auto path_clear() noexcept -> void - { - path_list_.clear(); - } - - constexpr auto path_reserve(const std::size_t size) noexcept -> void - { - path_list_.reserve(size); - } - - constexpr auto path_reserve_extra(const std::size_t size) noexcept -> void - { - path_reserve(path_list_.size() + size); - } - - constexpr auto path_pin(const point_type& point) noexcept -> void - { - path_list_.push_back(point); - } - - auto path_stroke(const color_type& color, DrawFlag flag, float thickness) noexcept -> void; - - auto path_stroke(const color_type& color) noexcept -> void; - - auto path_arc_fast(const circle_type& circle, int from, int to) noexcept -> void; - - auto path_arc_fast(const circle_type& circle, DrawArcFlag flag) noexcept -> void; - - auto path_arc_n(const circle_type& circle, float from, float to, std::uint32_t segments) noexcept -> void; - - auto path_arc(const circle_type& circle, float from, float to) noexcept -> void; - - auto path_arc_elliptical_n(const ellipse_type& ellipse, float from, float to, std::uint32_t segments) noexcept -> void; - - auto path_quadrilateral(const point_type& p1, const point_type& p2, const point_type& p3, const point_type& p4) noexcept -> void; - - auto path_rect(const rect_type& rect, float rounding, DrawFlag flag) noexcept -> void; - - auto path_bezier_cubic_curve_casteljau( - const point_type& p1, - const point_type& p2, - const point_type& p3, - const point_type& p4, - float tessellation_tolerance, - std::size_t level - ) noexcept -> void; - - auto path_bezier_quadratic_curve_casteljau( - const point_type& p1, - const point_type& p2, - const point_type& p3, - float tessellation_tolerance, - std::size_t level - ) noexcept -> void; - - auto path_bezier_curve( - const point_type& p1, - const point_type& p2, - const point_type& p3, - const point_type& p4, - std::uint32_t segments - ) noexcept -> void; - - auto path_bezier_quadratic_curve( - const point_type& p1, - const point_type& p2, - const point_type& p3, - std::uint32_t segments - ) noexcept -> void; - - public: - /** - * @note The default font may not be set at this point, so we must manually call @c reset before using it. - */ - DrawList() noexcept; - - // ---------------------------------------------------------------------------- - // FLAG - - auto draw_list_flag(DrawListFlag flag) noexcept -> void; - - auto draw_list_flag(std::underlying_type_t flag) noexcept -> void; - - // ---------------------------------------------------------------------------- - // RESET - - auto reset() noexcept -> void; - - // ---------------------------------------------------------------------------- - // DRAW DATA - - [[nodiscard]] constexpr auto command_list() const noexcept -> auto - { - return private_data_.command_list | std::views::all; - } - - [[nodiscard]] constexpr auto vertex_list() const noexcept -> auto - { - return private_data_.vertex_list | std::views::all; - } - - [[nodiscard]] constexpr auto index_list() const noexcept -> auto - { - return private_data_.index_list | std::views::all; - } - - // ---------------------------------------------------------------------------- - // CLIP RECT & TEXTURE - - auto push_clip_rect(const rect_type& rect, bool intersect_with_current_clip_rect) noexcept -> rect_type&; - - auto push_clip_rect(const point_type& left_top, const point_type& right_bottom, bool intersect_with_current_clip_rect) noexcept -> rect_type&; - - auto pop_clip_rect() noexcept -> void; - - auto push_texture_id(texture_id_type texture) noexcept -> void; - - auto pop_texture_id() noexcept -> void; - - // ---------------------------------------------------------------------------- - // PRIMITIVE - - auto line( - const point_type& from, - const point_type& to, - const color_type& color, - float thickness = 1.f - ) noexcept -> void; - - auto triangle( - const point_type& a, - const point_type& b, - const point_type& c, - const color_type& color, - float thickness = 1.f - ) noexcept -> void; - - auto triangle_filled( - const point_type& a, - const point_type& b, - const point_type& c, - const color_type& color - ) noexcept -> void; - - auto rect( - const rect_type& rect, - const color_type& color, - float rounding = .0f, - DrawFlag flag = DrawFlag::NONE, - float thickness = 1.f - ) noexcept -> void; - - auto rect( - const point_type& left_top, - const point_type& right_bottom, - const color_type& color, - float rounding = .0f, - DrawFlag flag = DrawFlag::NONE, - float thickness = 1.f - ) noexcept -> void; - - auto rect_filled( - const rect_type& rect, - const color_type& color, - float rounding = .0f, - DrawFlag flag = DrawFlag::NONE - ) noexcept -> void; - - auto rect_filled( - const point_type& left_top, - const point_type& right_bottom, - const color_type& color, - float rounding = .0f, - DrawFlag flag = DrawFlag::NONE - ) noexcept -> void; - - auto rect_filled( - const rect_type& rect, - const color_type& color_left_top, - const color_type& color_right_top, - const color_type& color_left_bottom, - const color_type& color_right_bottom - ) noexcept -> void; - - auto rect_filled( - const point_type& left_top, - const point_type& right_bottom, - const color_type& color_left_top, - const color_type& color_right_top, - const color_type& color_left_bottom, - const color_type& color_right_bottom - ) noexcept -> void; - - auto quadrilateral( - const point_type& p1, - const point_type& p2, - const point_type& p3, - const point_type& p4, - const color_type& color, - float thickness = 1.f - ) noexcept -> void; - - auto quadrilateral_filled( - const point_type& p1, - const point_type& p2, - const point_type& p3, - const point_type& p4, - const color_type& color - ) noexcept -> void; - - auto circle_n( - const circle_type& circle, - const color_type& color, - std::uint32_t segments, - float thickness = 1.f - ) noexcept -> void; - - auto circle_n( - const point_type& center, - float radius, - const color_type& color, - std::uint32_t segments, - float thickness = 1.f - ) noexcept -> void; - - auto ellipse_n( - const ellipse_type& ellipse, - const color_type& color, - std::uint32_t segments, - float thickness = 1.f - ) noexcept -> void; - - auto ellipse_n( - const point_type& center, - const extent_type& radius, - float rotation, - const color_type& color, - std::uint32_t segments, - float thickness = 1.f - ) noexcept -> void; - - auto circle_n_filled( - const circle_type& circle, - const color_type& color, - std::uint32_t segments - ) noexcept -> void; - - auto circle_n_filled( - const point_type& center, - float radius, - const color_type& color, - std::uint32_t segments - ) noexcept -> void; - - auto ellipse_n_filled( - const ellipse_type& ellipse, - const color_type& color, - std::uint32_t segments - ) noexcept -> void; - - auto ellipse_n_filled( - const point_type& center, - const extent_type& radius, - float rotation, - const color_type& color, - std::uint32_t segments - ) noexcept -> void; - - auto circle( - const circle_type& circle, - const color_type& color, - std::uint32_t segments = 0, - float thickness = 1.f - ) noexcept -> void; - - auto circle( - const point_type& center, - float radius, - const color_type& color, - std::uint32_t segments = 0, - float thickness = 1.f - ) noexcept -> void; - - auto circle_filled( - const circle_type& circle, - const color_type& color, - std::uint32_t segments = 0 - ) noexcept -> void; - - auto circle_filled( - const point_type& center, - float radius, - const color_type& color, - std::uint32_t segments = 0 - ) noexcept -> void; - - auto ellipse( - const ellipse_type& ellipse, - const color_type& color, - std::uint32_t segments = 0, - float thickness = 1.f - ) noexcept -> void; - - auto ellipse( - const point_type& center, - const extent_type& radius, - float rotation, - const color_type& color, - std::uint32_t segments = 0, - float thickness = 1.f - ) noexcept -> void; - - auto ellipse_filled( - const ellipse_type& ellipse, - const color_type& color, - std::uint32_t segments = 0 - ) noexcept -> void; - - auto ellipse_filled( - const point_type& center, - const extent_type& radius, - float rotation, - const color_type& color, - std::uint32_t segments = 0 - ) noexcept -> void; - - auto bezier_cubic( - const point_type& p1, - const point_type& p2, - const point_type& p3, - const point_type& p4, - const color_type& color, - std::uint32_t segments = 0, - float thickness = 1.f - ) noexcept -> void; - - auto bezier_quadratic( - const point_type& p1, - const point_type& p2, - const point_type& p3, - const color_type& color, - std::uint32_t segments = 0, - float thickness = 1.f - ) noexcept -> void; - - // ---------------------------------------------------------------------------- - // TEXT - - auto text( - const Font& font, - float font_size, - const point_type& p, - const color_type& color, - std::string_view utf8_text, - float wrap_width = std::numeric_limits::max() - ) noexcept -> void; - - auto text( - float font_size, - const point_type& p, - const color_type& color, - std::string_view utf8_text, - float wrap_width = std::numeric_limits::max() - ) noexcept -> void; - - // ---------------------------------------------------------------------------- - // IMAGE - - // p1________ p2 - // | | - // | | - // p4|_______| p3 - auto image( - texture_id_type texture_id, - const point_type& display_p1, - const point_type& display_p2, - const point_type& display_p3, - const point_type& display_p4, - const uv_type& uv_p1 = {0, 0}, - const uv_type& uv_p2 = {1, 0}, - const uv_type& uv_p3 = {1, 1}, - const uv_type& uv_p4 = {0, 1}, - const color_type& color = primitive::colors::white - ) noexcept -> void; - - auto image( - texture_id_type texture_id, - const rect_type& display_rect, - const rect_type& uv_rect = {0, 0, 1, 1}, - const color_type& color = primitive::colors::white - ) noexcept -> void; - - auto image( - texture_id_type texture_id, - const point_type& display_left_top, - const point_type& display_right_bottom, - const uv_type& uv_left_top = {0, 0}, - const uv_type& uv_right_bottom = {1, 1}, - const color_type& color = primitive::colors::white - ) noexcept -> void; - - auto image_rounded( - texture_id_type texture_id, - const rect_type& display_rect, - float rounding = .0f, - DrawFlag flag = DrawFlag::NONE, - const rect_type& uv_rect = {0, 0, 1, 1}, - const color_type& color = primitive::colors::white - ) noexcept -> void; - - auto image_rounded( - texture_id_type texture_id, - const point_type& display_left_top, - const point_type& display_right_bottom, - float rounding = .0f, - DrawFlag flag = DrawFlag::NONE, - const uv_type& uv_left_top = {0, 0}, - const uv_type& uv_right_bottom = {1, 1}, - const color_type& color = primitive::colors::white - ) noexcept -> void; - }; -} diff --git a/src/draw/flag.hpp b/src/draw/flag.hpp deleted file mode 100644 index 03f390b8..00000000 --- a/src/draw/flag.hpp +++ /dev/null @@ -1,162 +0,0 @@ -// This file is part of prometheus -// Copyright (C) 2022-2025 Life4gal -// This file is subject to the license terms in the LICENSE file -// found in the top-level directory of this distribution. - -#pragma once - -#include -#include -#include -#include - -#include - -namespace gal::prometheus::draw -{ - enum class DrawFlag : std::uint8_t - { - NONE = 0, - // specify that shape should be closed - // @see DrawList::draw_polygon_line - // @see DrawList::draw_polygon_line_aa - // @see DrawList::path_stroke - CLOSED = 1 << 0, - // enable rounding left-top corner only (when rounding > 0.0f, we default to all corners) - // @see DrawList::path_rect - // @see DrawList::rect - // @see DrawList::rect_filled - ROUND_CORNER_LEFT_TOP = 1 << 1, - // enable rounding right_top corner only (when rounding > 0.0f, we default to all corners) - // @see DrawList::path_rect - // @see DrawList::rect - // @see DrawList::rect_filled - ROUND_CORNER_RIGHT_TOP = 1 << 2, - // enable rounding left-bottom corner only (when rounding > 0.0f, we default to all corners) - // @see DrawList::path_rect - // @see DrawList::rect - // @see DrawList::rect_filled - ROUND_CORNER_LEFT_BOTTOM = 1 << 3, - // enable rounding right-bottom corner only (when rounding > 0.0f, we default to all corners) - // @see DrawList::path_rect - // @see DrawList::rect - // @see DrawList::rect_filled - ROUND_CORNER_RIGHT_BOTTOM = 1 << 4, - // disable rounding on all corners (when rounding > 0.0f) - ROUND_CORNER_NONE = 1 << 5, - - ROUND_CORNER_LEFT = ROUND_CORNER_LEFT_TOP | ROUND_CORNER_LEFT_BOTTOM, - ROUND_CORNER_TOP = ROUND_CORNER_LEFT_TOP | ROUND_CORNER_RIGHT_TOP, - ROUND_CORNER_RIGHT = ROUND_CORNER_RIGHT_TOP | ROUND_CORNER_RIGHT_BOTTOM, - ROUND_CORNER_BOTTOM = ROUND_CORNER_LEFT_BOTTOM | ROUND_CORNER_RIGHT_BOTTOM, - - ROUND_CORNER_ALL = ROUND_CORNER_LEFT_TOP | ROUND_CORNER_RIGHT_TOP | ROUND_CORNER_LEFT_BOTTOM | ROUND_CORNER_RIGHT_BOTTOM, - ROUND_CORNER_DEFAULT = ROUND_CORNER_ALL, - ROUND_CORNER_MASK = ROUND_CORNER_ALL | ROUND_CORNER_NONE, - - PROMETHEUS_MAGIC_ENUM_FLAG = std::numeric_limits::max(), - }; - - enum class DrawListFlag : std::uint8_t - { - NONE = 0, - ANTI_ALIASED_LINE = 1 << 0, - ANTI_ALIASED_LINE_USE_TEXTURE = 1 << 1, - ANTI_ALIASED_FILL = 1 << 2, - - PROMETHEUS_MAGIC_ENUM_FLAG = std::numeric_limits::max(), - }; - - enum class DrawArcFlag : std::uint8_t - { - // [0~3) - Q1 = 1 << 0, - // [3~6) - Q2 = 1 << 1, - // [6~9) - Q3 = 1 << 2, - // [9~12) - Q4 = 1 << 3, - - RIGHT_TOP = Q1, - LEFT_TOP = Q2, - LEFT_BOTTOM = Q3, - RIGHT_BOTTOM = Q4, - TOP = Q1 | Q2, - BOTTOM = Q3 | Q4, - LEFT = Q2 | Q3, - RIGHT = Q1 | Q4, - ALL = Q1 | Q2 | Q3 | Q4, - - // [3, 0) - Q1_CLOCK_WISH = 1 << 4, - // [6, 3) - Q2_CLOCK_WISH = 1 << 5, - // [9, 6) - Q3_CLOCK_WISH = 1 << 6, - // [12, 9) - Q4_CLOCK_WISH = 1 << 7, - - RIGHT_TOP_CLOCK_WISH = Q1_CLOCK_WISH, - LEFT_TOP_CLOCK_WISH = Q2_CLOCK_WISH, - LEFT_BOTTOM_CLOCK_WISH = Q3_CLOCK_WISH, - RIGHT_BOTTOM_CLOCK_WISH = Q4_CLOCK_WISH, - TOP_CLOCK_WISH = Q1_CLOCK_WISH | Q2_CLOCK_WISH, - BOTTOM_CLOCK_WISH = Q3_CLOCK_WISH | Q4_CLOCK_WISH, - LEFT_CLOCK_WISH = Q2_CLOCK_WISH | Q3_CLOCK_WISH, - RIGHT_CLOCK_WISH = Q1_CLOCK_WISH | Q4_CLOCK_WISH, - ALL_CLOCK_WISH = Q1_CLOCK_WISH | Q2_CLOCK_WISH | Q3_CLOCK_WISH | Q4_CLOCK_WISH, - - PROMETHEUS_MAGIC_ENUM_FLAG = std::numeric_limits::max(), - }; - - [[nodiscard]] auto range_of_arc_quadrant(DrawArcFlag quadrant) noexcept -> std::pair; - - enum class ThemeCategory : std::uint8_t - { - TEXT = 0, - BORDER, - - WINDOW_BACKGROUND, - - WIDGET_BACKGROUND, - WIDGET_ACTIVATED, - - TITLE_BAR, - TITLE_BAR_COLLAPSED, - - SLIDER, - SLIDER_ACTIVATED, - - BUTTON, - BUTTON_HOVERED, - BUTTON_ACTIVATED, - - RESIZE_GRIP, - RESIZE_GRIP_HOVERED, - RESIZE_GRIP_ACTIVATED, - - TOOLTIP_BACKGROUND, - TOOLTIP_TEXT, - - // ------------------------------- - INTERNAL_COUNT - }; - - constexpr auto theme_category_count = static_cast(ThemeCategory::INTERNAL_COUNT); - - enum class WindowFlag : std::uint8_t - { - NONE = 0, - - BORDERED = 1 << 0, - - NO_TITLE_BAR = 1 << 1, - // Meaningful if and only if NO_TITLE_BAR is not set - NO_CLOSE = 1 << 2, - NO_RESIZE = 1 << 3, - NO_MOVE = 1 << 4, - - PROMETHEUS_MAGIC_ENUM_FLAG = std::numeric_limits::max(), - }; -} diff --git a/src/draw/font.cpp b/src/draw/font.cpp deleted file mode 100644 index 2c6b318c..00000000 --- a/src/draw/font.cpp +++ /dev/null @@ -1,548 +0,0 @@ -// This file is part of prometheus -// Copyright (C) 2022-2025 Life4gal -// This file is subject to the license terms in the LICENSE file -// found in the top-level directory of this distribution. - -#include - -#include -#include -#include -#include -#include -#include - -#include -#include FT_FREETYPE_H // -// #include FT_MODULE_H // -// #include FT_GLYPH_H // -// #include FT_SYNTHESIS_H // - -#define STB_RECT_PACK_IMPLEMENTATION -#include - -#include -#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE - -namespace -{ - struct ft_type - { - FT_Library library; - FT_Face face; - - [[nodiscard]] constexpr auto valid() const noexcept -> bool - { - return library and face; - } - }; - - [[nodiscard]] auto create_ft(const std::string_view font_path, const std::uint32_t pixel_height) noexcept -> ft_type - { - FT_Library ft_library; - if (FT_Init_FreeType(&ft_library)) - { - // Could not initialize FreeType library - return {.library = nullptr, .face = nullptr}; - } - - FT_Face ft_face; - if (FT_New_Face(ft_library, font_path.data(), 0, &ft_face)) - { - FT_Done_FreeType(ft_library); - // Could not load font - return {.library = nullptr, .face = nullptr}; - } - - FT_Set_Pixel_Sizes(ft_face, 0, pixel_height); - return {.library = ft_library, .face = ft_face}; - } - - auto destroy_ft(const ft_type ft) noexcept -> void - { - auto [ft_library, ft_face] = ft; - - FT_Done_Face(ft_face); - FT_Done_FreeType(ft_library); - } -} - -namespace gal::prometheus::draw -{ - Font::Texture::~Texture() noexcept - { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(id_ != invalid_texture_id, "Texture is not bound to a GPU resource id!"); - } - - auto Font::Texture::bind(const texture_id_type id) const noexcept -> void - { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(valid(), "Only valid textures can be bound to a GPU resource id"); - - id_.get() = id; - } - - auto Font::reset() noexcept -> void - { - font_path_.clear(); - pixel_height_ = std::numeric_limits::min(); - baked_line_max_width_ = std::numeric_limits::min(); - - glyphs_.clear(); - fallback_glyph_ = {}; - - white_pixel_uv_ = {}; - baked_line_uv_ = {}; - - texture_id_ = invalid_texture_id; - } - - Font::Font() noexcept - { - reset(); - } - - Font::~Font() noexcept = default; - - auto Font::option() noexcept -> Option - { - return {}; - } - - auto Font::load(const Option& option) noexcept -> Texture - { - reset(); - - font_path_ = std::format("{}-{}px", option.font_path_, option.pixel_height_); - pixel_height_ = option.pixel_height_; - - if (option.baked_line_max_width_ == 0 or option.baked_line_max_width_ > default_baked_line_max_width) - { - baked_line_max_width_ = default_baked_line_max_width; - } - else - { - baked_line_max_width_ = option.baked_line_max_width_; - } - - Texture texture{texture_id_}; - - auto ft = create_ft(option.font_path_, option.pixel_height_); - if (not ft.valid()) - { - return texture; - } - - auto [library, face] = ft; - - // =============================== - - std::vector rects; - - // baked line - constexpr auto id_baked_line = std::numeric_limits::min() + 0; - { - const auto baked_line_uv_width = baked_line_max_width_ + 1; - const auto baked_line_uv_height = baked_line_max_width_ + 2; - - baked_line_uv_.reserve(baked_line_uv_height); - - rects.emplace_back( - stbrp_rect - { - .id = id_baked_line, - .w = static_cast(baked_line_uv_width), - .h = static_cast(baked_line_uv_height), - .x = 0, - .y = 0, - .was_packed = 0 - } - ); - } - - // =============================== - - std::ranges::for_each( - option.glyph_ranges_, - [&face, &rects](const auto& pair) noexcept -> void - { - const auto [from, to] = pair; - - for (auto c = from; c <= to; ++c) - { - if (FT_Load_Char(face, c, FT_LOAD_RENDER)) - { - continue; - } - - const auto& g = face->glyph; - rects.emplace_back( - stbrp_rect - { - .id = std::bit_cast(c), - .w = static_cast(g->bitmap.width), - .h = static_cast(g->bitmap.rows), - .x = static_cast(g->bitmap_left), - .y = static_cast(g->bitmap_top), - .was_packed = 0 - } - ); - } - } - ); - - // =============================== - - // todo: texture size? - const auto size = [&rects]() - { - Texture::size_type total_area = 0; - Texture::size_type max_width = 0; - Texture::size_type max_height = 0; - - for (const auto& [id, w, h, x, y, was_packed]: rects) - { - total_area += w * h; - max_width = std::ranges::max(max_width, static_cast(w)); - max_height = std::ranges::max(max_height, static_cast(h)); - } - - const auto min_side = static_cast(std::sqrt(total_area)); - const auto max_side = std::ranges::max(max_width, max_height); - - return std::bit_ceil(std::ranges::max(min_side, max_side)); - }(); - auto atlas_width = size; - auto atlas_height = size; - - stbrp_context context; - std::vector nodes{atlas_width}; - while (true) - { - stbrp_init_target(&context, static_cast(atlas_width), static_cast(atlas_height), nodes.data(), static_cast(nodes.size())); - if (stbrp_pack_rects(&context, rects.data(), static_cast(rects.size()))) - { - break; - } - - atlas_width *= 2; - atlas_height *= 2; - nodes.resize(atlas_width); - } - - // =============================== - - // note: We don't necessarily overwrite all the memory, but it doesn't matter. - // auto texture_data = std::make_unique(static_cast(atlas_width * atlas_height)); - auto texture_data = std::make_unique_for_overwrite(static_cast(atlas_width * atlas_height)); - - const uv_extent_type texture_uv_scale{1.f / static_cast(atlas_width), 1.f / static_cast(atlas_height)}; - - // =============================== - - for (const auto& [id, rect_width, rect_height, rect_x, rect_y, was_packed]: rects) - { - if (id == id_baked_line) - { - using value_type = uv_point_type::value_type; - constexpr std::uint32_t white_color = 0xff'ff'ff'ff; - - // hacky: baked line rect area, one pixel - { - const auto x = rect_x + rect_y * atlas_width; - texture_data[x] = white_color; - - const auto uv_x = static_cast(static_cast(rect_x) + .5f) * texture_uv_scale.width; - const auto uv_y = static_cast(static_cast(rect_y) + .5f) * texture_uv_scale.height; - - white_pixel_uv_ = uv_point_type{uv_x, uv_y}; - } - - // ◿ - for (stbrp_coord y = 0; y < rect_height; ++y) - { - const auto line_width = y; - const auto offset_y = (rect_y + y) * atlas_width; - - for (stbrp_coord x = line_width; x > 0; --x) - { - const auto offset_x = rect_x + (rect_width - x); - const auto index = offset_x + offset_y; - texture_data[index] = white_color; - } - - const auto p_x = rect_x + (rect_width - line_width); - const auto p_y = rect_y + y; - const auto width = line_width; - constexpr auto height = .5f; - - const auto uv_x = static_cast(p_x) * texture_uv_scale.width; - const auto uv_y = static_cast(p_y) * texture_uv_scale.height; - const auto uv_width = static_cast(width) * texture_uv_scale.width; - const auto uv_height = static_cast(height) * texture_uv_scale.height; - - baked_line_uv_.emplace_back(uv_x, uv_y, uv_width, uv_height); - } - - continue; - } - - const auto c = std::bit_cast(id); - - // todo - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(c <= std::numeric_limits::max()); - - if (FT_Load_Char(face, c, FT_LOAD_RENDER)) - { - continue; - } - - const auto& g = face->glyph; - - for (std::uint32_t y = 0; y < g->bitmap.rows; ++y) - { - for (std::uint32_t x = 0; x < g->bitmap.width; ++x) - { - const auto index = rect_x + x + (rect_y + y) * atlas_width; - const auto a = g->bitmap.buffer[x + y * g->bitmap.pitch] << 24; - const auto color = - // A - a | - // B - std::uint32_t{0xff} << 16 | - // G - std::uint32_t{0xff} << 8 | - // R - std::uint32_t{0xff}; - texture_data[index] = color; - } - } - - const auto p_x = static_cast(g->bitmap_left); - const auto p_y = static_cast(g->bitmap_top); - const auto p_width = static_cast(g->bitmap.width); - const auto p_height = static_cast(g->bitmap.rows); - - const auto uv_x = static_cast(rect_x) * texture_uv_scale.width; - const auto uv_y = static_cast(rect_y) * texture_uv_scale.height; - const auto uv_width = static_cast(g->bitmap.width) * texture_uv_scale.width; - const auto uv_height = static_cast(g->bitmap.rows) * texture_uv_scale.height; - - const glyph_type glyph{ - .rect = {p_x, p_y, p_width, p_height}, - .uv = {uv_x, uv_y, uv_width, uv_height}, - .advance_x = static_cast(g->advance.x) / 64.f - }; - - glyphs_.insert_or_assign(static_cast(c), glyph); - } - - fallback_glyph_ = glyphs_[static_cast('?')]; - - // =============================== - - destroy_ft(ft); - - texture.set_size(atlas_width, atlas_height); - texture.set_data(std::move(texture_data)); - return texture; - } - - auto Font::loaded() const noexcept -> bool - { - return not glyphs_.empty() and texture_id_ != invalid_texture_id; - } - - auto Font::font_path() const noexcept -> std::string_view - { - return font_path_; - } - - auto Font::pixel_height() const noexcept -> std::uint32_t - { - return pixel_height_; - } - - auto Font::baked_line_max_width() const noexcept -> std::uint32_t - { - return baked_line_max_width_; - } - - auto Font::glyphs() const noexcept -> const glyphs_type& - { - return glyphs_; - } - - auto Font::fallback_glyph() const noexcept -> const glyph_type& - { - return fallback_glyph_; - } - - auto Font::white_pixel_uv() const noexcept -> const uv_point_type& - { - return white_pixel_uv_; - } - - auto Font::baked_line_uv() const noexcept -> const baked_line_uv_type& - { - return baked_line_uv_; - } - - auto Font::texture_id() const noexcept -> texture_id_type - { - return texture_id_; - } - - auto Font::text_size( - const std::basic_string_view utf8_text, - const float font_size, - const float wrap_width, - std::basic_string& out_text - ) const noexcept -> extent_type - { - // todo - auto utf16_text = chars::convert(utf8_text); - static_assert(std::is_same_v); - - const auto line_height = font_size; - const auto scale = line_height / static_cast(pixel_height_); - const auto& glyphs = glyphs_; - const auto& fallback_glyph = fallback_glyph_; - - float max_width = 0; - float current_width = 0; - float total_height = line_height; - - auto it_input_current = utf16_text.begin(); - const auto it_input_end = utf16_text.end(); - - while (it_input_current != it_input_end) - { - const auto this_char = *it_input_current; - it_input_current += 1; - - if (this_char == u'\n') - { - max_width = std::ranges::max(max_width, current_width); - current_width = 0; - total_height += line_height; - } - else - { - const auto& [glyph_rect, glyph_uv, glyph_advance_x] = [&glyphs, &fallback_glyph](const auto c) -> const auto& { - if (const auto it = glyphs.find(c); - it != glyphs.end()) - { - return it->second; - } - - return fallback_glyph; - }(this_char); - - if (const auto advance_x = glyph_advance_x * scale; - current_width + advance_x > wrap_width) - { - max_width = std::ranges::max(max_width, current_width); - current_width = advance_x; - total_height += line_height; - } - else - { - current_width += advance_x; - } - } - } - - max_width = std::ranges::max(max_width, current_width); - out_text = std::move(utf16_text); - - return {max_width, total_height}; - } - - auto Font::text_size( - const std::basic_string_view utf8_text, - const float font_size, - const float wrap_width - ) const noexcept -> extent_type - { - std::basic_string out; - return text_size(utf8_text, font_size, wrap_width, out); - } - - auto Font::text_draw( - const std::basic_string_view utf8_text, - const float font_size, - const float wrap_width, - const point_type point, - const color_type color, - const DrawListDef::Accessor accessor - ) const noexcept -> void - { - // todo - auto utf16_text = chars::convert(utf8_text); - static_assert(std::is_same_v); - - const auto newline_count = std::ranges::count(utf16_text, u'\n'); - const auto text_size_exclude_newline = utf16_text.size() - newline_count; - - const auto vertex_count = 4 * text_size_exclude_newline; - const auto index_count = 6 * text_size_exclude_newline; - accessor.reserve(vertex_count, index_count); - - const auto line_height = font_size; - const auto scale = line_height / static_cast(pixel_height_); - const auto& glyphs = glyphs_; - const auto& fallback_glyph = fallback_glyph_; - - auto cursor = point + point_type{0, line_height}; - - auto it_input_current = utf16_text.begin(); - const auto it_input_end = utf16_text.end(); - - while (it_input_current != it_input_end) - { - const auto this_char = *it_input_current; - it_input_current += 1; - - if (this_char == u'\n') - { - cursor.x = point.x; - cursor.y += line_height; - continue; - } - - const auto& [glyph_rect, glyph_uv, glyph_advance_x] = [&glyphs, &fallback_glyph](const auto c) -> const auto& { - if (const auto it = glyphs.find(c); - it != glyphs.end()) - { - return it->second; - } - - return fallback_glyph; - }(this_char); - - const auto advance_x = glyph_advance_x * scale; - if (cursor.x + advance_x > point.x + wrap_width) - { - cursor.x = point.x; - cursor.y += line_height; - } - - const rect_type char_rect - { - cursor + point_type{glyph_rect.left_top().x, -glyph_rect.left_top().y} * scale, - glyph_rect.size() * scale - }; - cursor.x += advance_x; - - const auto current_vertex_index = static_cast(accessor.size()); - - accessor.add_vertex(char_rect.left_top(), glyph_uv.left_top(), color); - accessor.add_vertex(char_rect.right_top(), glyph_uv.right_top(), color); - accessor.add_vertex(char_rect.right_bottom(), glyph_uv.right_bottom(), color); - accessor.add_vertex(char_rect.left_bottom(), glyph_uv.left_bottom(), color); - - accessor.add_index(current_vertex_index + 0, current_vertex_index + 1, current_vertex_index + 2); - accessor.add_index(current_vertex_index + 0, current_vertex_index + 2, current_vertex_index + 3); - } - } -} diff --git a/src/draw/font.hpp b/src/draw/font.hpp deleted file mode 100644 index 6eece7a8..00000000 --- a/src/draw/font.hpp +++ /dev/null @@ -1,252 +0,0 @@ -// This file is part of prometheus -// Copyright (C) 2022-2025 Life4gal -// This file is subject to the license terms in the LICENSE file -// found in the top-level directory of this distribution. - -#pragma once - -#include -#include -#include - -#include - -#include - -namespace gal::prometheus::draw -{ - // ReSharper disable once CppClassCanBeFinal - class Font - { - public: - using rect_type = DrawListDef::rect_type; - using point_type = DrawListDef::point_type; - using extent_type = DrawListDef::extent_type; - - using color_type = DrawListDef::color_type; - using index_type = DrawListDef::index_type; - - using texture_id_type = DrawListDef::texture_id_type; - constexpr static texture_id_type invalid_texture_id = 0; - - using uv_rect_type = primitive::basic_rect_2d; - using uv_point_type = uv_rect_type::point_type; - using uv_extent_type = uv_rect_type::extent_type; - - // todo: char32_t ? - using char_type = char16_t; - - #if defined(GAL_PROMETHEUS_COMPILER_MSVC) - GAL_PROMETHEUS_COMPILER_DISABLE_WARNING_PUSH - // Variable '%1$s' is uninitialized. Always initialize a member variable (type.6). - GAL_PROMETHEUS_COMPILER_DISABLE_WARNING(26495) - #endif - - struct glyph_type - { - rect_type rect; - uv_rect_type uv; - float advance_x; - }; - - #if defined(GAL_PROMETHEUS_COMPILER_MSVC) - GAL_PROMETHEUS_COMPILER_DISABLE_WARNING_POP - #endif - - using glyphs_type = std::unordered_map; - - using glyph_value_type = i18n::RangeBuilder::value_type; - using glyph_ranges_type = i18n::RangeBuilder::ranges_type; - - using baked_line_uv_type = std::vector; - - // todo - constexpr static std::uint32_t default_baked_line_max_width = 63; - - class Option - { - friend Font; - - std::string font_path_; - glyph_ranges_type glyph_ranges_; - - std::uint32_t pixel_height_; - std::uint32_t baked_line_max_width_; - - Option() noexcept - : pixel_height_{18}, - baked_line_max_width_{default_baked_line_max_width} {} - - public: - constexpr auto path(const std::string_view path) noexcept -> Option& - { - font_path_ = path; - - return *this; - } - - constexpr auto glyph_ranges(glyph_ranges_type&& ranges) noexcept -> Option& - { - glyph_ranges_ = std::move(ranges); - - return *this; - } - - constexpr auto pixel_height(const std::uint32_t height) noexcept -> Option& - { - pixel_height_ = height; - - return *this; - } - - constexpr auto baked_line_max_width(const std::uint32_t width) noexcept -> Option& - { - baked_line_max_width_ = width; - - return *this; - } - }; - - class Texture final - { - friend Font; - - public: - using size_type = std::uint32_t; - // size.width * size.height (RGBA) - using data_type = std::unique_ptr; - - private: - size_type width_; - size_type height_; - data_type data_; - std::reference_wrapper id_; - - explicit Texture(texture_id_type& id) noexcept - : width_{0}, - height_{0}, - data_{nullptr}, - id_{id} {} - - constexpr auto set_size(const size_type width, const size_type height) noexcept -> void - { - width_ = width; - height_ = height; - } - - constexpr auto set_data(data_type&& data) noexcept -> void - { - data_ = std::move(data); - } - - public: - Texture(const Texture& other) = delete; - Texture(Texture&& other) noexcept = default; - auto operator=(const Texture& other) -> Texture& = delete; - auto operator=(Texture&& other) noexcept -> Texture& = default; - - ~Texture() noexcept; - - [[nodiscard]] constexpr auto valid() const noexcept -> bool - { - return data_ != nullptr; - } - - [[nodiscard]] constexpr auto width() const noexcept -> size_type - { - return width_; - } - - [[nodiscard]] constexpr auto height() const noexcept -> size_type - { - return height_; - } - - [[nodiscard]] constexpr auto data() const & noexcept -> const data_type& - { - return data_; - } - - [[nodiscard]] constexpr auto data() && noexcept -> data_type&& - { - return std::move(data_); - } - - auto bind(texture_id_type id) const noexcept -> void; - }; - - private: - std::string font_path_; - std::uint32_t pixel_height_; - std::uint32_t baked_line_max_width_; - - glyphs_type glyphs_; - glyph_type fallback_glyph_; - - uv_point_type white_pixel_uv_; - baked_line_uv_type baked_line_uv_; - - texture_id_type texture_id_; - - auto reset() noexcept -> void; - - public: - explicit Font() noexcept; - - Font(const Font& other) = delete; - Font(Font&& other) noexcept = default; - auto operator=(const Font& other) -> Font& = delete; - auto operator=(Font&& other) noexcept -> Font& = default; - - virtual ~Font() noexcept; - - [[nodiscard]] static auto option() noexcept -> Option; - - [[nodiscard]] auto load(const Option& option) noexcept -> Texture; - - // --------------------------------------------------------- - - [[nodiscard]] auto loaded() const noexcept -> bool; - - [[nodiscard]] auto font_path() const noexcept -> std::string_view; - - [[nodiscard]] auto pixel_height() const noexcept -> std::uint32_t; - - [[nodiscard]] auto baked_line_max_width() const noexcept -> std::uint32_t; - - [[nodiscard]] auto glyphs() const noexcept -> const glyphs_type&; - - [[nodiscard]] auto fallback_glyph() const noexcept -> const glyph_type&; - - [[nodiscard]] auto white_pixel_uv() const noexcept -> const uv_point_type&; - - [[nodiscard]] auto baked_line_uv() const noexcept -> const baked_line_uv_type&; - - [[nodiscard]] auto texture_id() const noexcept -> texture_id_type; - - // ========================================= - // DRAW TEXT - - [[nodiscard]] virtual auto text_size( - std::basic_string_view utf8_text, - float font_size, - float wrap_width, - std::basic_string& out_text - ) const noexcept -> extent_type; - - [[nodiscard]] virtual auto text_size( - std::basic_string_view utf8_text, - float font_size, - float wrap_width - ) const noexcept -> extent_type; - - virtual auto text_draw( - std::basic_string_view utf8_text, - float font_size, - float wrap_width, - point_type point, - color_type color, - DrawListDef::Accessor accessor - ) const noexcept -> void; - }; -} diff --git a/src/draw/mouse.cpp b/src/draw/mouse.cpp deleted file mode 100644 index 6e3f7b19..00000000 --- a/src/draw/mouse.cpp +++ /dev/null @@ -1,97 +0,0 @@ -// This file is part of prometheus -// Copyright (C) 2022-2025 Life4gal -// This file is subject to the license terms in the LICENSE file -// found in the top-level directory of this distribution. - -#include - -namespace gal::prometheus::draw -{ - Mouse::Mouse(const time_type double_click_interval_threshold, const value_type double_click_distance_threshold) noexcept - : double_click_interval_threshold_{double_click_interval_threshold}, - double_click_distance_threshold_{double_click_distance_threshold}, - position_current_{std::numeric_limits::min(), std::numeric_limits::min()}, - position_previous_{std::numeric_limits::min(), std::numeric_limits::min()}, - position_clicked_{std::numeric_limits::min(), std::numeric_limits::min()}, - down_{false}, - clicked_{false}, - double_clicked_{false}, - pad_{false}, - down_duration_{0}, - click_duration_{0} - { - std::ignore = pad_; - } - - auto Mouse::position() const noexcept -> point_type - { - return position_current_; - } - - auto Mouse::position_delta() const noexcept -> extent_type - { - const auto delta = position_current_ - position_previous_; - - return delta.to(); - } - - auto Mouse::down() const noexcept -> bool - { - return down_; - } - - auto Mouse::clicked() const noexcept -> bool - { - return clicked_; - } - - auto Mouse::double_clicked() const noexcept -> bool - { - return double_clicked_; - } - - auto Mouse::move(const point_type position) noexcept -> void - { - position_current_ = position; - } - - auto Mouse::tick(const time_type tick_time) noexcept -> void - { - position_previous_ = position_current_; - - clicked_ = false; - double_clicked_ = false; - if (down_) - { - if (down_duration_ > 0) - { - down_duration_ += tick_time; - } - else - { - down_duration_ = 0; - clicked_ = true; - } - } - else - { - down_duration_ = std::numeric_limits::min(); - } - if (clicked_) - { - if (0 - click_duration_ < double_click_interval_threshold_) - { - if (position_current_.distance(position_clicked_) < double_click_distance_threshold_) - { - double_clicked_ = true; - } - click_duration_ = std::numeric_limits::min(); - } - else - { - click_duration_ = 0; - position_clicked_ = position_current_; - } - } - } -} diff --git a/src/draw/mouse.hpp b/src/draw/mouse.hpp deleted file mode 100644 index be82a3eb..00000000 --- a/src/draw/mouse.hpp +++ /dev/null @@ -1,69 +0,0 @@ -// This file is part of prometheus -// Copyright (C) 2022-2025 Life4gal -// This file is subject to the license terms in the LICENSE file -// found in the top-level directory of this distribution. - -#pragma once - -#include - -namespace gal::prometheus::draw -{ - class Context; - - class Mouse final - { - friend Context; - - public: - using rect_type = DrawListDef::rect_type; - using point_type = DrawListDef::point_type; - using extent_type = DrawListDef::extent_type; - - using value_type = point_type::value_type; - - using time_type = float; - - private: - // ================================== - // static - // ================================== - - time_type double_click_interval_threshold_; - value_type double_click_distance_threshold_; - - // ================================== - // dynamic - // ================================== - - point_type position_current_; - point_type position_previous_; - point_type position_clicked_; - - bool down_; - bool clicked_; - bool double_clicked_; - bool pad_; - - time_type down_duration_; - time_type click_duration_; - - Mouse(time_type double_click_interval_threshold, value_type double_click_distance_threshold) noexcept; - - public: - [[nodiscard]] auto position() const noexcept -> point_type; - - [[nodiscard]] auto position_delta() const noexcept -> extent_type; - - [[nodiscard]] auto down() const noexcept -> bool; - - [[nodiscard]] auto clicked() const noexcept -> bool; - - [[nodiscard]] auto double_clicked() const noexcept -> bool; - - private: - auto move(point_type position) noexcept -> void; - - auto tick(time_type tick_time) noexcept -> void; - }; -} diff --git a/src/draw/shared_data.cpp b/src/draw/shared_data.cpp deleted file mode 100644 index cd7bb84a..00000000 --- a/src/draw/shared_data.cpp +++ /dev/null @@ -1,165 +0,0 @@ -// This file is part of prometheus -// Copyright (C) 2022-2025 Life4gal -// This file is subject to the license terms in the LICENSE file -// found in the top-level directory of this distribution. - -#include - -#include -#include - -#include - -#include - -namespace -{ - using namespace gal::prometheus; - using namespace draw; - - constexpr auto circle_segments_min = DrawListSharedData::circle_segments_min; - constexpr auto circle_segments_max = DrawListSharedData::circle_segments_max; - - // @see https://stackoverflow.com/a/2244088/15194693 - // Number of segments (N) is calculated using equation: - // N = ceil ( pi / acos(1 - error / r) ) where r > 0 and error <= r - [[nodiscard]] constexpr auto circle_segments_calc(const float radius, const float max_error) noexcept -> auto - { - constexpr auto circle_segments_roundup_to_even = [](const auto v) noexcept -> auto - { - return (v + 1) / 2 * 2; - }; - - return std::ranges::clamp( - circle_segments_roundup_to_even(static_cast(std::ceil(std::numbers::pi_v / std::acos(1 - std::ranges::min(radius, max_error) / radius)))), - circle_segments_min, - circle_segments_max - ); - } - - [[nodiscard]] constexpr auto circle_segments_calc_radius(const std::size_t n, const float max_error) noexcept -> auto - { - return max_error / (1 - math::cos(std::numbers::pi_v / std::ranges::max(static_cast(n), std::numbers::pi_v))); - } - - [[nodiscard]] constexpr auto circle_segments_calc_error(const std::size_t n, const float radius) noexcept -> auto - { - return (1 - math::cos(std::numbers::pi_v / std::ranges::max(static_cast(n), std::numbers::pi_v))) / radius; - } - - template - [[nodiscard]] constexpr auto vertex_sample_points_calc() noexcept -> DrawListSharedData::vertex_sample_points_type - { - return [](std::index_sequence) noexcept -> DrawListSharedData::vertex_sample_points_type - { - constexpr auto make_point = []() noexcept -> DrawListSharedData::point_type - { - const auto a = static_cast(I) / static_cast(N) * 2 * std::numbers::pi_v; - return {math::cos(a), -math::sin(a)}; - }; - - return {{make_point.template operator()()...}}; - }(std::make_index_sequence{}); - } -} - -namespace gal::prometheus::draw -{ - [[nodiscard]] auto range_of_arc_quadrant(const DrawArcFlag quadrant) noexcept -> std::pair - { - static_assert(DrawListSharedData::vertex_sample_points_count % 12 == 0); - constexpr auto factor = static_cast(DrawListSharedData::vertex_sample_points_count / 12); - - switch (quadrant) - { - case DrawArcFlag::Q1: { return std::make_pair(0 * factor, 3 * factor); } - case DrawArcFlag::Q2: { return std::make_pair(3 * factor, 6 * factor); } - case DrawArcFlag::Q3: { return std::make_pair(6 * factor, 9 * factor); } - case DrawArcFlag::Q4: { return std::make_pair(9 * factor, 12 * factor); } - case DrawArcFlag::TOP: { return std::make_pair(0 * factor, 6 * factor); } - case DrawArcFlag::BOTTOM: { return std::make_pair(6 * factor, 12 * factor); } - case DrawArcFlag::LEFT: { return std::make_pair(3 * factor, 9 * factor); } - case DrawArcFlag::RIGHT: { return std::make_pair(9 * factor, 15 * factor); } - case DrawArcFlag::ALL: { return std::make_pair(0 * factor, 12 * factor); } - case DrawArcFlag::Q1_CLOCK_WISH: { return std::make_pair(3 * factor, 0 * factor); } - case DrawArcFlag::Q2_CLOCK_WISH: { return std::make_pair(6 * factor, 3 * factor); } - case DrawArcFlag::Q3_CLOCK_WISH: { return std::make_pair(9 * factor, 6 * factor); } - case DrawArcFlag::Q4_CLOCK_WISH: { return std::make_pair(12 * factor, 9 * factor); } - case DrawArcFlag::TOP_CLOCK_WISH: { return std::make_pair(6 * factor, 0 * factor); } - case DrawArcFlag::BOTTOM_CLOCK_WISH: { return std::make_pair(12 * factor, 6 * factor); } - case DrawArcFlag::LEFT_CLOCK_WISH: { return std::make_pair(9 * factor, 3 * factor); } - case DrawArcFlag::RIGHT_CLOCK_WISH: { return std::make_pair(15 * factor, 9 * factor); } - case DrawArcFlag::ALL_CLOCK_WISH: { return std::make_pair(12 * factor, 0 * factor); } - default: { GAL_PROMETHEUS_ERROR_UNREACHABLE(); } - } - } - - DrawListSharedData::DrawListSharedData() noexcept - : - circle_segment_counts_{}, - vertex_sample_points_{vertex_sample_points_calc()}, - circle_segment_max_error_{}, - arc_fast_radius_cutoff_{}, - curve_tessellation_tolerance_{1.25f} - { - set_circle_tessellation_max_error(.3f); - } - - auto DrawListSharedData::get_circle_auto_segment_count(const float radius) const noexcept -> circle_segment_count_type - { - // ceil to never reduce accuracy - if (const auto radius_index = static_cast(radius + .999999f); - radius_index < circle_segment_counts_.size()) - { - return circle_segment_counts_[radius_index]; - } - return static_cast(circle_segments_calc(radius, circle_segment_max_error_)); - } - - auto DrawListSharedData::get_vertex_sample_point(const std::size_t index) const noexcept -> const point_type& - { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(index < vertex_sample_points_.size()); - - return vertex_sample_points_[index]; - } - - auto DrawListSharedData::get_circle_tessellation_max_error() const noexcept -> float - { - return circle_segment_max_error_; - } - - auto DrawListSharedData::get_arc_fast_radius_cutoff() const noexcept -> float - { - return arc_fast_radius_cutoff_; - } - - auto DrawListSharedData::get_curve_tessellation_tolerance() const noexcept -> float - { - return curve_tessellation_tolerance_; - } - - auto DrawListSharedData::set_circle_tessellation_max_error(const float max_error) noexcept -> void - { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(max_error > .0f); - - if (circle_segment_max_error_ == max_error) // NOLINT(clang-diagnostic-float-equal) - { - return; - } - - for (decltype(circle_segment_counts_.size()) i = 0; i < circle_segment_counts_.size(); ++i) - { - const auto radius = static_cast(i); - circle_segment_counts_[i] = static_cast(circle_segments_calc(radius, max_error)); - } - circle_segment_max_error_ = max_error; - arc_fast_radius_cutoff_ = circle_segments_calc_radius(vertex_sample_points_count, max_error); - } - - auto DrawListSharedData::set_curve_tessellation_tolerance(const float tolerance) noexcept -> void - { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(tolerance > .0f); - - curve_tessellation_tolerance_ = tolerance; - } -} diff --git a/src/draw/shared_data.hpp b/src/draw/shared_data.hpp deleted file mode 100644 index 8fbd8668..00000000 --- a/src/draw/shared_data.hpp +++ /dev/null @@ -1,63 +0,0 @@ -// This file is part of prometheus -// Copyright (C) 2022-2025 Life4gal -// This file is subject to the license terms in the LICENSE file -// found in the top-level directory of this distribution. - -#pragma once - -#include - -namespace gal::prometheus::draw -{ - class DrawListSharedData final - { - public: - using rect_type = DrawListDef::rect_type; - using point_type = DrawListDef::point_type; - using extent_type = DrawListDef::extent_type; - - using circle_segment_count_type = std::uint8_t; - constexpr static std::size_t circle_segment_counts_count = 64; - using circle_segment_counts_type = std::array; - - constexpr static std::uint32_t circle_segments_min = 4; - constexpr static std::uint32_t circle_segments_max = 512; - - constexpr static std::size_t vertex_sample_points_count = 48; - using vertex_sample_points_type = std::array; - - private: - circle_segment_counts_type circle_segment_counts_; - vertex_sample_points_type vertex_sample_points_; - - // Maximum error (in pixels) allowed when using `circle`/`circle_filled` or drawing rounded corner rectangles with no explicit segment count specified. - // Decrease for higher quality but more geometry. - float circle_segment_max_error_; - // Cutoff radius after which arc drawing will fall back to slower `path_arc` - float arc_fast_radius_cutoff_; - // Tessellation tolerance when using `path_bezier_curve` without a specific number of segments. - // Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. - float curve_tessellation_tolerance_; - - public: - DrawListSharedData() noexcept; - - // -------------------------------------------------- - - [[nodiscard]] auto get_circle_auto_segment_count(float radius) const noexcept -> circle_segment_count_type; - - [[nodiscard]] auto get_vertex_sample_point(std::size_t index) const noexcept -> const point_type&; - - [[nodiscard]] auto get_circle_tessellation_max_error() const noexcept -> float; - - [[nodiscard]] auto get_arc_fast_radius_cutoff() const noexcept -> float; - - [[nodiscard]] auto get_curve_tessellation_tolerance() const noexcept -> float; - - // -------------------------------------------------- - - auto set_circle_tessellation_max_error(float max_error) noexcept -> void; - - auto set_curve_tessellation_tolerance(float tolerance) noexcept -> void; - }; -} diff --git a/src/draw/theme.cpp b/src/draw/theme.cpp deleted file mode 100644 index ccd36c03..00000000 --- a/src/draw/theme.cpp +++ /dev/null @@ -1,126 +0,0 @@ -// This file is part of prometheus -// Copyright (C) 2022-2025 Life4gal -// This file is subject to the license terms in the LICENSE file -// found in the top-level directory of this distribution. -#include - -namespace -{ - using namespace gal::prometheus; - using colors_type = draw::Theme::colors_type; - - [[nodiscard]] constexpr auto default_colors() noexcept -> colors_type - { - using draw::ThemeCategory; - - colors_type colors{}; - - colors[static_cast(ThemeCategory::TEXT)] = primitive::colors::black; - colors[static_cast(ThemeCategory::BORDER)] = primitive::colors::magenta; - - colors[static_cast(ThemeCategory::WINDOW_BACKGROUND)] = primitive::colors::gains_boro; - - colors[static_cast(ThemeCategory::WIDGET_BACKGROUND)] = primitive::colors::white; - colors[static_cast(ThemeCategory::WIDGET_ACTIVATED)] = primitive::colors::dark_salmon; - - colors[static_cast(ThemeCategory::TITLE_BAR)] = primitive::colors::light_coral; - colors[static_cast(ThemeCategory::TITLE_BAR_COLLAPSED)] = primitive::colors::dark_khaki; - - colors[static_cast(ThemeCategory::SLIDER)] = primitive::colors::light_blue; - colors[static_cast(ThemeCategory::SLIDER_ACTIVATED)] = primitive::colors::deep_sky_blue; - - colors[static_cast(ThemeCategory::BUTTON)] = primitive::colors::sienna; - colors[static_cast(ThemeCategory::BUTTON_HOVERED)] = primitive::colors::slate_gray; - colors[static_cast(ThemeCategory::BUTTON_ACTIVATED)] = primitive::colors::steel_blue; - - colors[static_cast(ThemeCategory::RESIZE_GRIP)] = primitive::colors::gold; - colors[static_cast(ThemeCategory::RESIZE_GRIP_HOVERED)] = primitive::colors::peru; - colors[static_cast(ThemeCategory::RESIZE_GRIP_ACTIVATED)] = primitive::colors::powder_blue; - - colors[static_cast(ThemeCategory::TOOLTIP_BACKGROUND)] = primitive::colors::black; - colors[static_cast(ThemeCategory::TOOLTIP_TEXT)] = primitive::colors::red; - - return colors; - } - - [[nodiscard]] constexpr auto another_colors_for_test() noexcept -> colors_type - { - using draw::ThemeCategory; - - colors_type colors{}; - - colors[static_cast(ThemeCategory::TEXT)] = primitive::colors::black; - colors[static_cast(ThemeCategory::BORDER)] = primitive::colors::magenta; - - colors[static_cast(ThemeCategory::WINDOW_BACKGROUND)] = primitive::colors::pink; - - colors[static_cast(ThemeCategory::WIDGET_BACKGROUND)] = primitive::colors::white; - colors[static_cast(ThemeCategory::WIDGET_ACTIVATED)] = primitive::colors::dark_salmon; - - colors[static_cast(ThemeCategory::TITLE_BAR)] = primitive::colors::light_coral; - colors[static_cast(ThemeCategory::TITLE_BAR_COLLAPSED)] = primitive::colors::dark_khaki; - - colors[static_cast(ThemeCategory::SLIDER)] = primitive::colors::light_blue; - colors[static_cast(ThemeCategory::SLIDER_ACTIVATED)] = primitive::colors::deep_sky_blue; - - colors[static_cast(ThemeCategory::BUTTON)] = primitive::colors::sienna; - colors[static_cast(ThemeCategory::BUTTON_HOVERED)] = primitive::colors::slate_gray; - colors[static_cast(ThemeCategory::BUTTON_ACTIVATED)] = primitive::colors::steel_blue; - - colors[static_cast(ThemeCategory::RESIZE_GRIP)] = primitive::colors::red; - colors[static_cast(ThemeCategory::RESIZE_GRIP_HOVERED)] = primitive::colors::yellow; - colors[static_cast(ThemeCategory::RESIZE_GRIP_ACTIVATED)] = primitive::colors::blue; - - colors[static_cast(ThemeCategory::TOOLTIP_BACKGROUND)] = primitive::colors::black; - colors[static_cast(ThemeCategory::TOOLTIP_TEXT)] = primitive::colors::blue; - - return colors; - } -} - -namespace gal::prometheus::draw -{ - auto Theme::default_theme() noexcept -> Theme - { - return - { - #if defined(GAL_PROMETHEUS_PLATFORM_WINDOWS) - .font_path = R"(C:\Windows\Fonts\msyh.ttc)", - .font_size = 18, - #else - #error "fixme" - #endif - .title_bar_height = 20, - .window_rounding = 0, - .window_padding = {8, 8}, - .window_min_size = {640, 480}, - .resize_grip_size = {20, 20}, - .frame_padding = {4, 4}, - .item_spacing = {10, 5}, - .item_inner_spacing = {5, 5}, - .colors = default_colors() - }; - } - - auto Theme::another_theme_for_test() noexcept -> Theme - { - return - { - #if defined(GAL_PROMETHEUS_PLATFORM_WINDOWS) - .font_path = R"(C:\Windows\Fonts\msyh.ttc)", - .font_size = 24, - #else - #error "fixme" - #endif - .title_bar_height = 40, - .window_rounding = 5, - .window_padding = {15, 15}, - .window_min_size = {1280, 960}, - .resize_grip_size = {40, 40}, - .frame_padding = {8, 8}, - .item_spacing = {20, 10}, - .item_inner_spacing = {10, 10}, - .colors = another_colors_for_test() - }; - } -} diff --git a/src/draw/theme.hpp b/src/draw/theme.hpp deleted file mode 100644 index e3a71bb5..00000000 --- a/src/draw/theme.hpp +++ /dev/null @@ -1,63 +0,0 @@ -// This file is part of prometheus -// Copyright (C) 2022-2025 Life4gal -// This file is subject to the license terms in the LICENSE file -// found in the top-level directory of this distribution. - -#pragma once - -#include -#include - -namespace gal::prometheus::draw -{ - class [[nodiscard]] Theme final - { - public: - using rect_type = DrawListDef::rect_type; - using point_type = DrawListDef::point_type; - using extent_type = DrawListDef::extent_type; - - using value_type = point_type::value_type; - - using color_type = DrawListDef::color_type; - using colors_type = std::array; - - // ----------------------------------------------- - // FONT - - std::string font_path; - value_type font_size; - - // ----------------------------------------------- - // WINDOW - - value_type title_bar_height; - value_type window_rounding; - extent_type window_padding; - extent_type window_min_size; - extent_type resize_grip_size; - - // ----------------------------------------------- - // WINDOW CANVAS LAYOUT - - extent_type frame_padding; - extent_type item_spacing; - extent_type item_inner_spacing; - - // ----------------------------------------------- - // WINDOW WIDGET COLOR - - colors_type colors; - - template - requires (std::to_underlying(Category) < theme_category_count) - [[nodiscard]] constexpr auto color() const noexcept -> color_type - { - return colors[static_cast(Category)]; - } - - [[nodiscard]] static auto default_theme() noexcept -> Theme; - - [[nodiscard]] static auto another_theme_for_test() noexcept -> Theme; - }; -} diff --git a/src/draw/window.cpp b/src/draw/window.cpp deleted file mode 100644 index 8bfdb794..00000000 --- a/src/draw/window.cpp +++ /dev/null @@ -1,648 +0,0 @@ -// This file is part of prometheus -// Copyright (C) 2022-2025 Life4gal -// This file is subject to the license terms in the LICENSE file -// found in the top-level directory of this distribution. -#include - -#include - -namespace -{ - using namespace gal::prometheus; - - constexpr std::string_view window_widget_name_move{"@WINDOW::MOVE@"}; - constexpr std::string_view window_widget_name_close{"@WINDOW::CLOSE@"}; - constexpr std::string_view window_widget_name_resize{"@WINDOW::RESIZE@"}; -} - -namespace gal::prometheus::draw -{ - auto Window::get_id(const std::string_view name) const noexcept -> id_type - { - return functional::hash_combine_2(id_, functional::hash(name)); - } - - auto Window::rect_of_title_bar() const noexcept -> rect_type - { - const auto& context = Context::instance(); - const auto& theme = context.theme(); - - const auto point = rect_.left_top(); - const auto size = extent_type{rect_.width(), theme.title_bar_height}; - return {point, size}; - } - - auto Window::rect_of_close_button() const noexcept -> rect_type - { - const auto& context = Context::instance(); - const auto& theme = context.theme(); - - // RIGHT-TOP-CORNER - const auto point = rect_.right_top() - extent_type{theme.title_bar_height, 0}; - const auto size = extent_type{theme.title_bar_height, theme.title_bar_height}; - return {point, size}; - } - - auto Window::rect_of_resize_grip() const noexcept -> rect_type - { - const auto& context = Context::instance(); - const auto& theme = context.theme(); - - // RIGHT-BOTTOM-CORNER - const auto point = rect_.right_bottom() - theme.resize_grip_size; - const auto size = theme.resize_grip_size; - return {point, size}; - } - - auto Window::rect_of_canvas() const noexcept -> rect_type - { - // const auto& context = Context::instance(); - // const auto& theme = context.theme(); - - return rect_; - } - - auto Window::make_canvas() noexcept -> bool - { - auto& window = *this; - - auto& canvas = window.canvas_; - auto& draw_list = window.draw_list_; - const auto flag_value = std::to_underlying(window.flag_); - - auto& context = Context::instance(); - const auto& theme = context.theme(); - const auto& mouse = context.mouse(); - - // ------------------------------ - // TEST TITLE BAR - auto close_button_color = theme.color(); - auto close_button_pressed = false; - { - if (flag_value & std::to_underlying(WindowFlag::NO_TITLE_BAR)) - { - window.collapse_ = false; - } - else if (mouse.double_clicked() and window.rect_of_title_bar().includes(mouse.position())) - { - window.collapse_ = not window.collapse_; - } - - if (flag_value & std::to_underlying(WindowFlag::NO_CLOSE)) - { - // - } - else - { - const auto id = get_id(window_widget_name_close); - const auto close_button_rect = window.rect_of_close_button(); - - const auto status = context.test_widget_status( - id, - close_button_rect, - false - #if defined(GAL_PROMETHEUS_DRAW_CONTEXT_DEBUG) - , - std::format("Test Window({})'s close-button({}).", window.name_, close_button_rect) - #endif - ); - - if (status.hovered) - { - if (status.keeping) - { - close_button_color = theme.color(); - } - else - { - close_button_color = theme.color(); - } - } - - if (status.pressed) - { - close_button_pressed = true; - } - } - - if (flag_value & std::to_underlying(WindowFlag::NO_MOVE)) - { - // - } - else - { - if (const auto id = get_id(window_widget_name_move); - context.is_widget_activated(id)) - { - if (mouse.down()) - { - window.rect_.point += mouse.position_delta(); - } - else - { - context.invalidate_widget_activated( - #if defined(GAL_PROMETHEUS_DRAW_CONTEXT_DEBUG) - std::format("{} is not a movable window.", window.name_) - #endif - ); - } - } - } - } - - // ------------------------------ - // TEST RESIZE GRIP - auto resize_grip_color = theme.color(); - { - if (flag_value & std::to_underlying(WindowFlag::NO_RESIZE)) - { - // - } - else - { - if (not window.collapse_) - { - const auto id = get_id(window_widget_name_resize); - const auto resize_grip_rect = window.rect_of_resize_grip(); - - const auto status = context.test_widget_status( - id, - resize_grip_rect, - false - #if defined(GAL_PROMETHEUS_DRAW_CONTEXT_DEBUG) - , - std::format("Test Window({})'s resize-grip({}).", window.name_, resize_grip_rect) - #endif - ); - - if (status.keeping) - { - const auto target_size = window.rect_.size() + mouse.position_delta(); - const auto min_size = theme.window_min_size; - window.rect_.extent = {std::ranges::max(target_size.width, min_size.width), std::ranges::max(target_size.height, min_size.height)}; - resize_grip_color = theme.color(); - } - else if (status.hovered) - { - resize_grip_color = theme.color(); - } - } - } - } - - // ------------------------------ - // INIT CANVAS - { - if (flag_value & std::to_underlying(WindowFlag::NO_TITLE_BAR)) - { - canvas.cursor_start_line = (theme.window_padding + extent_type{0, 0}).to(); - } - else - { - canvas.cursor_start_line = (theme.window_padding + extent_type{0, theme.title_bar_height}).to(); - } - canvas.cursor_current_line = canvas.cursor_start_line; - canvas.cursor_previous_line = canvas.cursor_current_line; - canvas.height_current_line = 0; - canvas.height_previous_line = 0; - canvas.item_width.resize(0); - canvas.item_width.push_back(window.default_item_width_); - } - - // ------------------------------ - // DRAW CANVAS BACKGROUND - { - const auto canvas_rect = window.rect_of_canvas(); - - if (window.collapse_) - { - // - } - else - { - draw_list.rect_filled(canvas_rect, theme.color(), theme.window_rounding, DrawFlag::ROUND_CORNER_ALL); - if (flag_value & std::to_underlying(WindowFlag::BORDERED)) - { - draw_list.rect(canvas_rect, theme.color(), theme.window_rounding, DrawFlag::ROUND_CORNER_ALL); - } - } - } - - // ------------------------------ - // DRAW TITTLE BAR - { - const auto title_bar_rect = window.rect_of_title_bar(); - - if (window.collapse_) - { - draw_list.rect_filled(title_bar_rect, theme.color(), theme.window_rounding, DrawFlag::ROUND_CORNER_ALL); - - if (flag_value & std::to_underlying(WindowFlag::BORDERED)) - { - draw_list.rect(title_bar_rect, theme.color(), theme.window_rounding, DrawFlag::ROUND_CORNER_ALL); - } - } - else - { - draw_list.rect_filled(title_bar_rect, theme.color(), theme.window_rounding, DrawFlag::ROUND_CORNER_TOP); - } - - // todo: position - const auto text_point = title_bar_rect.left_top() + extent_type{theme.item_inner_spacing.width, 0}; - draw_list.text( - theme.font_size, - text_point, - theme.color(), - window.name_ - ); - - const auto close_button_rect = window.rect_of_close_button(); - if (flag_value & std::to_underlying(WindowFlag::NO_CLOSE)) - { - // - } - else - { - const auto center = close_button_rect.center(); - const auto radius = close_button_rect.width() / 2; - - const auto r = radius / std::numbers::sqrt2_v; - - const auto line1_from = center + extent_type{-r, -r}; - const auto line1_to = center + extent_type{r, r}; - - const auto line2_from = center + extent_type{-r, r}; - const auto line2_to = center + extent_type{r, -r}; - - draw_list.circle_filled(close_button_rect.center(), close_button_rect.width() / 2, close_button_color); - draw_list.line(line1_from, line1_to, theme.color()); - draw_list.line(line2_from, line2_to, theme.color()); - } - } - - // ------------------------------ - // DRAW RESIZE GRIP - { - const auto resize_grip_rect = window.rect_of_resize_grip(); - - if (window.collapse_ or (flag_value & std::to_underlying(WindowFlag::NO_RESIZE))) - { - // - } - else - { - // todo: rounding? - draw_list.triangle_filled(resize_grip_rect.left_bottom(), resize_grip_rect.right_bottom(), resize_grip_rect.right_top(), resize_grip_color); - } - } - - return close_button_pressed; - } - - auto Window::cursor_abs_position() const noexcept -> point_type - { - auto& window = *this; - - const auto& canvas = window.canvas_; - // auto& draw_list = window.draw_list_; - // const auto flag_value = std::to_underlying(window.flag_); - - // const auto& context = Context::instance(); - // const auto& theme = context.theme(); - // const auto& mouse = context.mouse(); - - return window.rect_.left_top() + canvas.cursor_current_line; - } - - auto Window::cursor_remaining_width() const noexcept -> value_type - { - auto& window = *this; - - const auto& canvas = window.canvas_; - // auto& draw_list = window.draw_list_; - // const auto flag_value = std::to_underlying(window.flag_); - - // const auto& context = Context::instance(); - // const auto& theme = context.theme(); - // const auto& mouse = context.mouse(); - - return window.rect_.width() - canvas.cursor_current_line.x; - } - - auto Window::adjust_item_size(const extent_type& size) noexcept -> void - { - auto& window = *this; - - auto& canvas = window.canvas_; - // auto& draw_list = window.draw_list_; - // const auto flag_value = std::to_underlying(window.flag_); - - const auto& context = Context::instance(); - const auto& theme = context.theme(); - // const auto& mouse = context.mouse(); - - if (window.collapse_) - { - return; - } - - const auto line_height = std::ranges::max(canvas.height_current_line, size.height); - - // Always align ourselves on pixel boundaries - canvas.cursor_previous_line = {canvas.cursor_current_line.x + size.width, canvas.cursor_current_line.y}; - canvas.cursor_current_line = {theme.window_padding.width, canvas.cursor_current_line.y + line_height + theme.item_spacing.height}; - - canvas.height_previous_line = line_height; - canvas.height_current_line = 0; - } - - auto Window::draw_widget_frame(const rect_type& rect, const color_type& color) noexcept -> void - { - auto& window = *this; - - // auto& canvas = window.canvas_; - auto& draw_list = window.draw_list_; - const auto flag_value = std::to_underlying(window.flag_); - - const auto& context = Context::instance(); - // const auto& font = context.font(); - const auto& theme = context.theme(); - // const auto& mouse = context.mouse(); - - draw_list.rect_filled(rect, color); - if (flag_value & std::to_underlying(WindowFlag::BORDERED)) - { - draw_list.rect(rect, theme.color()); - } - } - - Window::Window(const std::string_view name, const WindowFlag flag, const rect_type& rect) noexcept - : name_{name}, - id_{functional::hash(name_)}, - flag_{flag}, - rect_{rect}, - default_item_width_{0}, - visible_{true}, - collapse_{false} {} - - auto Window::make(const std::string_view name, const WindowFlag flag, const rect_type& rect) noexcept -> Window - { - Window window{name, flag, rect}; - window.draw_list_.reset(); - // todo - std::ignore = window.make_canvas(); - - return window; - } - - auto Window::name() const noexcept -> std::string_view - { - return name_; - } - - auto Window::rect() const noexcept -> const rect_type& - { - return rect_; - } - - auto Window::hovered(const point_type mouse) const noexcept -> bool - { - // todo - return rect_.includes(mouse); - } - - auto Window::layout_same_line(const value_type column_width, value_type spacing_width) noexcept -> void - { - auto& window = *this; - - auto& canvas = window.canvas_; - // auto& draw_list = window.draw_list_; - // const auto flag_value = std::to_underlying(window.flag_); - - const auto& context = Context::instance(); - // const auto& font = context.font(); - const auto& theme = context.theme(); - // const auto& mouse = context.mouse(); - - if (window.collapse_) - { - return; - } - - canvas.height_current_line = canvas.height_previous_line; - canvas.cursor_current_line = canvas.cursor_previous_line; - - if (column_width <= 0) - { - if (spacing_width <= 0) - { - spacing_width = theme.item_spacing.width; - } - - canvas.cursor_current_line.x += spacing_width; - } - else - { - spacing_width = std::ranges::max(spacing_width, static_cast(0)); - - canvas.cursor_current_line.x = column_width + spacing_width; - } - } - - auto Window::draw_text(const std::string_view utf8_text) noexcept -> void - { - auto& window = *this; - - // const auto& canvas = window.canvas_; - auto& draw_list = window.draw_list_; - // const auto flag_value = std::to_underlying(window.flag_); - - const auto& context = Context::instance(); - const auto& font = context.font(); - const auto& theme = context.theme(); - // const auto& mouse = context.mouse(); - - if (window.collapse_) - { - return; - } - - const auto text_context_size = font.text_size(utf8_text, theme.font_size, cursor_remaining_width()); - - const auto text_point = cursor_abs_position(); - const auto text_size = text_context_size; - const rect_type text_rect{text_point, text_size}; - adjust_item_size(text_size); - - draw_list.text( - font, - theme.font_size, - text_rect.left_top(), - theme.color(), - utf8_text, - text_rect.width() - ); - } - - auto Window::draw_button(std::string_view utf8_text, extent_type size) noexcept -> bool - { - auto& window = *this; - - // const auto& canvas = window.canvas_; - auto& draw_list = window.draw_list_; - // const auto flag_value = std::to_underlying(window.flag_); - - auto& context = Context::instance(); - const auto& font = context.font(); - const auto& theme = context.theme(); - // const auto& mouse = context.mouse(); - - if (window.collapse_) - { - return false; - } - - const auto text_context_size = font.text_size(utf8_text, theme.font_size, cursor_remaining_width()); - - if (size.width <= 0) - { - size.width = text_context_size.width; - } - if (size.height <= 0) - { - size.height = text_context_size.height; - } - - // todo - const auto text_point = cursor_abs_position() + extent_type{theme.item_inner_spacing.width, theme.frame_padding.height}; - - const auto button_point = cursor_abs_position(); - const auto button_size = size + theme.frame_padding * 2; - const rect_type button_rect{button_point, button_size}; - adjust_item_size(button_size); - - const auto id = get_id(utf8_text); - const auto status = context.test_widget_status( - id, - button_rect, - false - #if defined(GAL_PROMETHEUS_DRAW_CONTEXT_DEBUG) - , - std::format("Test Window({})'s button[{}]({}).", window.name_, utf8_text, button_rect) - #endif - ); - - color_type button_color = theme.color(); - { - if (status.keeping or status.pressed) - { - button_color = theme.color(); - } - else if (status.hovered) - { - button_color = theme.color(); - } - } - draw_widget_frame(button_rect, button_color); - - draw_list.text( - font, - theme.font_size, - text_point, - theme.color(), - utf8_text, - button_rect.width() - ); - - return status.pressed; - } - - auto Window::draw_checkbox(std::string_view utf8_text, bool checked, extent_type size) noexcept -> bool - { - auto& window = *this; - - // const auto& canvas = window.canvas_; - auto& draw_list = window.draw_list_; - // const auto flag_value = std::to_underlying(window.flag_); - - auto& context = Context::instance(); - const auto& font = context.font(); - const auto& theme = context.theme(); - // const auto& mouse = context.mouse(); - - if (window.collapse_) - { - return false; - } - - const auto text_context_size = font.text_size(utf8_text, theme.font_size, cursor_remaining_width()); - - if (size.width <= 0) - { - size.width = text_context_size.width; - } - if (size.height <= 0) - { - size.height = text_context_size.height; - } - - // □ + text - - // □, side length equals string rect height - const auto check_point = cursor_abs_position(); - const auto check_size = extent_type{size.height + theme.frame_padding.height * 2, size.height + theme.frame_padding.height * 2}; - const rect_type check_rect{check_point, check_size}; - adjust_item_size(check_size); - - // □ text - layout_same_line(0, theme.item_inner_spacing.width); - - // text - const auto text_point = cursor_abs_position() + extent_type{0, theme.frame_padding.height}; - const auto text_size = size; - const rect_type text_rect{text_point, text_size}; - adjust_item_size(text_size); - - draw_widget_frame(check_rect, theme.color()); - - const auto id = get_id(utf8_text); - const auto status = context.test_widget_status( - id, - check_rect, - false - #if defined(GAL_PROMETHEUS_DRAW_CONTEXT_DEBUG) - , - std::format("Test Window({})'s checkbox[{}]({}).", window.name_, utf8_text, check_rect) - #endif - ); - - if (status.pressed) - { - checked = not checked; - } - - if (checked) - { - const auto check_fill_point = check_point + theme.item_inner_spacing; - const auto check_fill_size = check_size - theme.item_inner_spacing * 2; - const rect_type check_fill_rect{check_fill_point, check_fill_size}; - draw_list.rect_filled(check_fill_rect, theme.color()); - } - - draw_list.text( - font, - theme.font_size, - text_rect.left_top(), - theme.color(), - utf8_text, - text_rect.width() - ); - - return checked; - } - - auto Window::render() noexcept -> DrawList& - { - return draw_list_; - } -} diff --git a/src/draw/window.hpp b/src/draw/window.hpp deleted file mode 100644 index 07f32223..00000000 --- a/src/draw/window.hpp +++ /dev/null @@ -1,151 +0,0 @@ -// This file is part of prometheus -// Copyright (C) 2022-2025 Life4gal -// This file is subject to the license terms in the LICENSE file -// found in the top-level directory of this distribution. - -#pragma once - -#include - -#include -#include -#include -#include - -namespace gal::prometheus::draw -{ - class [[nodiscard]] Window final - { - public: - template - using container_type = DrawListDef::container_type; - - using rect_type = DrawListDef::rect_type; - using point_type = DrawListDef::point_type; - using extent_type = DrawListDef::extent_type; - - using value_type = point_type::value_type; - - using color_type = DrawListDef::color_type; - - using id_type = functional::hash_result_type; - constexpr static auto invalid_id = std::numeric_limits::max(); - - private: - struct canvas_type - { - point_type cursor_start_line{0}; - point_type cursor_current_line{0}; - point_type cursor_previous_line{0}; - - value_type height_current_line{0}; - value_type height_previous_line{0}; - - container_type item_width{}; - }; - - std::string name_; - id_type id_; - WindowFlag flag_; - rect_type rect_; - - DrawList draw_list_; - - canvas_type canvas_; - - value_type default_item_width_; - - bool visible_; - bool collapse_; - - // ----------------------------------- - // ID - - [[nodiscard]] auto get_id(std::string_view name) const noexcept -> id_type; - - // ----------------------------------- - // CANVAS - - [[nodiscard]] auto rect_of_title_bar() const noexcept -> rect_type; - - [[nodiscard]] auto rect_of_close_button() const noexcept -> rect_type; - - [[nodiscard]] auto rect_of_resize_grip() const noexcept -> rect_type; - - [[nodiscard]] auto rect_of_canvas() const noexcept -> rect_type; - - [[nodiscard]] auto make_canvas() noexcept -> bool; - - // ----------------------------------- - // CANVAS CONTEXT - - [[nodiscard]] auto cursor_abs_position() const noexcept -> point_type; - - [[nodiscard]] auto cursor_remaining_width() const noexcept -> value_type; - - auto adjust_item_size(const extent_type& size) noexcept -> void; - - auto draw_widget_frame(const rect_type& rect, const color_type& color) noexcept -> void; - - // ----------------------------------- - // INITIALIZE - - Window(std::string_view name, WindowFlag flag, const rect_type& rect) noexcept; - - public: - static auto make(std::string_view name, WindowFlag flag, const rect_type& rect) noexcept -> Window; - - // --------------------------------------------- - // INFO - - [[nodiscard]] auto name() const noexcept -> std::string_view; - - [[nodiscard]] auto rect() const noexcept -> const rect_type&; - - // --------------------------------------------- - // STATUS - - [[nodiscard]] auto hovered(point_type mouse) const noexcept -> bool; - - // --------------------------------------------- - // LAYOUT - - auto layout_same_line(value_type column_width = 0, value_type spacing_width = 0) noexcept -> void; - - // --------------------------------------------- - // WIDGET - - auto draw_text(std::string_view utf8_text) noexcept -> void; - - /** - * @return Whether the button is pressed or not. - */ - [[nodiscard]] auto draw_button(std::string_view utf8_text, extent_type size = {0, 0}) noexcept -> bool; - - /** - * @return Whether the checkbox state is toggled (checked to unchecked, or vice versa) - */ - [[nodiscard]] auto draw_checkbox(std::string_view utf8_text, bool checked, extent_type size = {0, 0}) noexcept -> bool; - - // --------------------------------------------- - // RENDER - - auto render() noexcept -> DrawList&; - - // --------------------------------------------- - // for test only - - Window(const std::string_view name, const rect_type& rect) noexcept - : Window{name, WindowFlag::NONE, rect} {} - - auto test_init() noexcept -> void - { - std::ignore = make_canvas(); - } - - [[nodiscard]] auto test_draw_list() noexcept -> DrawList& - { - return draw_list_; - } - }; -} diff --git a/src/functional/enumeration.hpp b/src/functional/enumeration.hpp index 49ff4d23..be3f6472 100644 --- a/src/functional/enumeration.hpp +++ b/src/functional/enumeration.hpp @@ -214,3 +214,322 @@ template { return !std::to_underlying(e); } + +namespace gal::prometheus::functional +{ + template + requires std::is_scoped_enum_v + class EnumWrapper + { + public: + using type = EnumType; + using underlying_type = std::underlying_type_t; + + private: + constexpr static auto is_flag = meta::user_defined::enum_is_flag::value; + + underlying_type value_; + + public: + constexpr explicit (false) EnumWrapper(const underlying_type value) noexcept + : value_{value} {} + + constexpr explicit (false) EnumWrapper(const type e) noexcept + : EnumWrapper{std::to_underlying(e)} {} + + [[nodiscard]] constexpr explicit (false) operator underlying_type() const noexcept + { + return value_; + } + + [[nodiscard]] constexpr explicit (false) operator type() const noexcept + { + return static_cast(value_); + } + + // ============================================================================= + // operator| + + template + [[nodiscard]] friend constexpr auto operator|(const EnumWrapper lhs, const ValueType rhs) noexcept -> EnumWrapper + { + if constexpr (is_flag) + { + return {lhs.operator type() | rhs}; + } + else + { + return {lhs.operator underlying_type() | rhs}; + } + } + + template + [[nodiscard]] friend constexpr auto operator|(const ValueType lhs, const EnumWrapper rhs) noexcept -> ValueType + { + if constexpr (is_flag) + { + return lhs | rhs.operator type(); + } + else + { + return lhs | rhs.operator underlying_type(); + } + } + + [[nodiscard]] friend constexpr auto operator|(const EnumWrapper lhs, const type rhs) noexcept -> EnumWrapper + { + if constexpr (is_flag) + { + return {lhs.operator type() | rhs}; + } + else + { + return {lhs.operator underlying_type() | rhs}; + } + } + + [[nodiscard]] friend constexpr auto operator|(const type lhs, const EnumWrapper rhs) noexcept -> type + { + if constexpr (is_flag) + { + return lhs | rhs.operator type(); + } + else + { + return lhs | rhs.operator underlying_type(); + } + } + + // ============================================================================= + // operator|= + + template + friend constexpr auto operator|=(EnumWrapper& lhs, const ValueType rhs) noexcept -> EnumWrapper& + { + lhs = lhs | rhs; + return lhs; + } + + template + friend constexpr auto operator|=(ValueType& lhs, const EnumWrapper rhs) noexcept -> ValueType& + { + lhs = lhs | rhs; + return lhs; + } + + friend constexpr auto operator|=(EnumWrapper& lhs, const type rhs) noexcept -> EnumWrapper& + { + lhs = lhs | rhs; + return lhs; + } + + friend constexpr auto operator|=(type& lhs, const EnumWrapper rhs) noexcept -> type& + { + lhs = lhs | rhs; + return lhs; + } + + // ============================================================================= + // operator& + + template + [[nodiscard]] friend constexpr auto operator&(const EnumWrapper lhs, const ValueType rhs) noexcept -> EnumWrapper + { + if constexpr (is_flag) + { + return {lhs.operator type() & rhs}; + } + else + { + return {lhs.operator underlying_type() & rhs}; + } + } + + template + [[nodiscard]] friend constexpr auto operator&(const ValueType lhs, const EnumWrapper rhs) noexcept -> ValueType + { + if constexpr (is_flag) + { + return lhs & rhs.operator type(); + } + else + { + return lhs & rhs.operator underlying_type(); + } + } + + [[nodiscard]] friend constexpr auto operator&(const EnumWrapper lhs, const type rhs) noexcept -> EnumWrapper + { + if constexpr (is_flag) + { + return {lhs.operator type() & rhs}; + } + else + { + return {lhs.operator underlying_type() & rhs}; + } + } + + [[nodiscard]] friend constexpr auto operator&(const type lhs, const EnumWrapper rhs) noexcept -> type + { + if constexpr (is_flag) + { + return lhs & rhs.operator type(); + } + else + { + return lhs & rhs.operator underlying_type(); + } + } + + // ============================================================================= + // operator&= + + template + friend constexpr auto operator&=(EnumWrapper& lhs, const ValueType rhs) noexcept -> EnumWrapper& + { + lhs = lhs & rhs; + return lhs; + } + + template + friend constexpr auto operator&=(ValueType& lhs, const EnumWrapper rhs) noexcept -> ValueType& + { + lhs = lhs & rhs; + return lhs; + } + + friend constexpr auto operator&=(EnumWrapper& lhs, const type rhs) noexcept -> EnumWrapper& + { + lhs = lhs & rhs; + return lhs; + } + + friend constexpr auto operator&=(type& lhs, const EnumWrapper rhs) noexcept -> type& + { + lhs = lhs & rhs; + return lhs; + } + + // ============================================================================= + // operator^ + + template + [[nodiscard]] friend constexpr auto operator^(const EnumWrapper lhs, const ValueType rhs) noexcept -> EnumWrapper + { + if constexpr (is_flag) + { + return {lhs.operator type() ^ rhs}; + } + else + { + return {lhs.operator underlying_type() ^ rhs}; + } + } + + template + [[nodiscard]] friend constexpr auto operator^(const ValueType lhs, const EnumWrapper rhs) noexcept -> ValueType + { + if constexpr (is_flag) + { + return lhs ^ rhs.operator type(); + } + else + { + return lhs ^ rhs.operator underlying_type(); + } + } + + [[nodiscard]] friend constexpr auto operator^(const EnumWrapper lhs, const type rhs) noexcept -> EnumWrapper + { + if constexpr (is_flag) + { + return {lhs.operator type() ^ rhs}; + } + else + { + return {lhs.operator underlying_type() ^ rhs}; + } + } + + [[nodiscard]] friend constexpr auto operator^(const type lhs, const EnumWrapper rhs) noexcept -> type + { + if constexpr (is_flag) + { + return lhs ^ rhs.operator type(); + } + else + { + return lhs ^ rhs.operator underlying_type(); + } + } + + // ============================================================================= + // operator^= + + template + friend constexpr auto operator^=(EnumWrapper& lhs, const ValueType rhs) noexcept -> EnumWrapper& + { + lhs = lhs ^ rhs; + return lhs; + } + + template + friend constexpr auto operator^=(ValueType& lhs, const EnumWrapper rhs) noexcept -> ValueType& + { + lhs = lhs ^ rhs; + return lhs; + } + + friend constexpr auto operator^=(EnumWrapper& lhs, const type rhs) noexcept -> EnumWrapper& + { + lhs = lhs ^ rhs; + return lhs; + } + + friend constexpr auto operator^=(type& lhs, const EnumWrapper rhs) noexcept -> type& + { + lhs = lhs ^ rhs; + return lhs; + } + + // ============================================================================= + // operator~ + + [[nodiscard]] friend constexpr auto operator~(const EnumWrapper self) noexcept -> EnumWrapper + { + if constexpr (is_flag) + { + return {~self.operator type()}; + } + else + { + return {~self.operator underlying_type()}; + } + } + + // ============================================================================= + // operator~ + + [[nodiscard]] friend constexpr auto operator!(const EnumWrapper self) noexcept -> bool + { + if constexpr (is_flag) + { + return !self.operator type(); + } + else + { + return !self.operator underlying_type(); + } + } + }; +} + +namespace std +{ + template + struct underlying_type> // NOLINT(cert-dcl58-cpp) + { + using type = typename gal::prometheus::functional::EnumWrapper::underlying_type; + }; +} diff --git a/src/functional/functor.hpp b/src/functional/functor.hpp index 7b12a564..cdc295dc 100644 --- a/src/functional/functor.hpp +++ b/src/functional/functor.hpp @@ -150,8 +150,20 @@ namespace gal::prometheus::functional template struct overloaded : Ts... { - constexpr explicit overloaded(Ts&&... ts) noexcept((std::is_nothrow_constructible_v and ...)) - : Ts{std::forward(ts)}... {} + // const auto visitor = overloaded + // { + // [](const T1&) { ... }, + // [](const T2&) { ... }, + // [](const T3&) { ... }, + // }; + constexpr explicit overloaded(Ts&&... ts) noexcept((std::is_nothrow_constructible_v and ...)) : Ts{std::forward(ts)}... {} + + // const auto lambda1 = [](const T1&) { ... }; + // const auto lambda2 = [](const T2&) { ... }; + // const auto lambda3 = [](const T3&) { ... }; + // + // const auto visitor = overloaded{ lambda1, lambda2, lambda3 }; + constexpr explicit overloaded(const Ts&... ts) noexcept((std::is_nothrow_constructible_v and ...)) : Ts{ts}... {} // This makes all overloads viable to participate in the resolution using Ts::operator()...; diff --git a/src/gfx/context.hpp b/src/gfx/context.hpp new file mode 100644 index 00000000..028bf213 --- /dev/null +++ b/src/gfx/context.hpp @@ -0,0 +1,51 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include + +namespace gal::prometheus::gfx +{ + class GlyphParser; + class Renderer; + + class Context final + { + friend RenderList; + + public: + class ContextPrivate; + + RenderListSharedData render_list_shared_data; + std::vector render_lists; + + GlyphParser* glyph_parser; + Renderer* renderer; + + private: + memory::UniquePointer private_; + + public: + Context(const Context&) noexcept = delete; + Context(Context&&) noexcept; // = default; + auto operator=(const Context&) noexcept -> Context& = delete; + auto operator=(Context&&) noexcept -> Context&; // = default; + + ~Context() noexcept; // = default; + + Context() noexcept; // = default; + + auto initialize() noexcept -> void; + + auto new_render_list() noexcept -> RenderList&; + + auto new_frame() noexcept -> void; + + auto present(const extent_type& display_size) noexcept -> void; + + auto end_frame() noexcept -> void; + }; +} diff --git a/src/gfx/extension/glyph_parser_freetype.cpp b/src/gfx/extension/glyph_parser_freetype.cpp new file mode 100644 index 00000000..eb65bc68 --- /dev/null +++ b/src/gfx/extension/glyph_parser_freetype.cpp @@ -0,0 +1,288 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +#include + +#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE + +#include +#include + +namespace +{ + [[nodiscard]] auto ft_size_to_float(const FT_Pos size) noexcept -> float + { + return static_cast((size + 63) >> 6); + } +} // namespace + +namespace gal::prometheus::gfx::extension +{ + class GlyphParserFreeType::Library final + { + public: + FT_Library library{nullptr}; + }; + + class GlyphParserFreeType::FontInfo final + { + public: + std::string path; + FT_Face face{nullptr}; + + float ascender{0}; + float descender{0}; + float line_spacing{0}; + float line_gap{0}; + float max_advance_width{0}; + + auto set_pixel_height(const std::size_t height) noexcept -> bool + { + FT_Size_RequestRec request{.type = FT_SIZE_REQUEST_TYPE_NOMINAL, .width = 0, .height = static_cast(height) * 64, .horiResolution = 0, .vertResolution = 0}; + + if (const auto error = FT_Request_Size(face, &request); error != FT_Err_Ok) + { + return false; + } + + const auto& metrics = face->size->metrics; + + ascender = ft_size_to_float(metrics.ascender); + descender = ft_size_to_float(metrics.descender); + line_spacing = ft_size_to_float(metrics.height); + line_gap = ft_size_to_float(metrics.height - metrics.ascender + metrics.descender); + max_advance_width = ft_size_to_float(metrics.max_advance); + + return true; + } + }; + + GlyphParserFreeType::~GlyphParserFreeType() noexcept + { + std::ranges::for_each( + infos_, + [](auto& info) noexcept -> void + { + ::FT_Done_Face(info.face); + } + ); + + FT_Done_FreeType(library_->library); + } + + GlyphParserFreeType::GlyphParserFreeType() noexcept + : library_{memory::make_unique()} + { + if (const auto error = FT_Init_FreeType(&library_->library); error != FT_Err_Ok) + { + // todo: error handling + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + } + } + + auto GlyphParserFreeType::load(const std::string_view path) noexcept -> FontLoadResult + { + if (std::ranges::contains(infos_, path, &FontInfo::path)) + { + return FontLoadResult::FILE_ALREADY_LOADED; + } + + std::error_code ec; + const std::filesystem::path fs_path{path}; + if (not exists(fs_path, ec)) + { + return FontLoadResult::FILE_NOT_FOUND; + } + + if (not is_regular_file(fs_path, ec)) + { + return FontLoadResult::INVALID_FONT_FORMAT; + } + + FT_Face face = nullptr; + if (const auto error = FT_New_Face(library_->library, path.data(), 0, &face); error != FT_Err_Ok) + { + return FontLoadResult::INVALID_FONT_FORMAT; + } + + if (const auto error = FT_Select_Charmap(face, FT_ENCODING_UNICODE); error != FT_Err_Ok) + { + FT_Done_Face(face); + return FontLoadResult::INVALID_FONT_FORMAT; + } + + auto& info = infos_.emplace_back(); + info.path = path; + info.face = face; + + return FontLoadResult::SUCCESS; + } + + // auto GlyphParserFreeType::load(const std::span data) noexcept -> FontDescriptor + // { + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(data.data() != nullptr); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not data.empty()); + // + // FT_Face face = nullptr; + // if (const auto error = FT_New_Memory_Face(library_->library, data.data(), static_cast(data.size()), 0, &face); error != FT_Err_Ok) + // { + // return FontDescriptor::error(); + // } + // + // if (const auto error = FT_Select_Charmap(face, FT_ENCODING_UNICODE); error != FT_Err_Ok) + // { + // FT_Done_Face(face); + // return FontDescriptor::error(); + // } + // + // const auto id = infos_.size(); + // + // auto& info = infos_.emplace_back(); + // info.face = face; + // + // return {.identifier = face->family_name, .id = static_cast(id)}; + // } + + auto GlyphParserFreeType::has_glyph(const std::uint32_t codepoint) const noexcept -> bool + { + return std::ranges::any_of( + infos_, + [codepoint](const auto& info) noexcept -> bool + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(info.face != nullptr); + return ::FT_Get_Char_Index(info.face, codepoint) != 0; + } + ); + } + + auto GlyphParserFreeType::parse(const GlyphCode& code) noexcept -> GlyphDescriptor + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(code.codepoint != 0); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(code.size != 0); + + for (auto& info: infos_) + { + if (const auto char_index = FT_Get_Char_Index(info.face, code.codepoint); char_index != 0) + { + const auto& face = info.face; + + info.set_pixel_height(code.size); + + if (const auto error = FT_Load_Glyph(face, char_index, FT_LOAD_DEFAULT); error != FT_Err_Ok) + { + // todo + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + return {.rect = {}, .advance_x = 0, .visible = false, .colored = false, .data = nullptr}; + } + + const auto& slot = face->glyph; + + if (std::to_underlying(code.flag) & GlyphFlag::BOLD) + { + FT_GlyphSlot_Embolden(slot); + } + if (std::to_underlying(code.flag) & GlyphFlag::ITALIC) + { + FT_GlyphSlot_Oblique(slot); + } + + if (const auto error = FT_Render_Glyph(slot, FT_RENDER_MODE_NORMAL); error != FT_Err_Ok) + { + // todo + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + return {.rect = {}, .advance_x = 0, .visible = false, .colored = false, .data = nullptr}; + } + + const auto& bitmap = slot->bitmap; + + const GlyphDescriptor::rect_type::point_type point{slot->bitmap_left, slot->bitmap_top}; + const GlyphDescriptor::rect_type::extent_type size{bitmap.width, bitmap.rows}; + const auto visible = size.width > 0 and size.height > 0; + + GlyphDescriptor result{ + .rect = {point, size}, + .advance_x = ft_size_to_float(slot->advance.x), + .visible = visible, + .colored = bitmap.pixel_mode == FT_PIXEL_MODE_BGRA, + .data = nullptr + }; + + if (visible) + { + const std::size_t data_length = static_cast(bitmap.width) * bitmap.rows; + result.data = std::make_unique_for_overwrite(data_length); + + const auto* source = bitmap.buffer; + auto* dest = result.data.get(); + + if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) + { + for (std::uint32_t y = 0; y < bitmap.rows; ++y) + { + for (std::uint32_t x = 0; x < bitmap.width; ++x) + { + const auto a = source[x]; + const auto color = + // A + a << 24 | + // B + std::uint32_t{0xff} << 16 | + // G + std::uint32_t{0xff} << 8 | + // R + std::uint32_t{0xff}; + dest[x] = color; + } + + source += bitmap.pitch; + dest += bitmap.width; + } + } + else if (bitmap.pixel_mode == FT_PIXEL_MODE_MONO) + { + for (std::uint32_t y = 0; y < bitmap.rows; ++y) + { + const std::uint8_t* p = source; + std::uint8_t bits = 0; + + for (std::uint32_t x = 0; x < bitmap.width; ++x) + { + if ((x & 7) == 0) + { + bits = *p; + p += 1; + } + + const auto a = (bits & 0x80) ? std::uint32_t{0xff} : std::uint32_t{0}; + const auto color = + // A + a << 24 | + // B + std::uint32_t{0xff} << 16 | + // G + std::uint32_t{0xff} << 8 | + // R + std::uint32_t{0xff}; + dest[x] = color; + + bits <<= 1; + } + + source += bitmap.pitch; + dest += bitmap.width; + } + } + } + + return result; + } + } + + // todo + return {.rect = {}, .advance_x = 0, .visible = false, .colored = false, .data = nullptr}; + } +} diff --git a/src/gfx/extension/glyph_parser_freetype.hpp b/src/gfx/extension/glyph_parser_freetype.hpp new file mode 100644 index 00000000..69bd7e72 --- /dev/null +++ b/src/gfx/extension/glyph_parser_freetype.hpp @@ -0,0 +1,44 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include + +#include + +#include + +namespace gal::prometheus::gfx::extension +{ + class GlyphParserFreeType final : public GlyphParser + { + class Library; + class FontInfo; + + public: + using infos_type = std::vector; + + private: + memory::UniquePointer library_; + infos_type infos_; + + public: + GlyphParserFreeType(const GlyphParserFreeType&) noexcept = delete; + GlyphParserFreeType(GlyphParserFreeType&&) noexcept = default; + auto operator=(const GlyphParserFreeType&) noexcept -> GlyphParserFreeType& = delete; + auto operator=(GlyphParserFreeType&&) noexcept -> GlyphParserFreeType& = default; + + ~GlyphParserFreeType() noexcept override; + + GlyphParserFreeType() noexcept; + + auto load(std::string_view path) noexcept -> FontLoadResult override; + + [[nodiscard]] auto has_glyph(std::uint32_t codepoint) const noexcept -> bool override; + + auto parse(const GlyphCode& code) noexcept -> GlyphDescriptor override; + }; +} diff --git a/src/gfx/extension/renderer_d3d11.cpp b/src/gfx/extension/renderer_d3d11.cpp new file mode 100644 index 00000000..a1bbc35b --- /dev/null +++ b/src/gfx/extension/renderer_d3d11.cpp @@ -0,0 +1,862 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +#include + +#if GAL_PROMETHEUS_COMPILER_DEBUG +#define GAL_PROMETHEUS_GFX_DEBUG +#include +#endif + +#include + +#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE + +#include +#include + +namespace +{ + using namespace gal::prometheus; + using namespace gfx; + + auto check_hr_error( + const HRESULT result +#if defined(GAL_PROMETHEUS_GFX_DEBUG) + , + const std::source_location& location = std::source_location::current() +#endif + ) noexcept -> bool + { + if (SUCCEEDED(result)) + { + return true; + } + +#if defined(GAL_PROMETHEUS_GFX_DEBUG) + + const _com_error err{result}; + std::println(stderr, "Error: {} --- at {}:{}", err.ErrorMessage(), location.file_name(), location.line()); + + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + +#else + + GAL_PROMETHEUS_COMPILER_UNREACHABLE(); + +#endif + + return false; + } + + using projection_matrix_type = float[4][4]; + + [[nodiscard]] auto id_to_gpu_handle(const texture_id_type id) noexcept -> ID3D11ShaderResourceView* + { + return reinterpret_cast(id); // NOLINT(performance-no-int-to-ptr) + } + + [[nodiscard]] auto gpu_handle_to_id(const ID3D11ShaderResourceView* srv) noexcept -> texture_id_type + { + return reinterpret_cast(srv); + } +} + +namespace gal::prometheus::gfx::extension +{ + auto RendererD3D11::create_blend_state() noexcept -> bool + { + constexpr D3D11_RENDER_TARGET_BLEND_DESC render_target + { + .BlendEnable = TRUE, + .SrcBlend = D3D11_BLEND_SRC_ALPHA, + .DestBlend = D3D11_BLEND_INV_SRC_ALPHA, + .BlendOp = D3D11_BLEND_OP_ADD, + .SrcBlendAlpha = D3D11_BLEND_ONE, + .DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA, + .BlendOpAlpha = D3D11_BLEND_OP_ADD, + .RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL + }; + constexpr D3D11_BLEND_DESC blend_desc + { + .AlphaToCoverageEnable = FALSE, + .IndependentBlendEnable = FALSE, + .RenderTarget = + { + render_target, + } + }; + + return check_hr_error(device_->CreateBlendState(&blend_desc, blend_state_.ReleaseAndGetAddressOf())); + } + + auto RendererD3D11::create_rasterizer_state() noexcept -> bool + { + constexpr D3D11_RASTERIZER_DESC rasterizer_desc + { + .FillMode = D3D11_FILL_SOLID, + .CullMode = D3D11_CULL_NONE, + .FrontCounterClockwise = FALSE, + .DepthBias = 0, + .DepthBiasClamp = 0, + .SlopeScaledDepthBias = 0, + .DepthClipEnable = TRUE, + .ScissorEnable = TRUE, + .MultisampleEnable = TRUE, + .AntialiasedLineEnable = TRUE + }; + + return check_hr_error(device_->CreateRasterizerState(&rasterizer_desc, rasterizer_state_.ReleaseAndGetAddressOf())); + } + + auto RendererD3D11::create_depth_stencil_state() noexcept -> bool + { + constexpr D3D11_DEPTH_STENCIL_DESC depth_stencil_desc + { + .DepthEnable = FALSE, + .DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL, + .DepthFunc = D3D11_COMPARISON_ALWAYS, + .StencilEnable = FALSE, + .StencilReadMask = 0, + .StencilWriteMask = 0, + .FrontFace = {.StencilFailOp = D3D11_STENCIL_OP_KEEP, .StencilDepthFailOp = D3D11_STENCIL_OP_KEEP, .StencilPassOp = D3D11_STENCIL_OP_KEEP, .StencilFunc = D3D11_COMPARISON_ALWAYS}, + .BackFace = {.StencilFailOp = D3D11_STENCIL_OP_KEEP, .StencilDepthFailOp = D3D11_STENCIL_OP_KEEP, .StencilPassOp = D3D11_STENCIL_OP_KEEP, .StencilFunc = D3D11_COMPARISON_ALWAYS} + }; + + return check_hr_error(device_->CreateDepthStencilState(&depth_stencil_desc, depth_stencil_state_.ReleaseAndGetAddressOf())); + } + + auto RendererD3D11::create_vertex_shader() noexcept -> bool + { + constexpr char shader_code[] + { + "cbuffer vertexBuffer : register(b0)" + "{" + " float4x4 ProjectionMatrix;" + "};" + "struct VS_INPUT" + "{" + " float2 pos : POSITION;" + " float4 col : COLOR0;" + " float2 uv : TEXCOORD0;" + "};" + "struct PS_INPUT" + "{" + " float4 pos : SV_POSITION;" + " float4 col : COLOR0;" + " float2 uv : TEXCOORD0;" + "};" + "PS_INPUT main(VS_INPUT input)" + "{" + " PS_INPUT output;" + " output.pos = mul(ProjectionMatrix, float4(input.pos.xy, 0.f, 1.f));" + " output.col = input.col;" + " output.uv = input.uv;" + " return output;" + "}" + }; + + ComPtr shader_blob; + ComPtr error_message; + + if (const auto result = D3DCompile( + shader_code, + sizeof(shader_code), + nullptr, + nullptr, + nullptr, + "main", + "vs_5_0", + 0, + 0, + shader_blob.GetAddressOf(), + error_message.GetAddressOf() + ); + FAILED(result)) + { + std::println( + stderr, + "D3DCompile failed: {} -- at {}:{}", + static_cast(error_message->GetBufferPointer()), + std::source_location::current().file_name(), + std::source_location::current().line() + ); + + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + return false; + } + + if (not check_hr_error( + device_->CreateVertexShader( + shader_blob->GetBufferPointer(), + shader_blob->GetBufferSize(), + nullptr, + vertex_shader_.ReleaseAndGetAddressOf() + ) + )) + { + return false; + } + + // vertex input layout + constexpr D3D11_INPUT_ELEMENT_DESC input_element_desc[]{ + { + .SemanticName = "POSITION", + .SemanticIndex = 0, + .Format = DXGI_FORMAT_R32G32_FLOAT, + .InputSlot = 0, + .AlignedByteOffset = static_cast(offsetof(vertex_type, position)), + .InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA, + .InstanceDataStepRate = 0 + }, + { + .SemanticName = "COLOR", + .SemanticIndex = 0, + .Format = DXGI_FORMAT_R8G8B8A8_UNORM, + .InputSlot = 0, + .AlignedByteOffset = static_cast(offsetof(vertex_type, color)), + .InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA, + .InstanceDataStepRate = 0 + }, + { + .SemanticName = "TEXCOORD", + .SemanticIndex = 0, + .Format = DXGI_FORMAT_R32G32_FLOAT, + .InputSlot = 0, + .AlignedByteOffset = static_cast(offsetof(vertex_type, uv)), + .InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA, + .InstanceDataStepRate = 0 + }, + }; + if (not check_hr_error( + device_->CreateInputLayout( + input_element_desc, + static_cast(std::ranges::size(input_element_desc)), + shader_blob->GetBufferPointer(), + shader_blob->GetBufferSize(), + vertex_input_layout_.ReleaseAndGetAddressOf() + ) + )) + { + return false; + } + + // constant buffer + constexpr D3D11_BUFFER_DESC constant_buffer_desc{ + .ByteWidth = sizeof(projection_matrix_type), + .Usage = D3D11_USAGE_DYNAMIC, + .BindFlags = D3D11_BIND_CONSTANT_BUFFER, + .CPUAccessFlags = D3D11_CPU_ACCESS_WRITE, + .MiscFlags = 0, + .StructureByteStride = 0 + }; + if (not check_hr_error( + device_->CreateBuffer( + &constant_buffer_desc, + nullptr, + vertex_projection_matrix_.ReleaseAndGetAddressOf() + ) + )) + { + return false; + } + + return true; + } + + auto RendererD3D11::create_pixel_shader() noexcept -> bool + { + constexpr char shader_code[] + { + "struct PS_INPUT" + "{" + " float4 pos : SV_POSITION;" + " float4 col : COLOR0;" + " float2 uv : TEXCOORD0;" + "};" + "sampler sampler0;" + "Texture2D texture0;" + "float4 main(PS_INPUT input) : SV_Target" + "{" + " float4 out_col = texture0.Sample(sampler0, input.uv);" + " return input.col * out_col;" + "}" + }; + + ComPtr shader_blob; + ComPtr error_message; + + if (const auto result = D3DCompile( + shader_code, + sizeof(shader_code), + nullptr, + nullptr, + nullptr, + "main", + "ps_5_0", + 0, + 0, + shader_blob.GetAddressOf(), + error_message.GetAddressOf() + ); + FAILED(result)) + { + std::println( + stderr, + "D3DCompile failed: {} -- at {}:{}", + static_cast(error_message->GetBufferPointer()), + std::source_location::current().file_name(), + std::source_location::current().line() + ); + + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + return false; + } + + if (not check_hr_error( + device_->CreatePixelShader( + shader_blob->GetBufferPointer(), + shader_blob->GetBufferSize(), + nullptr, + pixel_shader_.ReleaseAndGetAddressOf() + ) + )) + { + return false; + } + + // Create pixel shader texture sampler + constexpr D3D11_SAMPLER_DESC sampler_desc + { + .Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR, + .AddressU = D3D11_TEXTURE_ADDRESS_WRAP, + .AddressV = D3D11_TEXTURE_ADDRESS_WRAP, + .AddressW = D3D11_TEXTURE_ADDRESS_WRAP, + .MipLODBias = 0, + .MaxAnisotropy = 0, + .ComparisonFunc = D3D11_COMPARISON_ALWAYS, + .BorderColor = {0, 0, 0, 0}, + .MinLOD = 0, + .MaxLOD = 0 + }; + if (not check_hr_error( + device_->CreateSamplerState( + &sampler_desc, + pixel_font_sampler_.ReleaseAndGetAddressOf() + ) + )) + { + return false; + } + + return true; + } + + auto RendererD3D11::upload_texture( + const Texture::data_type& data, + const Texture::size_type size, + const D3D11_USAGE usage, + const std::uint32_t bind_flags, + const std::uint32_t cpu_access_flags, + const std::uint32_t misc_flags, + const bool record_resource + ) noexcept -> texture_id_type + { + const D3D11_TEXTURE2D_DESC texture_2d_desc + { + .Width = static_cast(size.width), + .Height = static_cast(size.height), + .MipLevels = 1, + .ArraySize = 1, + .Format = DXGI_FORMAT_R8G8B8A8_UNORM, + .SampleDesc = {.Count = 1, .Quality = 0}, + .Usage = usage, + .BindFlags = bind_flags, + .CPUAccessFlags = cpu_access_flags, + .MiscFlags = misc_flags + }; + + const D3D11_SUBRESOURCE_DATA subresource_data + { + .pSysMem = data.get(), + .SysMemPitch = static_cast(size.width * sizeof(Texture::element_type)), + .SysMemSlicePitch = 0 + }; + + ID3D11Texture2D* texture_2d; + if (not check_hr_error( + device_->CreateTexture2D( + &texture_2d_desc, + &subresource_data, + &texture_2d + ) + )) + { + return invalid_texture_id; + } + + const D3D11_SHADER_RESOURCE_VIEW_DESC shader_resource_view_desc + { + .Format = texture_2d_desc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = + { + .MostDetailedMip = 0, + .MipLevels = texture_2d_desc.MipLevels + } + }; + + ID3D11ShaderResourceView* srv = nullptr; + if (not check_hr_error( + device_->CreateShaderResourceView( + texture_2d, + &shader_resource_view_desc, + &srv + ) + )) + { + return false; + } + + if (record_resource) + { + textures_.insert_or_assign(srv, texture_2d); + } + else + { + texture_2d->Release(); + } + + return gpu_handle_to_id(srv); + } + + RendererD3D11::RendererD3D11() noexcept + : device_{nullptr}, + device_immediate_context_{nullptr}, + blend_state_{nullptr}, + rasterizer_state_{nullptr}, + depth_stencil_state_{nullptr}, + vertex_shader_{nullptr}, + vertex_input_layout_{nullptr}, + vertex_projection_matrix_{nullptr}, + pixel_shader_{nullptr}, + pixel_font_sampler_{nullptr}, + render_buffer_{} {} + + RendererD3D11::RendererD3D11(ID3D11Device* device, ID3D11DeviceContext* device_immediate_context) noexcept + : RendererD3D11{} + { + bind_device(device); + bind_device_context(device_immediate_context); + } + + RendererD3D11::RendererD3D11(ComPtr device, ComPtr device_immediate_context) noexcept + : RendererD3D11{} + { + bind_device(std::move(device)); + bind_device_context(std::move(device_immediate_context)); + } + + auto RendererD3D11::bind_device(ID3D11Device* device) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(device != nullptr); + device_ = device; + } + + auto RendererD3D11::bind_device(ComPtr device) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(device != nullptr); + device_ = std::move(device); + } + + auto RendererD3D11::bind_device_context(ID3D11DeviceContext* device_immediate_context) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(device_immediate_context != nullptr); + device_immediate_context_ = device_immediate_context; + } + + auto RendererD3D11::bind_device_context(ComPtr device_immediate_context) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(device_immediate_context != nullptr); + device_immediate_context_ = std::move(device_immediate_context); + } + + auto RendererD3D11::construct() noexcept -> bool + { + if (not create_blend_state()) + { + return false; + } + if (not create_rasterizer_state()) + { + return false; + } + if (not create_depth_stencil_state()) + { + return false; + } + if (not create_vertex_shader()) + { + return false; + } + if (not create_pixel_shader()) + { + return false; + } + + return true; + } + + auto RendererD3D11::destruct() noexcept -> void + { + // ComPtr + blend_state_ = nullptr; + rasterizer_state_ = nullptr; + depth_stencil_state_ = nullptr; + vertex_shader_ = nullptr; + vertex_input_layout_ = nullptr; + vertex_projection_matrix_ = nullptr; + pixel_shader_ = nullptr; + pixel_font_sampler_ = nullptr; + render_buffer_.index = nullptr; + render_buffer_.index_count = 0; + render_buffer_.vertex = nullptr; + render_buffer_.vertex_count = 0; + + // RAW + std::ranges::for_each( + textures_, + [](auto& kv) noexcept -> void + { + kv.first->Release(); + kv.second->Release(); + } + ); + textures_.clear(); + + // ComPtr + // device_immediate_context_->ClearState(); + // device_immediate_context_->Flush(); + device_immediate_context_ = nullptr; + device_ = nullptr; + } + + auto RendererD3D11::create_texture(const Texture::data_type& data, const Texture::size_type size) noexcept -> texture_id_type + { + return upload_texture(data, size, D3D11_USAGE_DYNAMIC, D3D11_BIND_SHADER_RESOURCE, D3D11_CPU_ACCESS_WRITE, 0); + } + + auto RendererD3D11::update_texture(const texture_id_type id, std::span update_viewer) noexcept -> void + { + auto* srv = id_to_gpu_handle(id); + const auto it = textures_.find(srv); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(it != textures_.end(), "Invalid texture id"); + + auto* texture_2d = it->second; + + // todo + if (update_viewer.size() > 16) + { + D3D11_MAPPED_SUBRESOURCE mapped_resource{}; + if (const auto result = device_immediate_context_->Map(texture_2d, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped_resource); result != S_OK) + { + // todo: error handling + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + } + else + { + std::ranges::for_each( + update_viewer, + [ + this, + dest_begin = static_cast(mapped_resource.pData), + pitch = static_cast(mapped_resource.RowPitch / sizeof(Texture::element_type)) + ](const TextureViewer& viewer) noexcept -> void + { + const auto point = viewer.position(); + const auto size = viewer.size(); + + const auto y_offset = static_cast(point.y) * pitch; + const auto x_offset = point.x; + auto* dest = dest_begin + y_offset + x_offset; + + for (Texture::size_type::value_type y = 0; y < size.height; ++y) + { + const auto this_line_source = viewer.line(y); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(this_line_source.size() == size.width, "TextureViewer line size mismatch"); + + auto* this_line_dest = dest + static_cast(y) * pitch; + + std::ranges::copy(this_line_source, this_line_dest); + } + } + ); + + device_immediate_context_->Unmap(texture_2d, 0); + return; + } + } + + std::ranges::for_each( + update_viewer, + [this, texture_2d](const TextureViewer& viewer) noexcept -> void + { + const auto point = viewer.position(); + const auto size = viewer.size(); + + const auto data = std::make_unique_for_overwrite(static_cast(size.width) * size.height); + for (Texture::size_type::value_type y = 0; y < size.height; ++y) + { + const auto this_line = viewer.line(y); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(this_line.size() == size.width, "TextureViewer line size mismatch"); + + std::ranges::copy(this_line, data.get() + static_cast(size.width) * y); + } + + const D3D11_BOX box + { + .left = point.x, + .top = point.y, + .front = 0, + .right = point.x + size.width, + .bottom = point.y + size.height, + .back = 1 + }; + + device_immediate_context_->UpdateSubresource( + texture_2d, + 0, + &box, + data.get(), + static_cast(size.width * sizeof(Texture::element_type)), + 0 + ); + } + ); + } + + auto RendererD3D11::destroy_texture(const texture_id_type id) noexcept -> void + { + auto* srv = id_to_gpu_handle(id); + + if (const auto it = textures_.find(srv); it != textures_.end()) + { + srv->Release(); + it->first->Release(); + it->second->Release(); + + textures_.erase(it); + } + } + + auto RendererD3D11::present(const render_data_list_type& render_data_list, const extent_type& display_size) noexcept -> void + { + auto& [this_frame_index_buffer, this_frame_index_count, this_frame_vertex_buffer, this_frame_vertex_count] = render_buffer_; + + const auto [total_vertex_count, total_index_count] = [&]() noexcept + { + struct sum + { + UINT vertex; + UINT index; + }; + + return std::ranges::fold_left( + render_data_list, + sum{.vertex = 0, .index = 0}, + [](const sum s, const RenderData& render_data) noexcept -> sum + { + const auto vertex_list = render_data.vertex_list.get(); + const auto index_list = render_data.index_list.get(); + + return {.vertex = s.vertex + static_cast(vertex_list.size()), .index = s.index + static_cast(index_list.size())}; + } + ); + }(); + + if (not this_frame_vertex_buffer or total_vertex_count > this_frame_vertex_count) + { + // todo: grow factor + this_frame_vertex_count = total_vertex_count + 5000; + + const D3D11_BUFFER_DESC buffer_desc{ + .ByteWidth = static_cast(this_frame_vertex_count * sizeof(vertex_type)), + .Usage = D3D11_USAGE_DYNAMIC, + .BindFlags = D3D11_BIND_VERTEX_BUFFER, + .CPUAccessFlags = D3D11_CPU_ACCESS_WRITE, + .MiscFlags = 0, + .StructureByteStride = 0 + }; + check_hr_error(device_->CreateBuffer(&buffer_desc, nullptr, this_frame_vertex_buffer.ReleaseAndGetAddressOf())); + } + if (not this_frame_index_buffer or total_index_count > this_frame_index_count) + { + // todo: grow factor + this_frame_index_count = total_index_count + 10000; + + const D3D11_BUFFER_DESC buffer_desc{ + .ByteWidth = static_cast(this_frame_index_count * sizeof(index_type)), + .Usage = D3D11_USAGE_DYNAMIC, + .BindFlags = D3D11_BIND_INDEX_BUFFER, + .CPUAccessFlags = D3D11_CPU_ACCESS_WRITE, + .MiscFlags = 0, + .StructureByteStride = 0 + }; + check_hr_error(device_->CreateBuffer(&buffer_desc, nullptr, this_frame_index_buffer.ReleaseAndGetAddressOf())); + } + + // Upload vertex/index data into a single contiguous GPU buffer + { + D3D11_MAPPED_SUBRESOURCE mapped_vertex_resource; + D3D11_MAPPED_SUBRESOURCE mapped_index_resource; + check_hr_error(device_immediate_context_->Map(this_frame_vertex_buffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped_vertex_resource)); + check_hr_error(device_immediate_context_->Map(this_frame_index_buffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped_index_resource)); + + auto* mapped_vertex = static_cast(mapped_vertex_resource.pData); + auto* mapped_index = static_cast(mapped_index_resource.pData); + + UINT vertex_offset = 0; + UINT index_offset = 0; + + std::ranges::for_each( + render_data_list, + [&](const RenderData& render_data) noexcept -> void + { + const auto vertex_list = render_data.vertex_list.get(); + const auto index_list = render_data.index_list.get(); + + // std::ranges::transform( + // vertex_list, + // mapped_vertex + vertex_offset, + // [](const vertex_type& vertex) noexcept -> vertex_type + // { + // // return { + // // .position = {vertex.position.x, vertex.position.y}, + // // .uv = {vertex.uv.x, vertex.uv.y}, + // // .color = vertex.color.to(primitive::color_format) + // // }; + // return std::bit_cast(vertex); + // } + // ); + std::ranges::copy(vertex_list, mapped_vertex + vertex_offset); + // std::ranges::transform( + // index_list, + // mapped_index + index_offset, + // [vertex_offset](const index_type index) noexcept -> index_type + // { + // return static_cast(index + vertex_offset); + // } + // ); + std::ranges::copy(index_list, mapped_index + index_offset); + + vertex_offset += static_cast(vertex_list.size()); + index_offset += static_cast(index_list.size()); + } + ); + + device_immediate_context_->Unmap(this_frame_vertex_buffer.Get(), 0); + device_immediate_context_->Unmap(this_frame_index_buffer.Get(), 0); + } + + // Setup orthographic projection matrix into our constant buffer + { + D3D11_MAPPED_SUBRESOURCE mapped_resource; + check_hr_error(device_immediate_context_->Map(vertex_projection_matrix_.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped_resource)); + + auto* mapped_projection_matrix = static_cast(mapped_resource.pData); + + constexpr auto left = 0.f; + const auto right = display_size.width; + constexpr auto top = 0.f; + const auto bottom = display_size.height; + + const projection_matrix_type mvp{ + {2.0f / (right - left), 0.0f, 0.0f, 0.0f}, + {0.0f, 2.0f / (top - bottom), 0.0f, 0.0f}, + {0.0f, 0.0f, 0.5f, 0.0f}, + {(right + left) / (left - right), (top + bottom) / (bottom - top), 0.5f, 1.0f}, + }; + std::memcpy(mapped_projection_matrix, &mvp, sizeof(projection_matrix_type)); + + device_immediate_context_->Unmap(vertex_projection_matrix_.Get(), 0); + } + + // Setup viewport + { + const D3D11_VIEWPORT viewport{ + .TopLeftX = .0f, + .TopLeftY = .0f, + .Width = display_size.width, + .Height = display_size.height, + .MinDepth = 0, + .MaxDepth = 1 + }; + device_immediate_context_->RSSetViewports(1, &viewport); + } + + // Bind shader and vertex buffers + constexpr UINT stride = sizeof(vertex_type); + constexpr UINT offset = 0; + device_immediate_context_->IASetInputLayout(vertex_input_layout_.Get()); + device_immediate_context_->IASetVertexBuffers(0, 1, this_frame_vertex_buffer.GetAddressOf(), &stride, &offset); + device_immediate_context_->IASetIndexBuffer( + this_frame_index_buffer.Get(), + // ReSharper disable once CppUnreachableCode + sizeof(index_type) == 2 ? DXGI_FORMAT_R16_UINT : DXGI_FORMAT_R32_UINT, + 0 + ); + device_immediate_context_->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + device_immediate_context_->VSSetShader(vertex_shader_.Get(), nullptr, 0); + device_immediate_context_->VSSetConstantBuffers(0, 1, vertex_projection_matrix_.GetAddressOf()); + device_immediate_context_->PSSetShader(pixel_shader_.Get(), nullptr, 0); + device_immediate_context_->PSSetSamplers(0, 1, pixel_font_sampler_.GetAddressOf()); + device_immediate_context_->DSSetShader(nullptr, nullptr, 0); + device_immediate_context_->HSSetShader(nullptr, nullptr, 0); + device_immediate_context_->GSSetShader(nullptr, nullptr, 0); + device_immediate_context_->CSSetShader(nullptr, nullptr, 0); + + // Setup render state + constexpr float blend_factor[]{0, 0, 0, 0}; + device_immediate_context_->OMSetBlendState(blend_state_.Get(), blend_factor, (std::numeric_limits::max)()); + device_immediate_context_->OMSetDepthStencilState(depth_stencil_state_.Get(), 0); + device_immediate_context_->RSSetState(rasterizer_state_.Get()); + + UINT total_index_offset = 0; + std::ranges::for_each( + render_data_list, + [this, &total_index_offset](const RenderData& render_data) noexcept -> void + { + const auto vertex_list = render_data.vertex_list.get(); + const auto index_list = render_data.index_list.get(); + + for (const auto& command_list = render_data.command_list.get(); + const auto& [scissor, texture, index_offset, element_count]: command_list) + { + const auto [point, extent] = scissor; + const D3D11_RECT rect + { + static_cast(point.x), + static_cast(point.y), + static_cast(point.x + extent.width), + static_cast(point.y + extent.height) + }; + device_immediate_context_->RSSetScissorRects(1, &rect); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(texture != invalid_texture_id); + auto* srv = id_to_gpu_handle(texture); + device_immediate_context_->PSSetShaderResources(0, 1, &srv); + + const auto this_index_offset = static_cast(total_index_offset + index_offset); + // device_immediate_context_->DrawIndexed(static_cast(element_count), this_index_offset, 0); + device_immediate_context_->DrawIndexedInstanced(static_cast(element_count), 1, this_index_offset, 0, 0); + } + + total_index_offset += static_cast(index_list.size()); + } + ); + } +} diff --git a/src/gfx/extension/renderer_d3d11.hpp b/src/gfx/extension/renderer_d3d11.hpp new file mode 100644 index 00000000..0aabfd84 --- /dev/null +++ b/src/gfx/extension/renderer_d3d11.hpp @@ -0,0 +1,87 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include + +#include + +#include +#include + +namespace gal::prometheus::gfx::extension +{ + using Microsoft::WRL::ComPtr; + + class RendererD3D11 final : public Renderer + { + public: + using textures_type = std::unordered_map; + + private: + struct render_buffer_type + { + ComPtr index; + UINT index_count; + ComPtr vertex; + UINT vertex_count; + }; + + ComPtr device_; + ComPtr device_immediate_context_; + + ComPtr blend_state_; + ComPtr rasterizer_state_; + ComPtr depth_stencil_state_; + + ComPtr vertex_shader_; + ComPtr vertex_input_layout_; + ComPtr vertex_projection_matrix_; + + ComPtr pixel_shader_; + ComPtr pixel_font_sampler_; + + textures_type textures_; + + render_buffer_type render_buffer_; + + [[nodiscard]] auto create_blend_state() noexcept -> bool; + [[nodiscard]] auto create_rasterizer_state() noexcept -> bool; + [[nodiscard]] auto create_depth_stencil_state() noexcept -> bool; + + [[nodiscard]] auto create_vertex_shader() noexcept -> bool; + [[nodiscard]] auto create_pixel_shader() noexcept -> bool; + + [[nodiscard]] auto upload_texture( + const Texture::data_type& data, + Texture::size_type size, + D3D11_USAGE usage, + std::uint32_t bind_flags, + std::uint32_t cpu_access_flags, + std::uint32_t misc_flags, + bool record_resource = true + ) noexcept -> texture_id_type; + + public: + RendererD3D11() noexcept; + RendererD3D11(ID3D11Device* device, ID3D11DeviceContext* device_immediate_context) noexcept; + RendererD3D11(ComPtr device, ComPtr device_immediate_context) noexcept; + + auto bind_device(ID3D11Device* device) noexcept -> void; + auto bind_device(ComPtr device) noexcept -> void; + auto bind_device_context(ID3D11DeviceContext* device_immediate_context) noexcept -> void; + auto bind_device_context(ComPtr device_immediate_context) noexcept -> void; + + auto construct() noexcept -> bool; + auto destruct() noexcept -> void; + + auto create_texture(const Texture::data_type& data, Texture::size_type size) noexcept -> texture_id_type override; + auto update_texture(texture_id_type id, std::span update_viewer) noexcept -> void override; + auto destroy_texture(texture_id_type id) noexcept -> void override; + + auto present(const render_data_list_type& render_data_list, const extent_type& display_size) noexcept -> void override; + }; +} diff --git a/src/gfx/extension/renderer_d3d12.cpp b/src/gfx/extension/renderer_d3d12.cpp new file mode 100644 index 00000000..a845030e --- /dev/null +++ b/src/gfx/extension/renderer_d3d12.cpp @@ -0,0 +1,1257 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +#include + +#if GAL_PROMETHEUS_COMPILER_DEBUG +#define GAL_PROMETHEUS_GFX_DEBUG +#include +#endif + +#include + +#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE + +#include +#include + +namespace +{ + using namespace gal::prometheus; + using namespace gfx; + + auto check_hr_error( + const HRESULT result +#if defined(GAL_PROMETHEUS_GFX_DEBUG) + , + const std::source_location& location = std::source_location::current() +#endif + ) noexcept -> bool + { + if (SUCCEEDED(result)) + { + return true; + } + +#if defined(GAL_PROMETHEUS_GFX_DEBUG) + + const _com_error err{result}; + std::println(stderr, "Error: {} --- at {}:{}", err.ErrorMessage(), location.file_name(), location.line()); + + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + +#else + + GAL_PROMETHEUS_COMPILER_UNREACHABLE(); + +#endif + + return false; + } + + using projection_matrix_type = float[4][4]; + + constexpr std::size_t gpu_handle_to_id_offset = 0x1000'0000; + + [[nodiscard]] auto id_to_gpu_handle(const texture_id_type id) noexcept -> std::size_t + { + return id - gpu_handle_to_id_offset; + } + + [[nodiscard]] auto gpu_handle_to_id(const std::size_t index) noexcept -> texture_id_type + { + return index + gpu_handle_to_id_offset; + } +} + +namespace gal::prometheus::gfx::extension +{ + auto RendererD3D12::create_root_signature() noexcept -> bool + { + // [0] projection_matrix + constexpr D3D12_ROOT_PARAMETER param_0 + { + .ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS, + .Constants = {.ShaderRegister = 0, .RegisterSpace = 0, .Num32BitValues = sizeof(projection_matrix_type) / sizeof(float)}, + .ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX + }; + // [1] texture + constexpr D3D12_DESCRIPTOR_RANGE range + { + .RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV, + .NumDescriptors = 1, + .BaseShaderRegister = 0, + .RegisterSpace = 0, + .OffsetInDescriptorsFromTableStart = 0 + }; + const D3D12_ROOT_PARAMETER param_1 + { + .ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE, + .DescriptorTable = {.NumDescriptorRanges = 1, .pDescriptorRanges = &range}, + .ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL + }; + // @see Dx12Renderer::do_present -> `command_list_->SetGraphicsRootXxx` + const D3D12_ROOT_PARAMETER params[]{param_0, param_1}; + + // Bi-linear sampling is required by default + constexpr D3D12_STATIC_SAMPLER_DESC static_sampler_desc + { + .Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR, + .AddressU = D3D12_TEXTURE_ADDRESS_MODE_WRAP, + .AddressV = D3D12_TEXTURE_ADDRESS_MODE_WRAP, + .AddressW = D3D12_TEXTURE_ADDRESS_MODE_WRAP, + .MipLODBias = .0f, + .MaxAnisotropy = 0, + .ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS, + .BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK, + .MinLOD = .0f, + .MaxLOD = .0f, + .ShaderRegister = 0, + .RegisterSpace = 0, + .ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL + }; + + const D3D12_ROOT_SIGNATURE_DESC root_signature_desc + { + .NumParameters = static_cast(std::ranges::size(params)), + .pParameters = params, + .NumStaticSamplers = 1, + .pStaticSamplers = &static_sampler_desc, + .Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT | D3D12_ROOT_SIGNATURE_FLAG_DENY_HULL_SHADER_ROOT_ACCESS | + D3D12_ROOT_SIGNATURE_FLAG_DENY_DOMAIN_SHADER_ROOT_ACCESS | D3D12_ROOT_SIGNATURE_FLAG_DENY_GEOMETRY_SHADER_ROOT_ACCESS + }; + + auto d3d12_dll = GetModuleHandleW(L"d3d12.dll"); + if (d3d12_dll == nullptr) + { + d3d12_dll = LoadLibraryW(L"d3d12.dll"); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(d3d12_dll != nullptr); + } + + const auto serialize_root_signature_function = + reinterpret_cast(GetProcAddress(d3d12_dll, "D3D12SerializeRootSignature")); // NOLINT(clang-diagnostic-cast-function-type-strict) + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(serialize_root_signature_function != nullptr); + + ComPtr blob = nullptr; + if (not check_hr_error(serialize_root_signature_function(&root_signature_desc, D3D_ROOT_SIGNATURE_VERSION_1, blob.GetAddressOf(), nullptr))) + { + return false; + } + if (not check_hr_error(device_->CreateRootSignature(0, blob->GetBufferPointer(), blob->GetBufferSize(), IID_PPV_ARGS(root_signature_.GetAddressOf())))) + { + return false; + } + + return true; + } + + auto RendererD3D12::create_pipeline_state() noexcept -> bool + { + // Create the vertex shader + auto vertex_shader_blob = []() -> ComPtr + { + constexpr static char shader[]{ + "cbuffer vertexBuffer : register(b0)" + "{" + " float4x4 ProjectionMatrix;" + "};" + "struct VS_INPUT" + "{" + " float2 pos : POSITION;" + " float4 col : COLOR0;" + " float2 uv : TEXCOORD0;" + "};" + "struct PS_INPUT" + "{" + " float4 pos : SV_POSITION;" + " float4 col : COLOR0;" + " float2 uv : TEXCOORD0;" + "};" + "PS_INPUT main(VS_INPUT input)" + "{" + " PS_INPUT output;" + " output.pos = mul(ProjectionMatrix, float4(input.pos.xy, 0.f, 1.f));" + " output.col = input.col;" + " output.uv = input.uv;" + " return output;" + "}" + }; + + ComPtr blob; + if (not check_hr_error(D3DCompile(shader, std::ranges::size(shader), nullptr, nullptr, nullptr, "main", "vs_5_0", 0, 0, blob.GetAddressOf(), nullptr))) + { + return nullptr; + } + + return blob; + }(); + + // Create the pixel shader + auto pixel_shader_blob = []() -> ComPtr + { + constexpr static char shader[]{ + "struct PS_INPUT" + "{" + " float4 pos : SV_POSITION;" + " float4 col : COLOR0;" + " float2 uv : TEXCOORD0;" + "};" + "sampler sampler0;" + "Texture2D texture0;" + "float4 main(PS_INPUT input) : SV_Target" + "{" + " float4 out_col = texture0.Sample(sampler0, input.uv);" + " return input.col * out_col;" + "}" + }; + + ComPtr blob; + if (not check_hr_error(D3DCompile(shader, std::ranges::size(shader), nullptr, nullptr, nullptr, "main", "ps_5_0", 0, 0, blob.GetAddressOf(), nullptr))) + { + return nullptr; + } + + return blob; + }(); + + if (vertex_shader_blob == nullptr or pixel_shader_blob == nullptr) + { + return false; + } + + // Create the blending setup + constexpr D3D12_RENDER_TARGET_BLEND_DESC render_target_blend_desc + { + .BlendEnable = true, + .LogicOpEnable = false, + .SrcBlend = D3D12_BLEND_SRC_ALPHA, + .DestBlend = D3D12_BLEND_INV_SRC_ALPHA, + .BlendOp = D3D12_BLEND_OP_ADD, + .SrcBlendAlpha = D3D12_BLEND_ONE, + .DestBlendAlpha = D3D12_BLEND_INV_SRC_ALPHA, + .BlendOpAlpha = D3D12_BLEND_OP_ADD, + .LogicOp = D3D12_LOGIC_OP_CLEAR, + .RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL + }; + constexpr D3D12_BLEND_DESC blend_desc + { + .AlphaToCoverageEnable = FALSE, + .IndependentBlendEnable = FALSE, + .RenderTarget = {render_target_blend_desc} + }; + + // Create the rasterizer state + constexpr D3D12_RASTERIZER_DESC rasterizer_desc + { + .FillMode = D3D12_FILL_MODE_SOLID, + .CullMode = D3D12_CULL_MODE_NONE, + .FrontCounterClockwise = FALSE, + .DepthBias = D3D12_DEFAULT_DEPTH_BIAS, + .DepthBiasClamp = D3D12_DEFAULT_DEPTH_BIAS_CLAMP, + .SlopeScaledDepthBias = D3D12_DEFAULT_SLOPE_SCALED_DEPTH_BIAS, + .DepthClipEnable = TRUE, + .MultisampleEnable = FALSE, + .AntialiasedLineEnable = FALSE, + .ForcedSampleCount = 0, + .ConservativeRaster = D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF + }; + + // Create depth-stencil State + constexpr D3D12_DEPTH_STENCIL_DESC depth_stencil_desc + { + .DepthEnable = FALSE, + .DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL, + .DepthFunc = D3D12_COMPARISON_FUNC_ALWAYS, + .StencilEnable = FALSE, + .StencilReadMask = 0, + .StencilWriteMask = 0, + .FrontFace = + { + .StencilFailOp = D3D12_STENCIL_OP_KEEP, + .StencilDepthFailOp = D3D12_STENCIL_OP_KEEP, + .StencilPassOp = D3D12_STENCIL_OP_KEEP, + .StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS + }, + .BackFace = + { + .StencilFailOp = D3D12_STENCIL_OP_KEEP, + .StencilDepthFailOp = D3D12_STENCIL_OP_KEEP, + .StencilPassOp = D3D12_STENCIL_OP_KEEP, + .StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS + } + }; + + // Create the input layout + constexpr D3D12_INPUT_ELEMENT_DESC input_element_desc[] + { + { + .SemanticName = "POSITION", + .SemanticIndex = 0, + .Format = DXGI_FORMAT_R32G32_FLOAT, + .InputSlot = 0, + .AlignedByteOffset = static_cast(offsetof(vertex_type, position)), + .InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, + .InstanceDataStepRate = 0 + }, + { + .SemanticName = "TEXCOORD", + .SemanticIndex = 0, + .Format = DXGI_FORMAT_R32G32_FLOAT, + .InputSlot = 0, + .AlignedByteOffset = static_cast(offsetof(vertex_type, uv)), + .InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, + .InstanceDataStepRate = 0 + }, + { + .SemanticName = "COLOR", + .SemanticIndex = 0, + .Format = DXGI_FORMAT_R8G8B8A8_UNORM, + .InputSlot = 0, + .AlignedByteOffset = static_cast(offsetof(vertex_type, color)), + .InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, + .InstanceDataStepRate = 0 + }, + }; + + D3D12_GRAPHICS_PIPELINE_STATE_DESC pipeline_state_desc + { + .pRootSignature = root_signature_.Get(), + .VS = {.pShaderBytecode = vertex_shader_blob->GetBufferPointer(), .BytecodeLength = vertex_shader_blob->GetBufferSize()}, + .PS = {.pShaderBytecode = pixel_shader_blob->GetBufferPointer(), .BytecodeLength = pixel_shader_blob->GetBufferSize()}, + .DS = {.pShaderBytecode = nullptr, .BytecodeLength = 0}, + .HS = {.pShaderBytecode = nullptr, .BytecodeLength = 0}, + .GS = {.pShaderBytecode = nullptr, .BytecodeLength = 0}, + .StreamOutput = {.pSODeclaration = nullptr, .NumEntries = 0, .pBufferStrides = nullptr, .NumStrides = 0, .RasterizedStream = 0}, + .BlendState = blend_desc, + .SampleMask = UINT_MAX, + .RasterizerState = rasterizer_desc, + .DepthStencilState = depth_stencil_desc, + .InputLayout = {.pInputElementDescs = input_element_desc, .NumElements = static_cast(std::ranges::size(input_element_desc))}, + .IBStripCutValue = D3D12_INDEX_BUFFER_STRIP_CUT_VALUE_DISABLED, + .PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE, + .NumRenderTargets = 1, + .RTVFormats = {DXGI_FORMAT_R8G8B8A8_UNORM}, + .DSVFormat = {}, + .SampleDesc = {.Count = 1, .Quality = 0}, + .NodeMask = 1, + .CachedPSO = {.pCachedBlob = nullptr, .CachedBlobSizeInBytes = 0}, + .Flags = D3D12_PIPELINE_STATE_FLAG_NONE + }; + + return check_hr_error(device_->CreateGraphicsPipelineState(&pipeline_state_desc, IID_PPV_ARGS(pipeline_state_.GetAddressOf()))); + } + + auto RendererD3D12::create_srv_descriptor_heap(const UINT num) noexcept -> bool + { + const D3D12_DESCRIPTOR_HEAP_DESC desc{ + .Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, + .NumDescriptors = num, + .Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE, + .NodeMask = 0 + }; + return check_hr_error(device_->CreateDescriptorHeap(&desc, IID_PPV_ARGS(srv_descriptor_heap_.ReleaseAndGetAddressOf()))); + } + + auto RendererD3D12::upload_texture( + const std::size_t index, + const Texture::data_type& data, + const Texture::size_type size, + const bool record_resource + ) noexcept -> texture_id_type + { + constexpr D3D12_HEAP_PROPERTIES heap_properties{ + .Type = D3D12_HEAP_TYPE_DEFAULT, + .CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN, + .MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN, + .CreationNodeMask = 0, + .VisibleNodeMask = 0 + }; + + const D3D12_RESOURCE_DESC resource_desc{ + .Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D, + .Alignment = 0, + .Width = static_cast(size.width), + .Height = static_cast(size.height), + .DepthOrArraySize = 1, + .MipLevels = 1, + .Format = DXGI_FORMAT_R8G8B8A8_UNORM, + .SampleDesc = {.Count = 1, .Quality = 0}, + .Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN, + .Flags = D3D12_RESOURCE_FLAG_NONE + }; + + ComPtr texture_2d; + if (not check_hr_error( + device_->CreateCommittedResource( + &heap_properties, + D3D12_HEAP_FLAG_NONE, + &resource_desc, + D3D12_RESOURCE_STATE_COPY_DEST, + nullptr, + IID_PPV_ARGS(texture_2d.GetAddressOf()) + ) + )) + { + return invalid_texture_id; + } + + const auto upload_pitch = (static_cast(size.width * 4) + D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1u) & ~(D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1u); + const auto upload_size = static_cast(size.height) * upload_pitch; + + constexpr D3D12_HEAP_PROPERTIES upload_heap_properties{ + .Type = D3D12_HEAP_TYPE_UPLOAD, + .CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN, + .MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN, + .CreationNodeMask = 0, + .VisibleNodeMask = 0 + }; + + const D3D12_RESOURCE_DESC upload_resource_desc{ + .Dimension = D3D12_RESOURCE_DIMENSION_BUFFER, + .Alignment = 0, + .Width = static_cast(upload_size), + .Height = 1, + .DepthOrArraySize = 1, + .MipLevels = 1, + .Format = DXGI_FORMAT_UNKNOWN, + .SampleDesc = {.Count = 1, .Quality = 0}, + .Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR, + .Flags = D3D12_RESOURCE_FLAG_NONE + }; + + ComPtr upload_buffer; + check_hr_error( + device_->CreateCommittedResource( + &upload_heap_properties, + D3D12_HEAP_FLAG_NONE, + &upload_resource_desc, + D3D12_RESOURCE_STATE_GENERIC_READ, + nullptr, + IID_PPV_ARGS(upload_buffer.GetAddressOf()) + ) + ); + + void* mapped_data = nullptr; + const D3D12_RANGE range{.Begin = 0, .End = upload_size}; + if (not check_hr_error(upload_buffer->Map(0, &range, &mapped_data))) + { + return invalid_texture_id; + } + for (UINT i = 0; i < static_cast(size.height); ++i) + { + auto* dest = static_cast(mapped_data) + static_cast(upload_pitch * i); + auto* source = reinterpret_cast(data.get()) + static_cast(size.width * i * 4); + const auto length = size.width * 4; + std::memcpy(dest, source, length); + } + upload_buffer->Unmap(0, &range); + + const D3D12_TEXTURE_COPY_LOCATION source_location{ + .pResource = upload_buffer.Get(), + .Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT, + .PlacedFootprint = + { + .Offset = 0, + .Footprint = + { + .Format = DXGI_FORMAT_R8G8B8A8_UNORM, + .Width = static_cast(size.width), + .Height = static_cast(size.height), + .Depth = 1, + .RowPitch = upload_pitch + } + } + }; + + const D3D12_TEXTURE_COPY_LOCATION dest_location{ + .pResource = texture_2d.Get(), + .Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX, + .SubresourceIndex = 0 + }; + + const D3D12_RESOURCE_BARRIER barrier{ + .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, + .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, + .Transition = + { + .pResource = texture_2d.Get(), + .Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, + .StateBefore = D3D12_RESOURCE_STATE_COPY_DEST, + .StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE + } + }; + + ComPtr command_allocator; + if (not check_hr_error( + device_->CreateCommandAllocator( + D3D12_COMMAND_LIST_TYPE_DIRECT, + IID_PPV_ARGS(command_allocator.GetAddressOf()) + ) + )) + { + return invalid_texture_id; + } + + ComPtr command_list; + if (not check_hr_error( + device_->CreateCommandList( + 0, + D3D12_COMMAND_LIST_TYPE_DIRECT, + command_allocator.Get(), + nullptr, + IID_PPV_ARGS(command_list.GetAddressOf()) + ) + )) + { + return invalid_texture_id; + } + + command_list->CopyTextureRegion(&dest_location, 0, 0, 0, &source_location, nullptr); + command_list->ResourceBarrier(1, &barrier); + if (not check_hr_error(command_list->Close())) + { + return invalid_texture_id; + } + + constexpr D3D12_COMMAND_QUEUE_DESC command_queue_desc{ + .Type = D3D12_COMMAND_LIST_TYPE_DIRECT, + .Priority = 0, + .Flags = D3D12_COMMAND_QUEUE_FLAG_NONE, + .NodeMask = 1 + }; + + ComPtr command_queue; + if (not check_hr_error( + device_->CreateCommandQueue( + &command_queue_desc, + IID_PPV_ARGS(command_queue.GetAddressOf()) + ) + )) + { + return invalid_texture_id; + } + + ID3D12CommandList* command_lists[]{command_list.Get()}; + command_queue->ExecuteCommandLists(1, command_lists); + + ComPtr fence; + if (not check_hr_error( + device_->CreateFence( + 0, + D3D12_FENCE_FLAG_NONE, + IID_PPV_ARGS(fence.GetAddressOf()) + ) + )) + { + return invalid_texture_id; + } + + constexpr UINT64 fence_value = 1; + if (not check_hr_error(command_queue->Signal(fence.Get(), fence_value))) + { + return invalid_texture_id; + } + if (fence->GetCompletedValue() < fence_value) + { + HANDLE event = CreateEvent(nullptr, FALSE, FALSE, nullptr); + if (not check_hr_error(fence->SetEventOnCompletion(fence_value, event))) + { + return invalid_texture_id; + } + WaitForSingleObject(event, INFINITE); + CloseHandle(event); + } + + // Create texture view + const D3D12_SHADER_RESOURCE_VIEW_DESC resource_view_desc{ + .Format = DXGI_FORMAT_R8G8B8A8_UNORM, + .ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D, + .Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING, + .Texture2D = {.MostDetailedMip = 0, .MipLevels = resource_desc.MipLevels, .PlaneSlice = 0, .ResourceMinLODClamp = .0f} + }; + + const auto increment_size = device_->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); + + auto cpu_handle = srv_descriptor_heap_->GetCPUDescriptorHandleForHeapStart(); + auto gpu_handle = srv_descriptor_heap_->GetGPUDescriptorHandleForHeapStart(); + + cpu_handle.ptr += index * increment_size; + gpu_handle.ptr += index * increment_size; + + device_->CreateShaderResourceView(texture_2d.Get(), &resource_view_desc, cpu_handle); + + if (record_resource) + { + textures_[index] = {.resource = texture_2d, .cpu = cpu_handle, .gpu = gpu_handle}; + } + else + { + // ComPtr + texture_2d = nullptr; + } + + return gpu_handle_to_id(index); + } + + RendererD3D12::RendererD3D12() noexcept + : device_{nullptr}, + command_list_{nullptr}, + root_signature_{nullptr}, + pipeline_state_{nullptr}, + srv_descriptor_heap_{nullptr}, + srv_max_size_{8}, + // note: overflow(max + 1 => 0) + frame_resource_index_{(std::numeric_limits::max)()}, + frame_resource_{} {} + + RendererD3D12::RendererD3D12(ID3D12Device* device, ID3D12GraphicsCommandList* command_list) noexcept + : RendererD3D12{} + { + bind_device(device); + bind_command_list(command_list); + } + + RendererD3D12::RendererD3D12(ComPtr device, ComPtr command_list) noexcept + : RendererD3D12{} + { + bind_device(std::move(device)); + bind_command_list(std::move(command_list)); + } + + auto RendererD3D12::bind_device(ID3D12Device* device) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(device != nullptr); + device_ = device; + } + + auto RendererD3D12::bind_device(ComPtr device) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(device != nullptr); + device_ = std::move(device); + } + + auto RendererD3D12::bind_command_list(ID3D12GraphicsCommandList* command_list) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(command_list != nullptr); + command_list_ = command_list; + } + + auto RendererD3D12::bind_command_list(ComPtr command_list) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(command_list != nullptr); + command_list_ = std::move(command_list); + } + + auto RendererD3D12::construct() noexcept -> bool + { + if (not create_root_signature()) + { + return false; + } + if (not create_pipeline_state()) + { + return false; + } + if (not create_srv_descriptor_heap(srv_max_size_)) + { + return false; + } + textures_.resize(srv_max_size_); + + return true; + } + + auto RendererD3D12::destruct() noexcept -> void + { + // ComPtr + // command_list_->ClearState(pipeline_state_.Get()); + // std::ignore = command_list_->Close(); + + pipeline_state_ = nullptr; + root_signature_ = nullptr; + + std::ranges::for_each( + textures_, + [](auto& texture) noexcept -> void + { + // ComPtr + texture.resource = nullptr; + } + ); + textures_.clear(); + srv_descriptor_heap_ = nullptr; + + std::ranges::for_each( + std::ranges::subrange{frame_resource_, num_frames_in_flight}, + [](render_buffer_type& render_buffer) noexcept -> void + { + // ComPtr + render_buffer.vertex = nullptr; + render_buffer.index = nullptr; + } + ); + + command_list_ = nullptr; + device_ = nullptr; + } + + auto RendererD3D12::create_texture(const Texture::data_type& data, const Texture::size_type size) noexcept -> texture_id_type + { + auto it = std::ranges::find(textures_, nullptr, &texture_type::resource); + if (it == textures_.end()) + { + // full, expand heap + const auto old_size = srv_max_size_; + const auto new_size = static_cast(static_cast(srv_max_size_) * 1.5f); + + srv_max_size_ = new_size; + textures_.resize(srv_max_size_); + it = textures_.begin() + old_size; + + const auto old_heap = srv_descriptor_heap_; + + const auto create_srv_descriptor_heap_result = create_srv_descriptor_heap(srv_max_size_); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(create_srv_descriptor_heap_result); + + const auto source = old_heap->GetCPUDescriptorHandleForHeapStart(); + const auto dest = srv_descriptor_heap_->GetCPUDescriptorHandleForHeapStart(); + + device_->CopyDescriptorsSimple( + old_size, + dest, + source, + D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV + ); + + const auto increment_size = device_->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); + const auto new_cpu_begin = srv_descriptor_heap_->GetCPUDescriptorHandleForHeapStart().ptr; + const auto new_gpu_begin = srv_descriptor_heap_->GetGPUDescriptorHandleForHeapStart().ptr; + + for (std::size_t i = 0; i < old_size; ++i) + { + if (auto& [resource, cpu, gpu] = textures_[i]; resource != nullptr) + { + cpu.ptr = new_cpu_begin + i * increment_size; + gpu.ptr = new_gpu_begin + i * increment_size; + } + } + } + + const auto index = std::ranges::distance(textures_.begin(), it); + + return upload_texture(index, data, size); + } + + // todo + auto RendererD3D12::update_texture(texture_id_type id, std::span update_viewer) noexcept -> void + { + const auto index = id_to_gpu_handle(id); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(index < srv_max_size_); + + auto& texture = textures_[index]; + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(texture.resource != nullptr); + + const auto texture_desc = texture.resource->GetDesc(); + const auto texture_size = Texture::size_type{static_cast(texture_desc.Width), texture_desc.Height}; + + const auto upload_pitch = (static_cast(texture_size.width * 4) + D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1u) & ~(D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1u); + const auto upload_size = static_cast(texture_size.height) * upload_pitch; + + constexpr D3D12_HEAP_PROPERTIES upload_heap_properties + { + .Type = D3D12_HEAP_TYPE_UPLOAD, + .CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN, + .MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN, + .CreationNodeMask = 0, + .VisibleNodeMask = 0 + }; + + const D3D12_RESOURCE_DESC upload_resource_desc + { + .Dimension = D3D12_RESOURCE_DIMENSION_BUFFER, + .Alignment = 0, + .Width = static_cast(upload_size), + .Height = 1, + .DepthOrArraySize = 1, + .MipLevels = 1, + .Format = DXGI_FORMAT_UNKNOWN, + .SampleDesc = {.Count = 1, .Quality = 0}, + .Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR, + .Flags = D3D12_RESOURCE_FLAG_NONE + }; + + ComPtr upload_buffer; + check_hr_error( + device_->CreateCommittedResource( + &upload_heap_properties, + D3D12_HEAP_FLAG_NONE, + &upload_resource_desc, + D3D12_RESOURCE_STATE_GENERIC_READ, + nullptr, + IID_PPV_ARGS(upload_buffer.GetAddressOf()) + ) + ); + + void* mapped_data = nullptr; + D3D12_RANGE range{.Begin = 0, .End = upload_size}; + if (not check_hr_error(upload_buffer->Map(0, &range, &mapped_data))) + { + // todo: error handling + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + } + else + { + std::ranges::for_each( + update_viewer, + [ + this, + dest_begin = static_cast(mapped_data), + pitch = static_cast(upload_pitch / sizeof(Texture::element_type)) + ](const TextureViewer& viewer) noexcept -> void + { + const auto point = viewer.position(); + const auto size = viewer.size(); + + const auto y_offset = static_cast(point.y) * pitch; + const auto x_offset = point.x; + auto* dest = dest_begin + y_offset + x_offset; + + for (Texture::size_type::value_type y = 0; y < size.height; ++y) + { + const auto this_line_source = viewer.line(y); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(this_line_source.size() == size.width, "TextureViewer line size mismatch"); + + auto* this_line_dest = dest + static_cast(y) * pitch; + + std::ranges::copy(this_line_source, this_line_dest); + } + } + ); + + upload_buffer->Unmap(0, &range); + } + + D3D12_RESOURCE_BARRIER barrier_before + { + .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, + .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, + .Transition = { + .pResource = texture.resource.Get(), + .Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, + .StateBefore = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, + .StateAfter = D3D12_RESOURCE_STATE_COPY_DEST + } + }; + + ComPtr command_allocator; + if (not check_hr_error( + device_->CreateCommandAllocator( + D3D12_COMMAND_LIST_TYPE_DIRECT, + IID_PPV_ARGS(command_allocator.GetAddressOf()) + ) + )) + { + // todo: error handling + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + return; + } + + ComPtr command_list; + if (not check_hr_error( + device_->CreateCommandList( + 0, + D3D12_COMMAND_LIST_TYPE_DIRECT, + command_allocator.Get(), + nullptr, + IID_PPV_ARGS(command_list.GetAddressOf()) + ) + )) + { + // todo: error handling + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + return; + } + + command_list->ResourceBarrier(1, &barrier_before); + + for (const auto& viewer: update_viewer) + { + const auto point = viewer.position(); + const auto size = viewer.size(); + + D3D12_TEXTURE_COPY_LOCATION src_location + { + .pResource = upload_buffer.Get(), + .Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT, + .PlacedFootprint = { + .Offset = static_cast(upload_pitch * point.y + point.x * 4), + .Footprint = { + .Format = DXGI_FORMAT_R8G8B8A8_UNORM, + .Width = static_cast(size.width), + .Height = static_cast(size.height), + .Depth = 1, + .RowPitch = upload_pitch + } + } + }; + + D3D12_TEXTURE_COPY_LOCATION dst_location + { + .pResource = texture.resource.Get(), + .Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX, + .SubresourceIndex = 0 + }; + + D3D12_BOX src_box + { + .left = 0, + .top = 0, + .front = 0, + .right = static_cast(size.width), + .bottom = static_cast(size.height), + .back = 1 + }; + + command_list->CopyTextureRegion(&dst_location, point.x, point.y, 0, &src_location, &src_box); + } + + D3D12_RESOURCE_BARRIER barrier_after + { + .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, + .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, + .Transition = + { + .pResource = texture.resource.Get(), + .Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, + .StateBefore = D3D12_RESOURCE_STATE_COPY_DEST, + .StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE + } + }; + command_list->ResourceBarrier(1, &barrier_after); + + if (not check_hr_error(command_list->Close())) + { + // todo: error handling + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + return; + } + + constexpr D3D12_COMMAND_QUEUE_DESC command_queue_desc + { + .Type = D3D12_COMMAND_LIST_TYPE_DIRECT, + .Priority = 0, + .Flags = D3D12_COMMAND_QUEUE_FLAG_NONE, + .NodeMask = 1 + }; + + ComPtr command_queue; + if (not check_hr_error( + device_->CreateCommandQueue( + &command_queue_desc, + IID_PPV_ARGS(command_queue.GetAddressOf()) + ) + )) + { + // todo: error handling + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + return; + } + + ID3D12CommandList* command_lists[]{command_list.Get()}; + command_queue->ExecuteCommandLists(1, command_lists); + + ComPtr fence; + if (not check_hr_error( + device_->CreateFence( + 0, + D3D12_FENCE_FLAG_NONE, + IID_PPV_ARGS(fence.GetAddressOf()) + ) + )) + { + // todo: error handling + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + return; + } + + constexpr UINT64 fence_value = 1; + if (not check_hr_error(command_queue->Signal(fence.Get(), fence_value))) + { + // todo: error handling + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + return; + } + if (fence->GetCompletedValue() < fence_value) + { + HANDLE event = CreateEvent(nullptr, FALSE, FALSE, nullptr); + if (not check_hr_error(fence->SetEventOnCompletion(fence_value, event))) + { + // todo: error handling + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + return; + } + WaitForSingleObject(event, INFINITE); + CloseHandle(event); + } + } + + auto RendererD3D12::destroy_texture(const texture_id_type id) noexcept -> void + { + const auto index = id_to_gpu_handle(id); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(index < srv_max_size_); + + auto& texture = textures_[index]; + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(texture.resource != nullptr); + // ComPtr + texture.resource = nullptr; + } + + auto RendererD3D12::present(const render_data_list_type& render_data_list, const extent_type& display_size) noexcept -> void + { + const auto [total_vertex_count, total_index_count] = [&]() noexcept + { + struct sum + { + UINT vertex; + UINT index; + }; + + return std::ranges::fold_left( + render_data_list, + sum{.vertex = 0, .index = 0}, + [](const sum s, const RenderData& render_data) noexcept -> sum + { + const auto vertex_list = render_data.vertex_list.get(); + const auto index_list = render_data.index_list.get(); + + return {.vertex = s.vertex + static_cast(vertex_list.size()), .index = s.index + static_cast(index_list.size())}; + } + ); + }(); + + frame_resource_index_ += 1; + const auto this_frame_index = frame_resource_index_ % num_frames_in_flight; + auto& this_frame = frame_resource_[this_frame_index]; + auto& [this_frame_index_buffer, this_frame_index_count, this_frame_vertex_buffer, this_frame_vertex_count] = this_frame; + + constexpr D3D12_HEAP_PROPERTIES heap_properties{ + .Type = D3D12_HEAP_TYPE_UPLOAD, + .CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN, + .MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN, + .CreationNodeMask = 0, + .VisibleNodeMask = 0 + }; + // Create and grow vertex/index buffers if needed + if (this_frame_vertex_buffer == nullptr or this_frame_vertex_count < total_vertex_count) + { + // todo: grow factor + this_frame_vertex_count = total_vertex_count + 5000; + + const D3D12_RESOURCE_DESC resource_desc{ + .Dimension = D3D12_RESOURCE_DIMENSION_BUFFER, + .Alignment = 0, + .Width = this_frame_vertex_count * sizeof(vertex_type), + .Height = 1, + .DepthOrArraySize = 1, + .MipLevels = 1, + .Format = DXGI_FORMAT_UNKNOWN, + .SampleDesc = {.Count = 1, .Quality = 0}, + .Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR, + .Flags = D3D12_RESOURCE_FLAG_NONE + }; + check_hr_error( + device_->CreateCommittedResource( + &heap_properties, + D3D12_HEAP_FLAG_NONE, + &resource_desc, + D3D12_RESOURCE_STATE_GENERIC_READ, + nullptr, + IID_PPV_ARGS(this_frame_vertex_buffer.ReleaseAndGetAddressOf()) + ) + ); + } + if (this_frame_index_buffer == nullptr or this_frame_index_count < total_index_count) + { + // todo: grow factor + this_frame_index_count = total_index_count + 10000; + + const D3D12_RESOURCE_DESC resource_desc{ + .Dimension = D3D12_RESOURCE_DIMENSION_BUFFER, + .Alignment = 0, + .Width = this_frame_index_count * sizeof(index_type), + .Height = 1, + .DepthOrArraySize = 1, + .MipLevels = 1, + .Format = DXGI_FORMAT_UNKNOWN, + .SampleDesc = {.Count = 1, .Quality = 0}, + .Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR, + .Flags = D3D12_RESOURCE_FLAG_NONE + }; + check_hr_error( + device_->CreateCommittedResource( + &heap_properties, + D3D12_HEAP_FLAG_NONE, + &resource_desc, + D3D12_RESOURCE_STATE_GENERIC_READ, + nullptr, + IID_PPV_ARGS(this_frame_index_buffer.ReleaseAndGetAddressOf()) + ) + ); + } + + // Upload vertex/index data into a single contiguous GPU buffer + { + void* mapped_vertex_resource; + void* mapped_index_resource; + constexpr D3D12_RANGE vertex_range{.Begin = 0, .End = 0}; + constexpr D3D12_RANGE index_range{.Begin = 0, .End = 0}; + check_hr_error(this_frame_vertex_buffer->Map(0, &vertex_range, &mapped_vertex_resource)); + check_hr_error(this_frame_index_buffer->Map(0, &index_range, &mapped_index_resource)); + + auto* mapped_vertex = static_cast(mapped_vertex_resource); + auto* mapped_index = static_cast(mapped_index_resource); + + UINT vertex_offset = 0; + UINT index_offset = 0; + + std::ranges::for_each( + render_data_list, + [&](const RenderData& render_data) noexcept -> void + { + const auto vertex_list = render_data.vertex_list.get(); + const auto index_list = render_data.index_list.get(); + + // std::ranges::transform( + // vertex_list, + // mapped_vertex + vertex_offset, + // [](const vertex_type& vertex) noexcept -> vertex_type + // { + // // return { + // // .position = {vertex.position.x, vertex.position.y}, + // // .uv = {vertex.uv.x, vertex.uv.y}, + // // .color = vertex.color.to(primitive::color_format) + // // }; + // return std::bit_cast(vertex); + // } + // ); + std::ranges::copy(vertex_list, mapped_vertex + vertex_offset); + // std::ranges::transform( + // index_list, + // mapped_index + index_offset, + // [vertex_offset](const index_type index) noexcept -> index_type + // { + // return static_cast(index + vertex_offset); + // } + // ); + std::ranges::copy(index_list, mapped_index + index_offset); + + vertex_offset += static_cast(vertex_list.size()); + index_offset += static_cast(index_list.size()); + } + ); + + this_frame_vertex_buffer->Unmap(0, &vertex_range); + this_frame_index_buffer->Unmap(0, &index_range); + } + + // Setup orthographic projection matrix into our constant buffer + projection_matrix_type projection_matrix; + { + constexpr auto left = 0.f; + const auto right = display_size.width; + constexpr auto top = 0.f; + const auto bottom = display_size.height; + + const float mvp[4][4]{ + {2.0f / (right - left), 0.0f, 0.0f, 0.0f}, + {0.0f, 2.0f / (top - bottom), 0.0f, 0.0f}, + {0.0f, 0.0f, 0.5f, 0.0f}, + {(right + left) / (left - right), (top + bottom) / (bottom - top), 0.5f, 1.0f}, + }; + static_assert(sizeof(mvp) == sizeof(projection_matrix_type)); + std::memcpy(projection_matrix, mvp, sizeof(projection_matrix_type)); + } + + // Setup viewport + { + const D3D12_VIEWPORT viewport{ + .TopLeftX = .0f, + .TopLeftY = .0f, + .Width = static_cast(display_size.width), + .Height = static_cast(display_size.height), + .MinDepth = .0f, + .MaxDepth = 1.f + }; + command_list_->RSSetViewports(1, &viewport); + } + + // Bind shader/vertex buffers, root signature and pipeline state + { + ID3D12DescriptorHeap* descriptor_heaps[]{srv_descriptor_heap_.Get()}; + command_list_->SetDescriptorHeaps(1, descriptor_heaps); + + command_list_->SetGraphicsRootSignature(root_signature_.Get()); + command_list_->SetGraphicsRoot32BitConstants(0, sizeof(projection_matrix_type) / sizeof(float), &projection_matrix, 0); + + command_list_->SetPipelineState(pipeline_state_.Get()); + + const D3D12_VERTEX_BUFFER_VIEW vertex_buffer_view{ + .BufferLocation = this_frame_vertex_buffer->GetGPUVirtualAddress(), + .SizeInBytes = this_frame_vertex_count * static_cast(sizeof(vertex_type)), + .StrideInBytes = sizeof(vertex_type) + }; + command_list_->IASetVertexBuffers(0, 1, &vertex_buffer_view); + + const D3D12_INDEX_BUFFER_VIEW index_buffer_view{ + .BufferLocation = this_frame_index_buffer->GetGPUVirtualAddress(), + .SizeInBytes = this_frame_index_count * static_cast(sizeof(index_type)), + // ReSharper disable once CppUnreachableCode + .Format = sizeof(index_type) == 2 ? DXGI_FORMAT_R16_UINT : DXGI_FORMAT_R32_UINT + }; + command_list_->IASetIndexBuffer(&index_buffer_view); + command_list_->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + } + + // Setup blend factor + constexpr float blend_factor[4]{.0f, .0f, .0f, .0f}; + command_list_->OMSetBlendFactor(blend_factor); + + UINT total_index_offset = 0; + std::ranges::for_each( + render_data_list, + [this, &total_index_offset](const RenderData& render_data) noexcept -> void + { + const auto vertex_list = render_data.vertex_list.get(); + const auto index_list = render_data.index_list.get(); + + for (const auto& command_list = render_data.command_list.get(); + const auto& [clip_rect, texture_id, index_offset, element_count]: command_list) + { + const auto [point, extent] = clip_rect; + const D3D12_RECT rect + { + static_cast(point.x), + static_cast(point.y), + static_cast(point.x + extent.width), + static_cast(point.y + extent.height) + }; + command_list_->RSSetScissorRects(1, &rect); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(texture_id != invalid_texture_id); + const auto index = id_to_gpu_handle(texture_id); + const auto& texture = textures_[index]; + command_list_->SetGraphicsRootDescriptorTable(1, texture.gpu); + + const auto this_index_offset = static_cast(total_index_offset + index_offset); + command_list_->DrawIndexedInstanced(static_cast(element_count), 1, this_index_offset, 0, 0); + } + + total_index_offset += static_cast(index_list.size()); + } + ); + } +} diff --git a/src/gfx/extension/renderer_d3d12.hpp b/src/gfx/extension/renderer_d3d12.hpp new file mode 100644 index 00000000..ee46f76a --- /dev/null +++ b/src/gfx/extension/renderer_d3d12.hpp @@ -0,0 +1,78 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include + +#include +#include + +namespace gal::prometheus::gfx::extension +{ + using Microsoft::WRL::ComPtr; + + class RendererD3D12 final : public Renderer + { + public: + struct texture_type + { + ComPtr resource; + D3D12_CPU_DESCRIPTOR_HANDLE cpu; + D3D12_GPU_DESCRIPTOR_HANDLE gpu; + }; + + using textures_type = std::vector; + + private: + struct render_buffer_type + { + ComPtr index; + UINT index_count; + ComPtr vertex; + UINT vertex_count; + }; + + ComPtr device_; + ComPtr command_list_; + + ComPtr root_signature_; + ComPtr pipeline_state_; + ComPtr srv_descriptor_heap_; + // 8 descriptors are enough, and if not, then 1.5 times more + UINT srv_max_size_; + + textures_type textures_; + + constexpr static UINT num_frames_in_flight = 3; + UINT frame_resource_index_; + render_buffer_type frame_resource_[num_frames_in_flight]; + + [[nodiscard]] auto create_root_signature() noexcept -> bool; + [[nodiscard]] auto create_pipeline_state() noexcept -> bool; + [[nodiscard]] auto create_srv_descriptor_heap(UINT num) noexcept -> bool; + + [[nodiscard]] auto upload_texture(std::size_t index, const Texture::data_type& data, Texture::size_type size, bool record_resource = true) noexcept -> texture_id_type; + + public: + RendererD3D12() noexcept; + RendererD3D12(ID3D12Device* device, ID3D12GraphicsCommandList* command_list) noexcept; + RendererD3D12(ComPtr device, ComPtr command_list) noexcept; + + auto bind_device(ID3D12Device* device) noexcept -> void; + auto bind_device(ComPtr device) noexcept -> void; + auto bind_command_list(ID3D12GraphicsCommandList* command_list) noexcept -> void; + auto bind_command_list(ComPtr command_list) noexcept -> void; + + auto construct() noexcept -> bool; + auto destruct() noexcept -> void; + + auto create_texture(const Texture::data_type& data, Texture::size_type size) noexcept -> texture_id_type override; + auto update_texture(texture_id_type id, std::span update_viewer) noexcept -> void override; + auto destroy_texture(texture_id_type id) noexcept -> void override; + + auto present(const render_data_list_type& render_data_list, const extent_type& display_size) noexcept -> void override; + }; +} diff --git a/src/draw/draw.hpp b/src/gfx/gfx.hpp similarity index 54% rename from src/draw/draw.hpp rename to src/gfx/gfx.hpp index a30a8629..3c2bce76 100644 --- a/src/draw/draw.hpp +++ b/src/gfx/gfx.hpp @@ -5,10 +5,9 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include diff --git a/src/gfx/glyph.cpp b/src/gfx/glyph.cpp new file mode 100644 index 00000000..31d93d3e --- /dev/null +++ b/src/gfx/glyph.cpp @@ -0,0 +1,11 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +namespace gal::prometheus::gfx +{ + GlyphParser::~GlyphParser() noexcept = default; +} diff --git a/src/gfx/glyph.hpp b/src/gfx/glyph.hpp new file mode 100644 index 00000000..f2956c35 --- /dev/null +++ b/src/gfx/glyph.hpp @@ -0,0 +1,111 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include + +#include + +#include + +namespace gal::prometheus::gfx +{ + enum class FontLoadResult : std::uint8_t + { + SUCCESS = 0, + + FILE_ALREADY_LOADED = 1 << 0, + FILE_NOT_FOUND = 1 << 1, + INVALID_FONT_FORMAT = 1 << 2, + OUT_OF_MEMORY = 1 << 3, + + UNKNOWN_ERROR = 1 << 7, + }; + + enum class GlyphFlag : std::uint8_t + { + NONE = 0, + BOLD = 1 << 0, + ITALIC = 1 << 1, + }; + + class GlyphCode final + { + public: + std::uint32_t codepoint; + std::uint32_t size; + GlyphFlag flag; + + constexpr GlyphCode(const std::uint32_t codepoint, const std::uint32_t size, const GlyphFlag flag) noexcept + : codepoint{codepoint}, + size{size}, + flag{flag} {} + }; + + class GlyphDescriptor final + { + public: + // The offset of the bitmap may be negative (signed) + using rect_type = primitive::basic_rect_2d; + // Texture::data_type data; + using data_type = std::unique_ptr; + + // Bitmap infos of this glyph + rect_type rect; + float advance_x; + + bool visible; + bool colored; + + // note: + // - visible: We always assume that the data length of @c data is not less than @c rect.size.width * @c rect.size.height + // - invisible: nullptr (1.currently loaded font does not contain the specified codepoint, 2.it only affects the layout and does not need to be rendered, e.g. line breaks) + data_type data; + }; + + class GlyphParser + { + public: + GlyphParser(const GlyphParser&) noexcept = delete; + GlyphParser(GlyphParser&&) noexcept = default; + auto operator=(const GlyphParser&) noexcept -> GlyphParser& = delete; + auto operator=(GlyphParser&&) noexcept -> GlyphParser& = default; + + virtual ~GlyphParser() noexcept; + + protected: + GlyphParser() noexcept = default; + + public: + /** + * @brief Load font data from @c path, return id of font + * @param path Font path + * @return id of the font, or invalid_font_id if failed to load + */ + [[nodiscard]] virtual auto load(std::string_view path) noexcept -> FontLoadResult = 0; + + /** + * @brief Determines whether the target font contains the glyphs of the specified codepoint + * @param codepoint The codepoint of the glyph to be checked + * @return Exists or not + */ + [[nodiscard]] virtual auto has_glyph(std::uint32_t codepoint) const noexcept -> bool = 0; + + /** + * @brief Get the glyph information of the specified size (and style) of the target codepoint + * @param code {codepoint, size, flag} + * @return The glyph information of the specified size (and style) of the target codepoint + */ + [[nodiscard]] virtual auto parse(const GlyphCode& code) noexcept -> GlyphDescriptor = 0; + }; +} + +// ReSharper disable once CppRedundantNamespaceDefinition +namespace gal::prometheus::meta::user_defined +{ + template<> + struct enum_is_flag : std::true_type {}; +} // namespace gal::prometheus::meta::user_defined diff --git a/src/gfx/internal/context.cpp b/src/gfx/internal/context.cpp new file mode 100644 index 00000000..2b95a7c6 --- /dev/null +++ b/src/gfx/internal/context.cpp @@ -0,0 +1,92 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +#include + +namespace gal::prometheus::gfx +{ + Context::Context(Context&&) noexcept = default; + + auto Context::operator=(Context&&) noexcept -> Context& = default; + + Context::~Context() noexcept = default; + + Context::Context() noexcept + : glyph_parser{nullptr}, + renderer{nullptr}, + private_{memory::make_unique()} + { + // ReSharper disable once CppUseStructuredBinding + auto& context_private = *private_; + auto& glyph_context = context_private.glyph_context; + + glyph_context.glyph_parser = &glyph_parser; + } + + auto Context::initialize() noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(glyph_parser != nullptr); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(renderer != nullptr); + + // ReSharper disable once CppUseStructuredBinding + auto& context_private = *private_; + auto& texture_context = context_private.texture_context; + + texture_context.initialize(render_list_shared_data); + } + + auto Context::new_render_list() noexcept -> RenderList& + { + RenderList render_list{*this}; + + return render_lists.emplace_back(std::move(render_list)); + } + + auto Context::new_frame() noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(glyph_parser != nullptr); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(renderer != nullptr); + + // ReSharper disable once CppUseStructuredBinding + auto& context_private = *private_; + auto& texture_context = context_private.texture_context; + + texture_context.update_all_atlas(*renderer); + } + + auto Context::present(const extent_type& display_size) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(glyph_parser != nullptr); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(renderer != nullptr); + + // ReSharper disable once CppUseStructuredBinding + auto& context_private = *private_; + auto& texture_context = context_private.texture_context; + auto& glyph_context = context_private.glyph_context; + + glyph_context.upload_all_glyph(texture_context); + + render_data_list_type all_render_data{}; + all_render_data.reserve(render_lists.size()); + + std::ranges::transform( + render_lists, + std::back_inserter(all_render_data), + &RenderList::data + ); + + renderer->present(all_render_data, display_size); + } + + auto Context::end_frame() noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(glyph_parser != nullptr); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(renderer != nullptr); + + render_lists.clear(); + } +} diff --git a/src/gfx/internal/context.hpp b/src/gfx/internal/context.hpp new file mode 100644 index 00000000..e9e10aaa --- /dev/null +++ b/src/gfx/internal/context.hpp @@ -0,0 +1,21 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include + +#include +#include + +namespace gal::prometheus::gfx +{ + class Context::ContextPrivate final + { + public: + TextureContext texture_context; + GlyphContext glyph_context; + }; +} diff --git a/src/gfx/internal/glyph.cpp b/src/gfx/internal/glyph.cpp new file mode 100644 index 00000000..85120b2a --- /dev/null +++ b/src/gfx/internal/glyph.cpp @@ -0,0 +1,103 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE + +namespace gal::prometheus::gfx +{ + auto GlyphUploadQueue::push(GlyphInfo& info, Texture::data_type&& data) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(data != nullptr); + + list_.emplace_back(memory::ref(info), std::move(data)); + } + + auto GlyphUploadQueue::upload(TextureContext& context) noexcept -> void + { + std::ranges::for_each( + list_, + [&context](element_type& element) noexcept -> void + { + auto& info = element.info.get(); + + const auto size = info.rect.size(); + + auto [writer, texture_atlas_id] = context.write(size); + const auto& texture = context.select_texture(texture_atlas_id); + const auto uv_scale = texture.uv_scale; + + const Texture::data_view_type data_view{element.data.get(), static_cast(size.width) * size.height}; + writer.fill(data_view); + + info.texture_atlas_id = texture_atlas_id; + info.uv.point = writer.position().to() * uv_scale; + info.uv.extent = size.to() * uv_scale; + } + ); + list_.clear(); + } + + auto GlyphContext::glyph_of(const GlyphKey& key) noexcept -> const GlyphInfo& + { + if (const auto& it = cached_glyphs_.find(key); it != cached_glyphs_.end()) + { + return it->second; + } + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(glyph_parser != nullptr); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(*glyph_parser != nullptr); + + auto [rect, advance_x, visible, colored, data] = (*glyph_parser)->parse(key); + + GlyphInfo info{}; + info.rect = rect; + info.advance_x = advance_x; + info.visible = visible; + info.colored = colored; + + auto [it, inserted] = cached_glyphs_.emplace(key, info); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(inserted); + + auto& inserted_info = it->second; + + if (visible) + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(data != nullptr); + + upload_queue_.push(inserted_info, std::move(data)); + } + + return inserted_info; + } + + auto GlyphContext::glyph_of(const std::uint32_t codepoint, const std::uint32_t size, const GlyphFlag flag) noexcept -> const GlyphInfo& + { + return this->glyph_of({codepoint, size, flag}); + } + + auto GlyphContext::glyph_of(std::u32string_view text, std::uint32_t size, GlyphFlag flag) noexcept -> std::vector> + { + std::vector> infos; + infos.reserve(text.size()); + + std::ranges::for_each( + text, + [&infos, this, size, flag](const auto codepoint) noexcept -> void + { + const auto& info = this->glyph_of(codepoint, size, flag); + infos.emplace_back(std::cref(info)); + } + ); + + return infos; + } + + auto GlyphContext::upload_all_glyph(TextureContext& context) noexcept -> void + { + upload_queue_.upload(context); + } +} diff --git a/src/gfx/internal/glyph.hpp b/src/gfx/internal/glyph.hpp new file mode 100644 index 00000000..603c3bb0 --- /dev/null +++ b/src/gfx/internal/glyph.hpp @@ -0,0 +1,153 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include + +#include + +#include + +#include +#include + +namespace gal::prometheus::gfx +{ + static_assert(std::is_same_v); + + /** + * @brief This class is essentially the same as @c GlyphParser::GlyphCode, but takes up less memory space. + */ + class GlyphKey final + { + public: + std::uint32_t codepoint; + std::uint32_t size : 26; + std::uint32_t flag : 6; + + constexpr GlyphKey(const std::uint32_t codepoint, const std::uint32_t size, const GlyphFlag flag) noexcept + : codepoint{codepoint}, + size{size}, + flag{static_cast(flag)} {} + + [[nodiscard]] constexpr auto operator==(const GlyphKey& other) const noexcept -> bool + { + return codepoint == other.codepoint and size == other.size and flag == other.flag; + } + + [[nodiscard]] constexpr explicit(false) operator GlyphCode() const noexcept + { + return {codepoint, size, static_cast(flag)}; + } + + struct hasher + { + [[nodiscard]] auto operator()(const GlyphKey& key) const noexcept -> std::size_t + { + return std::hash{}(key.codepoint) ^ std::hash{}(key.size) ^ std::hash{}(static_cast(key.flag)); + } + }; + }; + + static_assert(sizeof(GlyphKey) == sizeof(std::uint32_t) + sizeof(std::uint32_t)); + + /** + * @brief Glyph info (bitmap info & texture info) + */ + class GlyphInfo final + { + public: + using rect_type = GlyphDescriptor::rect_type; + using uv_type = primitive::basic_rect_2d; + + // ============= + // Data filled when loading glyph + // ============= + + // Bitmap infos of this glyph + rect_type rect; + float advance_x; + + bool visible; + bool colored; + + // ============= + // Data filled when writing texture (if and only if the glyph is visible) + // ============= + + // The id of the texture atlas where the glyph is located + // This id is present if and only if the glyph is in a texture atlas, otherwise it is invalid_texture_atlas_id + texture_atlas_id_type texture_atlas_id{invalid_texture_atlas_id}; + // The uv coordinate of the glyph in the texture atlas + uv_type uv{-1, -1, -1, -1}; + }; + + /** + * @brief Queue of glyph data to be uploaded to the texture + */ + class GlyphUploadQueue final + { + public: + struct element_type + { + memory::RefWrapper info; + + Texture::data_type data; + }; + + using list_type = std::vector; + + private: + list_type list_; + + public: + GlyphUploadQueue(const GlyphUploadQueue&) noexcept = delete; + GlyphUploadQueue(GlyphUploadQueue&&) noexcept = default; + auto operator=(const GlyphUploadQueue&) noexcept -> GlyphUploadQueue& = delete; + auto operator=(GlyphUploadQueue&&) noexcept -> GlyphUploadQueue& = default; + + ~GlyphUploadQueue() noexcept = default; + + GlyphUploadQueue() noexcept = default; + + auto push(GlyphInfo& info, Texture::data_type&& data) noexcept -> void; + + auto upload(TextureContext& context) noexcept -> void; + }; + + class GlyphContext final + { + public: + using cached_glyphs_type = std::unordered_map; + + GlyphParser** glyph_parser; + + private: + cached_glyphs_type cached_glyphs_; + + GlyphUploadQueue upload_queue_; + + public: + GlyphContext(const GlyphContext&) noexcept = delete; + GlyphContext(GlyphContext&&) noexcept = default; + auto operator=(const GlyphContext&) noexcept -> GlyphContext& = delete; + auto operator=(GlyphContext&&) noexcept -> GlyphContext& = default; + + ~GlyphContext() noexcept = default; + + GlyphContext() noexcept = default; + + [[nodiscard]] auto glyph_of(const GlyphKey& key) noexcept -> const GlyphInfo&; + [[nodiscard]] auto glyph_of(std::uint32_t codepoint, std::uint32_t size, GlyphFlag flag) noexcept -> const GlyphInfo&; + [[nodiscard]] auto glyph_of(std::u32string_view text, std::uint32_t size, GlyphFlag flag) noexcept -> std::vector>; + + /** + * @brief Upload all used glyphs to the texture (if it is not already uploaded) + * @note This function is usually called every frame (unless all the needed glyphs have been uploaded to the texture, but it can still be called) to upload all new (previously unused) glyphs to the texture + */ + auto upload_all_glyph(TextureContext& context) noexcept -> void; + }; +} diff --git a/src/gfx/internal/rect_pack.cpp b/src/gfx/internal/rect_pack.cpp new file mode 100644 index 00000000..57969762 --- /dev/null +++ b/src/gfx/internal/rect_pack.cpp @@ -0,0 +1,339 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +#include +#include + +#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE + +namespace gal::prometheus::gfx +{ + auto RectPackContext::nodes_count() const noexcept -> std::size_t + { + return nodes_.size() - 2; + } + + auto RectPackContext::align_of(const PackPrefer pack_prefer) const noexcept -> std::uint32_t + { + if (pack_prefer == PackPrefer::FAST_FAIL) + { + return static_cast((static_cast(size_.width) + nodes_count() - 1) / nodes_count()); + } + + return 1; + } + + auto RectPackContext::skyline_find_min_y(const rect_pack_node* head, const point_type::value_type x0, const size_type::value_type width) noexcept -> find_y_result + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(head->point.x <= x0); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(head->next->point.x > x0); + + const auto x1 = x0 + width; + const auto* current = head; + + find_y_result result{.y = 0, .waste = 0}; + size_type::value_type visited_width = 0; + + while (current->point.x < x1) + { + if (current->point.y > result.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 visited + result.waste += static_cast(visited_width) * (current->point.y - result.y); + result.y = current->point.y; + + // the first time through, visited_width might be reduced + visited_width += current->next->point.x - std::ranges::max(x0, current->point.x); + } + else + { + // add waste area + const auto under_width = std::ranges::min(current->next->point.x - current->point.x, width - visited_width); + + result.waste += static_cast(under_width) * (result.y - current->point.y); + visited_width += under_width; + } + + current = current->next; + } + + return result; + } + + auto RectPackContext::skyline_find_best_pos(size_type size, const PackPrefer pack_prefer, const Heuristic heuristic) noexcept -> find_result + { + const auto& context_size = size_; + + // align to multiple of 'align' + const auto align = align_of(pack_prefer); + size.width = ((size.width + align - 1) / align) * align; + + // if it can't possibly fit, bail immediately + if (size.width > context_size.width or size.height > context_size.height) + { + return {.point = {0, 0}, .prev_link = nullptr}; + } + + auto best_waste = std::numeric_limits::max(); + auto best_y = std::numeric_limits::max(); + + rect_pack_node** best = nullptr; + { + auto* current = active_head_; + auto** prev = &active_head_; + + while (current->point.x + size.width <= context_size.width) + { + const auto [min_y, waste] = skyline_find_min_y(current, current->point.x, size.width); + + if (heuristic == Heuristic::SKYLINE_BOTTOM_LEFT) + { + if (min_y < best_y) + { + best_y = min_y; + best = prev; + } + } + else if (heuristic == Heuristic::SKYLINE_BEST_FIT) + { + // best-fit + if (min_y + size.height <= context_size.height) + { + // can only use it if it first vertically + if (min_y < best_y or (min_y == best_y and waste < best_waste)) + { + best_y = min_y; + best_waste = waste; + best = prev; + } + } + } + else + { + GAL_PROMETHEUS_COMPILER_UNREACHABLE(); + } + + prev = ¤t->next; + current = current->next; + } + } + + auto best_x = best == nullptr ? 0 : (*best)->point.x; + + // 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 + + if (heuristic == Heuristic::SKYLINE_BEST_FIT) + { + const auto* tail = active_head_; + auto* current = active_head_; + auto** prev = ¤t; + + // find first node that's admissible + while (tail->point.x < size.width) + { + tail = tail->next; + } + + while (tail) + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(tail->point.x >= size.width); + const auto x = tail->point.x - size.width; + + // find the left position that matches this + while (current->next->point.x <= x) + { + prev = ¤t->next; + current = current->next; + } + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current->next->point.x > x and current->point.x <= x); + + if (const auto [min_y, waste] = skyline_find_min_y(current, x, size.width); + min_y + size.height <= context_size.height) + { + if (min_y <= best_y) + { + if (min_y < best_y or waste < best_waste or (waste == best_waste and x < best_x)) + { + best_x = x; + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(min_y <= best_y); + best_y = min_y; + best_waste = waste; + best = prev; + } + } + } + + tail = tail->next; + } + } + + return {.point = {best_x, best_y}, .prev_link = best}; + } + + auto RectPackContext::skyline_pack_rectangle(const size_type size, const PackPrefer pack_prefer, const Heuristic heuristic) noexcept -> find_result + { + const auto context_size = size_; + + // find best position according to heuristic + auto result = skyline_find_best_pos(size, pack_prefer, heuristic); + + // 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 (result.prev_link == nullptr or result.point.y + size.height > context_size.height or free_head_ == nullptr) + { + return {.point = result.point, .prev_link = nullptr}; + } + + // on success, create new node + auto* head = free_head_; + head->point = {result.point.x, result.point.y + size.height}; + free_head_ = head->next; + + // insert the new node into the right starting point, + // and let 'current' point to the remaining nodes needing to be stitched back in + auto* current = *result.prev_link; + if (current->point.x < result.point.x) + { + // preserve the existing one, so start testing with the next one + auto* next = current->next; + current->next = head; + current = next; + } + else + { + *result.prev_link = head; + } + + // from here, traverse current and free the nodes, until we get to one + // that shouldn't be freed + while (current->next and current->next->point.x <= result.point.x + size.width) + { + auto* next = current->next; + // move the current node to the free list + current->next = free_head_; + free_head_ = current; + current = next; + } + + // stitch the list back in + head->next = current; + + current->point.x = std::ranges::max(current->point.x, result.point.x + size.width); + +#if GAL_PROMETHEUS_COMPILER_DEBUG + current = active_head_; + while (current->point.x < context_size.width) + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current->point.x < current->next->point.x); + current = current->next; + } + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current->next == nullptr); + + std::size_t count = 0; + for (current = active_head_; current; current = current->next, count += 1) {} + for (current = free_head_; current; current = current->next, count += 1) {} + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(count == nodes_count() + 2); +#endif + + return result; + } + + RectPackContext::RectPackContext(const size_type& size) noexcept + : size_{size}, + nodes_{size.width + 2}, + free_head_{nodes_.data()}, + active_head_{nodes_.data() + nodes_count()} + { + for (auto [index, node]: nodes_ | std::views::take(nodes_count() - 1) | std::views::enumerate) + { + node.next = &nodes_[index + 1]; + } + nodes_[nodes_count() - 1].next = nullptr; + + // node 0 is the full width, node 1 is the sentinel (lets us not store width explicitly) + active_head_[0] = {.point = {0, 0}, .next = &active_head_[1]}; + active_head_[1] = {.point = {size_.width, std::numeric_limits::max()}, .next = nullptr}; + } + + auto RectPackContext::pack( + std::span in_out_rects, + const PackPrefer pack_prefer, + const Heuristic heuristic + ) noexcept -> bool + { + std::vector sorted_rects{}; + sorted_rects.reserve(in_out_rects.size()); + + std::ranges::transform( + in_out_rects, + std::back_inserter(sorted_rects), + [](rect_type& rect) noexcept -> rect_type* + { + return std::addressof(rect); + } + ); + // sort according to heuristic + std::ranges::sort( + sorted_rects, + [](const rect_type* r1, const rect_type* r2) noexcept -> bool + { + if (r1->size.height != r2->size.height) + { + return r1->size.height > r2->size.height; + } + + return r1->size.width > r2->size.width; + } + ); + + std::ranges::for_each( + sorted_rects, + [this, pack_prefer, heuristic](rect_type* rect) noexcept -> void + { + if (rect->size.width == 0 or rect->size.height == 0) + { + // empty rect needs no space + rect->point = {0, 0}; + } + else + { + if (const auto [point, prev_link] = skyline_pack_rectangle(rect->size, pack_prefer, heuristic); prev_link) + { + rect->point = point; + } + else + { + rect->point = invalid_point; + } + } + } + ); + + return std::ranges::all_of(in_out_rects, &rect_type::packed); + } +} diff --git a/src/gfx/internal/rect_pack.hpp b/src/gfx/internal/rect_pack.hpp new file mode 100644 index 00000000..e575208d --- /dev/null +++ b/src/gfx/internal/rect_pack.hpp @@ -0,0 +1,104 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +// Base on: stb_rect_pack.h - v1.01 - public domain - rectangle packing Sean Barrett 2014 + +#pragma once + +#include +#include + +#include +#include + +namespace gal::prometheus::gfx +{ + enum class PackPrefer : std::uint8_t + { + FAST_FAIL = 0, + SKIP = 1, + + DEFAULT = FAST_FAIL, + }; + + enum class Heuristic : std::uint8_t + { + SKYLINE_BOTTOM_LEFT = 0, + SKYLINE_BEST_FIT = 1, + + DEFAULT = SKYLINE_BOTTOM_LEFT, + }; + + class RectPackContext final + { + public: + using point_type = primitive::basic_point_2d; + using size_type = primitive::basic_extent_2d; + + constexpr static auto invalid_point = point_type{std::numeric_limits::max(), std::numeric_limits::max()}; + + struct rect_type final + { + // INPUT + size_type size; + + // OUTPUT + point_type point; + + [[nodiscard]] constexpr auto packed() const noexcept -> bool + { + return point != invalid_point; + } + }; + + private: + struct rect_pack_node final + { + point_type point; + rect_pack_node* next; + }; + + struct find_y_result + { + point_type::value_type y; + size_type::value_type waste; + }; + + struct find_result + { + point_type point; + rect_pack_node** prev_link; + }; + + size_type size_; + + // size_.width + 2 + std::vector nodes_; + // size_.width + rect_pack_node* free_head_; + // 2 + rect_pack_node* active_head_; + + [[nodiscard]] auto nodes_count() const noexcept -> std::size_t; + + [[nodiscard]] auto align_of(PackPrefer pack_prefer) const noexcept -> std::uint32_t; + + // find minimum y position if it starts at x1 + [[nodiscard]] static auto skyline_find_min_y(const rect_pack_node* head, point_type::value_type x0, size_type::value_type width) noexcept -> find_y_result; + + [[nodiscard]] auto skyline_find_best_pos(size_type size, PackPrefer pack_prefer, Heuristic heuristic) noexcept -> find_result; + + [[nodiscard]] auto skyline_pack_rectangle(size_type size, PackPrefer pack_prefer, Heuristic heuristic) noexcept -> find_result; + + public: + explicit RectPackContext(const size_type& size) noexcept; + + auto pack( + std::span in_out_rects, + PackPrefer pack_prefer = PackPrefer::DEFAULT, + Heuristic heuristic = Heuristic::DEFAULT + ) noexcept -> bool; + }; +} diff --git a/src/gfx/internal/texture.cpp b/src/gfx/internal/texture.cpp new file mode 100644 index 00000000..7ec53ac5 --- /dev/null +++ b/src/gfx/internal/texture.cpp @@ -0,0 +1,219 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include +#include +#include + +#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE + +namespace +{ + using namespace gal::prometheus; + using namespace gfx; + + [[nodiscard]] auto make_texture(const Texture::size_type size) noexcept -> Texture + { + auto data = std::make_unique_for_overwrite(static_cast(size.width) * size.height); + + return { + .data = std::move(data), + .size = size, + .uv_scale = {1.f / static_cast(size.width), 1.f / static_cast(size.height)}, + .id = invalid_texture_id + }; + } +} + +namespace gal::prometheus::gfx +{ + auto TextureContext::root_id() const noexcept -> texture_atlas_id_type + { + std::ignore = this; + return 0; + } + + auto TextureContext::active_id() const noexcept -> texture_atlas_id_type + { + return static_cast(atlas_list_.size() - 1); + } + + auto TextureContext::select_atlas(const texture_atlas_id_type texture_atlas_id) noexcept -> atlas_type& + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(texture_atlas_id < atlas_list_.size()); + + return atlas_list_[texture_atlas_id]; + } + + auto TextureContext::select_atlas(const texture_atlas_id_type texture_atlas_id) const noexcept -> const atlas_type& + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(texture_atlas_id < atlas_list_.size()); + + return atlas_list_[texture_atlas_id]; + } + + auto TextureContext::new_atlas(const size_type size) noexcept -> atlas_type& + { + atlas_type atlas + { + .texture = make_texture(size), + .pending_update_data = {}, + .rp_context = gfx::RectPackContext{size}, + }; + return atlas_list_.emplace_back(std::move(atlas)); + } + + auto TextureContext::write(const texture_atlas_id_type texture_atlas_id, const size_type size) noexcept -> TextureWriter + { + auto& [texture, pending_data_list, rp_context] = select_atlas(texture_atlas_id); + + gfx::RectPackContext::rect_type new_rect{.size = size, .point = {}}; + if (rp_context.pack({&new_rect, 1})) + { + const auto point = new_rect.point; + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(point != gfx::RectPackContext::invalid_point); + + auto* address = texture.data.get() + (point.y * size.width + point.x); + const auto mapping = TextureWriter::borrow_data_type::mapping_type{ + std::dextents{size.height, size.width}, + std::array{texture.size.width, 1}, + }; + const auto data = TextureWriter::borrow_data_type{address, mapping}; + + TextureViewer viewer{point, data}; + pending_data_list.emplace_back(viewer); + + return {point, data}; + } + + return {TextureWriter::invalid_point, {}}; + } + + TextureContext::TextureContext() noexcept + { + atlas_list_.reserve(2); + + // root atlas + constexpr size_type root_texture_atlas_size{128, 128}; + new_atlas(root_texture_atlas_size); + + // first + constexpr size_type first_texture_atlas_size{2048, 2048}; + new_atlas(first_texture_atlas_size); + } + + auto TextureContext::initialize(RenderListSharedData& shared_data) noexcept -> void + { + const auto atlas_id = root_id(); + const auto& texture = select_texture(atlas_id); + + // ======================================== + // BAKE LINES (AA) + // ======================================== + { + constexpr std::uint32_t white_color = 0xff'ff'ff'ff; + constexpr auto aa_width = static_cast(RenderListSharedData::baked_line_uv_count); + constexpr auto aa_height = static_cast(RenderListSharedData::baked_line_uv_count); + constexpr auto aa_size = size_type{aa_width, aa_height}; + + // baked line rect area: + // white pixel + ◿ + const auto texture_write = write(atlas_id, aa_size); + texture_write.fill(0); + + const auto aa_point = texture_write.position(); + const auto aa_uv_scale = texture.uv_scale; + + // white pixel + { + // LINE 0, 2 pixels + texture_write.fill(0, 2, white_color); + // LINE 1, 2 pixels + texture_write.fill(1, 2, white_color); + + const auto uv_x = static_cast(static_cast(aa_point.x) + 1.0f) * aa_uv_scale.width; + const auto uv_y = static_cast(static_cast(aa_point.y) + 1.0f) * aa_uv_scale.height; + + shared_data.white_pixel_uv = {uv_x, uv_y}; + } + + // ◿ + for (size_type::value_type y = 1; y < aa_height; ++y) + { + const auto line_width = y; + const auto offset = aa_width - line_width; + + texture_write.fill(y, offset, line_width, white_color); + + const auto p_x = aa_point.x + offset; + const auto p_y = aa_point.y + y; + const auto width = line_width; + constexpr auto height = .5f; + + const auto uv_x = static_cast(p_x) * aa_uv_scale.width; + const auto uv_y = static_cast(p_y) * aa_uv_scale.height; + const auto uv_width = static_cast(width) * aa_uv_scale.width; + const auto uv_height = static_cast(height) * aa_uv_scale.height; + + shared_data.baked_line_uvs[y] = {uv_x, uv_y, uv_width, uv_height}; + } + } + + // ======================================== + // + // ======================================== + { + std::ignore = texture; + } + } + + auto TextureContext::update_all_atlas(Renderer& renderer) noexcept -> void + { + std::ranges::for_each( + atlas_list_, + [&renderer](auto& atlas) noexcept -> void + { + if (atlas.texture.id == invalid_texture_id) + { + atlas.texture.id = renderer.create_texture(atlas.texture.data, atlas.texture.size); + } + else if (not atlas.pending_update_data.empty()) + { + renderer.update_texture(atlas.texture.id, atlas.pending_update_data); + + atlas.pending_update_data.clear(); + } + } + ); + } + + auto TextureContext::root_texture() noexcept -> Texture& + { + return select_texture(root_id()); + } + + auto TextureContext::root_texture() const noexcept -> const Texture& + { + return select_texture(root_id()); + } + + auto TextureContext::select_texture(const texture_atlas_id_type texture_atlas_id) noexcept -> Texture& + { + return select_atlas(texture_atlas_id).texture; + } + + auto TextureContext::select_texture(const texture_atlas_id_type texture_atlas_id) const noexcept -> const Texture& + { + return select_atlas(texture_atlas_id).texture; + } + + auto TextureContext::write(const size_type size) noexcept -> write_result + { + const auto id = active_id(); + const auto writer = write(id, size); + + return {.writer = writer, .texture_atlas_id = id}; + } +} diff --git a/src/gfx/internal/texture.hpp b/src/gfx/internal/texture.hpp new file mode 100644 index 00000000..8e39a6e9 --- /dev/null +++ b/src/gfx/internal/texture.hpp @@ -0,0 +1,110 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include + +#include + +#include + +namespace gal::prometheus::gfx +{ + // index + using texture_atlas_id_type = std::uint32_t; + constexpr texture_atlas_id_type invalid_texture_atlas_id{std::numeric_limits::max()}; + + class RenderListSharedData; + class Renderer; + + class TextureContext final + { + public: + using element_type = Texture::element_type; + using data_type = Texture::data_type; + using data_view_type = Texture::data_view_type; + + using point_type = Texture::point_type; + using size_type = Texture::size_type; + + static_assert(std::is_same_v); + static_assert(std::is_same_v); + + using uv_scale_type = Texture::uv_scale_type; + + struct atlas_type + { + Texture texture; + std::vector pending_update_data; + + gfx::RectPackContext rp_context; + }; + + using atlas_list_type = std::vector; + + private: + atlas_list_type atlas_list_; + + [[nodiscard]] auto root_id() const noexcept -> texture_atlas_id_type; + + [[nodiscard]] auto active_id() const noexcept -> texture_atlas_id_type; + + [[nodiscard]] auto select_atlas(texture_atlas_id_type texture_atlas_id) noexcept -> atlas_type&; + + [[nodiscard]] auto select_atlas(texture_atlas_id_type texture_atlas_id) const noexcept -> const atlas_type&; + + auto new_atlas(size_type size) noexcept -> atlas_type&; + + [[nodiscard]] auto write(texture_atlas_id_type texture_atlas_id, size_type size) noexcept -> TextureWriter; + + public: + TextureContext(const TextureContext&) noexcept = delete; + TextureContext(TextureContext&&) noexcept = default; + auto operator=(const TextureContext&) noexcept -> TextureContext& = delete; + auto operator=(TextureContext&&) noexcept -> TextureContext& = default; + + ~TextureContext() noexcept = default; + + TextureContext() noexcept; + + auto initialize(RenderListSharedData& shared_data) noexcept -> void; + + /** + * @brief Upload all texture atlas if it didn't upload, or update them if it dirty + * @note This function is usually called every frame to upload the texture to the GPU, or to update the texture (if new glyph data is written) + */ + auto update_all_atlas(Renderer& renderer) noexcept -> void; + + /** + * @brief Get root (default) texture + */ + [[nodiscard]] auto root_texture() noexcept -> Texture&; + + /** + * @brief Get root (default) texture + */ + [[nodiscard]] auto root_texture() const noexcept -> const Texture&; + + /** + * @brief Get texture of id + */ + [[nodiscard]] auto select_texture(texture_atlas_id_type texture_atlas_id) noexcept -> Texture&; + + /** + * @brief Get texture of id + */ + [[nodiscard]] auto select_texture(texture_atlas_id_type texture_atlas_id) const noexcept -> const Texture&; + + struct write_result + { + TextureWriter writer; + + texture_atlas_id_type texture_atlas_id; + }; + + [[nodiscard]] auto write(size_type size) noexcept -> write_result; + }; +} diff --git a/src/gfx/old/context.cpp b/src/gfx/old/context.cpp new file mode 100644 index 00000000..c79af73f --- /dev/null +++ b/src/gfx/old/context.cpp @@ -0,0 +1,462 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +#include + +// #include +#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE + +namespace gal::prometheus::gfx +{ + auto TextureContext::root_atlas() noexcept -> Texture& + { + return texture_atlases_.front(); + } + + auto TextureContext::root_atlas() const noexcept -> const Texture& + { + return texture_atlases_.front(); + } + + auto TextureContext::select_atlas(const texture_atlas_id_type id) noexcept -> Texture& + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(id < texture_atlases_.size()); + + return texture_atlases_[id]; + } + + auto TextureContext::select_atlas(const texture_atlas_id_type id) const noexcept -> const Texture& + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(id < texture_atlases_.size()); + + return texture_atlases_[id]; + } + + auto TextureContext::select_atlas(const Texture::size_type size) const noexcept -> texture_atlas_id_type + { + // width/height == 0 ==> whitespace + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(size.width >= 0 and size.height >= 0); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not texture_atlases_.empty()); + + std::ignore = size; + + // todo: root only? + return 0; + } + + auto TextureContext::make_territory(const Texture::size_type size) noexcept -> BorrowTexture + { + // width/height == 0 ==> whitespace + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(size.width >= 0 and size.height >= 0); + + const auto atlas_id = select_atlas(size); + auto& atlas = select_atlas(atlas_id); + + auto texture = atlas.select(size); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(texture.valid()); + + Territory territory{.id = atlas_id, .point = texture.point(), .size = size}; + territories_.emplace_back(territory); + + return texture; + } + + TextureContext::TextureContext() noexcept + { + // root atlas + constexpr Texture::size_type root_texture_atlas_size{2048, 2048}; + texture_atlases_.emplace_back(root_texture_atlas_size); + } + + auto TextureContext::initialize(RenderListSharedData& shared_data) noexcept -> void + { + // ======================================== + // BAKE LINES (AA) + // ======================================== + { + constexpr std::uint32_t white_color = 0xff'ff'ff'ff; + constexpr auto aa_width = static_cast(RenderListSharedData::baked_line_uv_count); + constexpr auto aa_height = static_cast(RenderListSharedData::baked_line_uv_count); + constexpr auto aa_size = Texture::size_type{aa_width, aa_height}; + + const auto atlas_id = select_atlas(aa_size); + auto& atlas = select_atlas(atlas_id); + + const auto atlas_uv_scale = atlas.uv(); + + const auto& aa_texture = make_territory(aa_size); + const auto aa_point = aa_texture.point(); + + // baked line rect area: + // white pixel + // ◿ + aa_texture.fill(0); + + // white pixel + { + aa_texture[0, 0] = white_color; + aa_texture[1, 0] = white_color; + aa_texture[0, 1] = white_color; + aa_texture[1, 1] = white_color; + + const auto uv_x = static_cast(static_cast(aa_point.x) + .5f) * atlas_uv_scale.width; + const auto uv_y = static_cast(static_cast(aa_point.y) + .5f) * atlas_uv_scale.height; + + shared_data.white_pixel_uv = {uv_x, uv_y}; + } + + // ◿ + for (Texture::size_type::value_type y = 1; y < aa_height; ++y) + { + const auto line_width = y; + const auto offset = aa_width - line_width; + + aa_texture.fill(y, offset, line_width, white_color); + + const auto p_x = aa_point.x + offset; + const auto p_y = aa_point.y + y; + const auto width = line_width; + constexpr auto height = .5f; + + const auto uv_x = static_cast(p_x) * atlas_uv_scale.width; + const auto uv_y = static_cast(p_y) * atlas_uv_scale.height; + const auto uv_width = static_cast(width) * atlas_uv_scale.width; + const auto uv_height = static_cast(height) * atlas_uv_scale.height; + + shared_data.baked_line_uvs[y] = {uv_x, uv_y, uv_width, uv_height}; + } + } + + // // ======================================== + // // set fallback glyph + // // ======================================== + // fonts_.set_fallback_glyph(); + } + + auto TextureContext::root_texture() const noexcept -> texture_id_type + { + return root_atlas().id(); + } + + auto TextureContext::atlas_of(const GlyphInfo& info) const noexcept -> const Texture& + { + return select_atlas(info.texture_atlas_id); + } + + auto TextureContext::bind_parser(GlyphParser& parser) noexcept -> void + { + fonts_.bind_parser(parser); + } + + auto TextureContext::add_font(const std::filesystem::path& path) noexcept -> bool + { + return fonts_.add_font(path); + } + + auto TextureContext::load_all_font() noexcept -> void + { + fonts_.load_all_font(); + } + + auto TextureContext::set_fallback_glyph() noexcept -> void + { + fonts_.set_fallback_glyph(); + } + + auto TextureContext::set_fallback_glyph(const GlyphKey& key) noexcept -> void + { + fonts_.set_fallback_glyph(key); + } + + auto TextureContext::set_fallback_glyph(const std::uint32_t codepoint, const std::uint32_t size, const GlyphFlag flag) noexcept -> void + { + fonts_.set_fallback_glyph(codepoint, size, flag); + } + + auto TextureContext::glyph_of(const GlyphKey& key) noexcept -> const GlyphInfo* + { + return fonts_.glyph_of(key); + } + + auto TextureContext::glyph_of(const std::uint32_t codepoint, const std::uint32_t size, const GlyphFlag flag) noexcept -> const GlyphInfo* + { + return fonts_.glyph_of(codepoint, size, flag); + } + + auto TextureContext::glyph_of(const std::u32string_view text, const std::uint32_t size, const GlyphFlag flag) noexcept -> std::vector + { + return fonts_.glyph_of(text, size, flag); + } + + // auto TextureContext::glyph_of(const std::string_view text, const std::uint32_t size, const GlyphFlag flag) noexcept -> std::vector + // { + // const auto utf32_text = chars::convert(text); + // + // return this->glyph_of(utf32_text, size, flag); + // } + + auto TextureContext::glyph_of_or_fallback(const GlyphKey& key) const noexcept -> const GlyphInfo& + { + return fonts_.glyph_of_or_fallback(key); + } + + auto TextureContext::glyph_of_or_fallback(const std::uint32_t codepoint, const std::uint32_t size, const GlyphFlag flag) const noexcept -> const GlyphInfo& + { + return fonts_.glyph_of_or_fallback(codepoint, size, flag); + } + + auto TextureContext::glyph_of_or_fallback(const std::u32string_view text, const std::uint32_t size, const GlyphFlag flag) const noexcept -> std::vector> + { + return fonts_.glyph_of_or_fallback(text, size, flag); + } + + // auto TextureContext::size_of(const std::uint32_t codepoint, const std::uint32_t size, const GlyphFlag flag) noexcept -> extent_type + // { + // const auto* info = this->glyph_of(codepoint, size, flag); + // + // if (info == nullptr) + // { + // return {0, 0}; + // } + // + // return {info->advance_x, info->rect.height()}; + // } + + // auto TextureContext::size_of(const std::u32string_view text, const std::uint32_t size, const GlyphFlag flag) noexcept -> extent_type + // { + // const auto infos = this->glyph_of(text, size, flag); + // + // return std::ranges::fold_left( + // infos, + // extent_type{0, 0}, + // [](const extent_type total, const auto* info) noexcept -> extent_type + // { + // if (info == nullptr) + // { + // return total; + // } + // + // return {total.width + info->advance_x, std::ranges::max(total.height, info->rect.height())}; + // } + // ); + // } + // + // auto TextureContext::size_of(const std::string_view text, const std::uint32_t size, const GlyphFlag flag) noexcept -> extent_type + // { + // const auto infos = glyph_of(text, size, flag); + // + // return std::ranges::fold_left( + // infos, + // extent_type{0, 0}, + // [](const extent_type total, const auto* info) noexcept -> extent_type + // { + // if (info == nullptr) + // { + // return total; + // } + // + // return {total.width + info->advance_x, std::ranges::max(total.height, info->rect.height())}; + // } + // ); + // } + + auto TextureContext::load_all_glyph() noexcept -> void + { + fonts_.load_all_glyph(*this); + } + + auto TextureContext::upload_parsed_info_to_texture(const GlyphParser::ParseResult& result) noexcept -> parsed_info_upload_result_type + { + const auto width = static_cast(result.rect.width()); + const auto height = static_cast(result.rect.height()); + const auto size = Texture::size_type{width, height}; + + const auto atlas_id = select_atlas(size); + const auto& atlas = select_atlas(atlas_id); + + const auto atlas_uv_scale = atlas.uv(); + + const auto& texture = make_territory(size); + texture.fill({result.data.get(), static_cast(width) * height}); + + const auto texture_point = texture.point(); + return { + .texture_atlas_id = atlas_id, + .uv = {texture_point.to() * atlas_uv_scale, size.to() * atlas_uv_scale} + }; + } + + auto TextureContext::upload_all_texture(Renderer& renderer) noexcept -> void + { + std::ranges::for_each( + texture_atlases_, + [&renderer](auto& texture) noexcept -> void + { + if (not texture.uploaded()) + { + texture.create(renderer); + } + else + { + texture.upload_if_required(renderer); + } + } + ); + } + + RenderContext::~RenderContext() noexcept = default; + + RenderContext::RenderContext() noexcept = default; + + auto RenderContext::initialize() noexcept -> void + { + texture_context_.initialize(render_list_shared_data_); + } + + auto RenderContext::begin_frame(Renderer& renderer) noexcept -> void + { + texture_context_.load_all_font(); + texture_context_.upload_all_texture(renderer); + } + + auto RenderContext::end_frame(Renderer& renderer) noexcept -> void + { + std::ignore = renderer; + texture_context_.load_all_glyph(); + } + + auto RenderContext::root_texture() const noexcept -> texture_id_type + { + return texture_context_.root_texture(); + } + + auto RenderContext::atlas_of(const GlyphInfo& info) const noexcept -> const Texture& + { + return texture_context_.atlas_of(info); + } + + auto RenderContext::bind_parser(GlyphParser& parser) noexcept -> void + { + texture_context_.bind_parser(parser); + } + + auto RenderContext::add_font(const std::filesystem::path& path) noexcept -> bool + { + return texture_context_.add_font(path); + } + + auto RenderContext::load_all_font() noexcept -> void + { + texture_context_.load_all_font(); + } + + auto RenderContext::set_fallback_glyph() noexcept -> void + { + texture_context_.set_fallback_glyph(); + } + + auto RenderContext::set_fallback_glyph(const GlyphKey& key) noexcept -> void + { + texture_context_.set_fallback_glyph(key); + } + + auto RenderContext::set_fallback_glyph(const std::uint32_t codepoint, const std::uint32_t size, const GlyphFlag flag) noexcept -> void + { + texture_context_.set_fallback_glyph(codepoint, size, flag); + } + + auto RenderContext::glyph_of(const GlyphKey& key) noexcept -> const GlyphInfo* + { + return texture_context_.glyph_of(key); + } + + auto RenderContext::glyph_of(const std::uint32_t codepoint, const std::uint32_t size, const GlyphFlag flag) noexcept -> const GlyphInfo* + { + return texture_context_.glyph_of(codepoint, size, flag); + } + + auto RenderContext::glyph_of(const std::u32string_view text, const std::uint32_t size, const GlyphFlag flag) noexcept -> std::vector + { + return texture_context_.glyph_of(text, size, flag); + } + + // auto RenderContext::glyph_of(const std::string_view text, const std::uint32_t size, const GlyphFlag flag) noexcept -> std::vector + // { + // return texture_context_.glyph_of(text, size, flag); + // } + + auto RenderContext::glyph_of_or_fallback(const GlyphKey& key) const noexcept -> const GlyphInfo& + { + return texture_context_.glyph_of_or_fallback(key); + } + + auto RenderContext::glyph_of_or_fallback(const std::uint32_t codepoint, const std::uint32_t size, const GlyphFlag flag) const noexcept -> const GlyphInfo& + { + return texture_context_.glyph_of_or_fallback(codepoint, size, flag); + } + + auto RenderContext::glyph_of_or_fallback(const std::u32string_view text, const std::uint32_t size, const GlyphFlag flag) const noexcept -> std::vector> + { + return texture_context_.glyph_of_or_fallback(text, size, flag); + } + + // auto RenderContext::size_of(const std::uint32_t codepoint, const std::uint32_t size, const GlyphFlag flag) noexcept -> extent_type + // { + // return texture_context_.size_of(codepoint, size, flag); + // } + // + // auto RenderContext::size_of(const std::u32string_view text, const std::uint32_t size, const GlyphFlag flag) noexcept -> extent_type + // { + // return texture_context_.size_of(text, size, flag); + // } + // + // auto RenderContext::size_of(const std::string_view text, const std::uint32_t size, const GlyphFlag flag) noexcept -> extent_type + // { + // return texture_context_.size_of(text, size, flag); + // } + + auto RenderContext::load_all_glyph() noexcept -> void + { + texture_context_.load_all_glyph(); + } + + auto RenderContext::upload_all_texture(Renderer& renderer) noexcept -> void + { + texture_context_.upload_all_texture(renderer); + } + + auto RenderContext::render_list_shared_data() const noexcept -> const RenderListSharedData& + { + return render_list_shared_data_; + } + + auto RenderContext::render_data() const noexcept -> std::vector + { + std::vector all_render_data{}; + all_render_data.reserve(render_lists_.size()); + + std::ranges::for_each( + render_lists_, + [&all_render_data](const auto& render_list) noexcept -> void + { + all_render_data.emplace_back(render_list.render_data()); + } + ); + + return all_render_data; + } + + auto RenderContext::test_render_list() noexcept -> RenderList& + { + if (render_lists_.empty()) + { + render_lists_.emplace_back(RenderListFlag::ANTI_ALIASED_LINE | RenderListFlag::ANTI_ALIASED_LINE_USE_TEXTURE | RenderListFlag::ANTI_ALIASED_FILL, *this); + } + + return render_lists_.front(); + } +} // namespace gal::prometheus::gfx diff --git a/src/gfx/old/context.hpp b/src/gfx/old/context.hpp new file mode 100644 index 00000000..a30543c6 --- /dev/null +++ b/src/gfx/old/context.hpp @@ -0,0 +1,340 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include + +#include +#include +#include +#include + +namespace gal::prometheus::gfx +{ + class TextureContext final + { + public: + using texture_atlases_type = std::vector; + + private: + class Territory final + { + public: + using point_type = Texture::point_type; + using size_type = Texture::size_type; + + texture_atlas_id_type id{invalid_texture_atlas_id}; + + point_type point{0, 0}; + size_type size{0, 0}; + }; + + using territories_type = std::vector; + + texture_atlases_type texture_atlases_; + territories_type territories_; + + Fonts fonts_; + + /** + * @brief Retain at least one texture atlas (root) + */ + [[nodiscard]] auto root_atlas() noexcept -> Texture&; + + /** + * @brief Retain at least one texture atlas (root) + */ + [[nodiscard]] auto root_atlas() const noexcept -> const Texture&; + + /** + * @brief Get the texture atlas for the specified id + * @param id Texture atlas id + * @return Texture atlas + */ + [[nodiscard]] auto select_atlas(texture_atlas_id_type id) noexcept -> Texture&; + + /** + * @brief Get the texture atlas for the specified id + * @param id Texture atlas id + * @return Texture atlas + */ + [[nodiscard]] auto select_atlas(texture_atlas_id_type id) const noexcept -> const Texture&; + + /** + * @brief Gets a texture atlas large enough to hold the specified @c size sub texture + * @param size sub texture size + * @return Texture atlas id + */ + [[nodiscard]] auto select_atlas(Texture::size_type size) const noexcept -> texture_atlas_id_type; + + /** + * @brief Find a suitable texture atlas based on @c size, and then find a suitable region on it (for writing sub texture) + * @param size Sub texture size + * @return Region on the texture atlas + */ + auto make_territory(Texture::size_type size) noexcept -> BorrowTexture; + + public: + TextureContext(const TextureContext&) noexcept = delete; + TextureContext(TextureContext&&) noexcept = default; + auto operator=(const TextureContext&) noexcept -> TextureContext& = delete; + auto operator=(TextureContext&&) noexcept -> TextureContext& = default; + ~TextureContext() noexcept = default; + + TextureContext() noexcept; + + /** + * @brief Initialization, usually used to set up RenderListSharedData's anti-aliased lines (uv) and initialize all font faces + */ + auto initialize(RenderListSharedData& shared_data) noexcept -> void; + + /** + * @brief Get root (default) texture + */ + [[nodiscard]] auto root_texture() const noexcept -> texture_id_type; + + /** + * @brief Get texture atlas for glyph information + */ + [[nodiscard]] auto atlas_of(const GlyphInfo& info) const noexcept -> const Texture&; + + /** + * @brief Bind parser, default parser is null pointer, must bind parser before loading fonts + */ + auto bind_parser(GlyphParser& parser) noexcept -> void; + + /** + * @brief Load fonts from the specified path, assuming the path is a valid font file + * @param path Font path + * @return Returns true if the file exists and was opened successfully (without checking if it is a valid font file), otherwise returns false + */ + auto add_font(const std::filesystem::path& path) noexcept -> bool; + + /** + * @brief Load the fonts previously added by @c add_font + * @note This function is usually called at initialization time (or at every frame if needed) to load all the required fonts + */ + auto load_all_font() noexcept -> void; + + auto set_fallback_glyph() noexcept -> void; + + /** + * @brief Set the fallback glyph, if we can't find the glyph of the specified codepoint, then use the fallback glyph + * @param key {codepoint, size, flag} + */ + auto set_fallback_glyph(const GlyphKey& key) noexcept -> void; + + /** + * @brief Set the fallback glyph, if we can't find the glyph of the specified codepoint, then use the fallback glyph + */ + auto set_fallback_glyph(std::uint32_t codepoint, std::uint32_t size, GlyphFlag flag) noexcept -> void; + + [[nodiscard]] auto glyph_of(const GlyphKey& key) noexcept -> const GlyphInfo*; + [[nodiscard]] auto glyph_of(std::uint32_t codepoint, std::uint32_t size, GlyphFlag flag = GlyphFlag::NONE) noexcept -> const GlyphInfo*; + [[nodiscard]] auto glyph_of(std::u32string_view text, std::uint32_t size, GlyphFlag flag = GlyphFlag::NONE) noexcept -> std::vector; + // [[nodiscard]] auto glyph_of(std::string_view text, std::uint32_t size, GlyphFlag flag = GlyphFlag::NONE) noexcept -> std::vector; + + [[nodiscard]] auto glyph_of_or_fallback(const GlyphKey& key) const noexcept -> const GlyphInfo&; + [[nodiscard]] auto glyph_of_or_fallback(std::uint32_t codepoint, std::uint32_t size, GlyphFlag flag = GlyphFlag::NONE) const noexcept -> const GlyphInfo&; + [[nodiscard]] auto glyph_of_or_fallback(std::u32string_view text, std::uint32_t size, GlyphFlag flag = GlyphFlag::NONE) const noexcept -> std::vector>; + // [[nodiscard]] auto glyph_of_or_fallback(std::string_view text, std::uint32_t size, GlyphFlag flag = GlyphFlag::NONE) const noexcept -> std::vector>; + + // /** + // * @brief The minimum space to be occupied if the specified codepoint is to be rendered in its entirety + // * @param codepoint + // * @param size + // * @param flag + // * @return + // */ + // [[nodiscard]] auto size_of(std::uint32_t codepoint, std::uint32_t size, GlyphFlag flag = GlyphFlag::NONE) noexcept -> extent_type; + // + // /** + // * @brief The minimum space to be occupied if the specified text is to be rendered in its entirety + // * @param text + // * @param size + // * @param flag + // * @return + // */ + // [[nodiscard]] auto size_of(std::u32string_view text, std::uint32_t size, GlyphFlag flag = GlyphFlag::NONE) noexcept -> extent_type; + // + // /** + // * @brief The minimum space to be occupied if the specified text is to be rendered in its entirety + // * @param text + // * @param size + // * @param flag + // * @return + // */ + // [[nodiscard]] auto size_of(std::string_view text, std::uint32_t size, GlyphFlag flag = GlyphFlag::NONE) noexcept -> extent_type; + + /** + * @brief Upload all used glyphs to the texture (if it is not already uploaded) + * @note This function is usually called every frame (unless all the needed glyphs have been uploaded to the texture, but it can still be called) to upload all new (previously unused) glyphs to the texture + */ + auto load_all_glyph() noexcept -> void; + + struct parsed_info_upload_result_type + { + texture_atlas_id_type texture_atlas_id; + GlyphInfo::uv_type uv; + }; + + /** + * @brief Write Font uploaded glyph data to texture + * @note @c FontGlyphQueue calls this function to upload glyph data to the texture and set its texture atlas ID and UV coordinates + */ + auto upload_parsed_info_to_texture(const GlyphParser::ParseResult& result) noexcept -> parsed_info_upload_result_type; + + /** + * @brief Upload all texture atlas (if it didn't upload or needs to be re-uploaded) + * @note This function is usually called every frame to upload the texture to the GPU, or to update the texture (if new glyph data is written) + */ + auto upload_all_texture(Renderer& renderer) noexcept -> void; + }; + + class RenderContext final + { + public: + using render_lists_type = std::vector; + + private: + TextureContext texture_context_; + + RenderListSharedData render_list_shared_data_; + + render_lists_type render_lists_; + + public: + RenderContext(const RenderContext&) noexcept = delete; + RenderContext(RenderContext&&) noexcept = default; + auto operator=(const RenderContext&) noexcept -> RenderContext& = delete; + auto operator=(RenderContext&&) noexcept -> RenderContext& = default; + + ~RenderContext() noexcept; + + RenderContext() noexcept; + + // ==================================================================== + // RenderContext + // ==================================================================== + + /** + * @brief Initialize RenderContext, called only once + */ + auto initialize() noexcept -> void; + + auto begin_frame(Renderer& renderer) noexcept -> void; + + auto end_frame(Renderer& renderer) noexcept -> void; + + // ==================================================================== + // TextureContext + // ==================================================================== + + /** + * @brief Get root (default) texture + */ + [[nodiscard]] auto root_texture() const noexcept -> texture_id_type; + + /** + * @brief Get texture atlas for glyph information + */ + [[nodiscard]] auto atlas_of(const GlyphInfo& info) const noexcept -> const Texture&; + + /** + * @brief Bind parser, default parser is null pointer, must bind parser before loading fonts + */ + auto bind_parser(GlyphParser& parser) noexcept -> void; + + /** + * @brief Load fonts from the specified path, assuming the path is a valid font file + * @param path Font path + */ + auto add_font(const std::filesystem::path& path) noexcept -> bool; + + /** + * @brief Load the fonts previously added by @c add_font + * @note This function is usually called at initialization time (or at every frame if needed) to load all the required fonts + */ + auto load_all_font() noexcept -> void; + + auto set_fallback_glyph() noexcept -> void; + + /** + * @brief Set the fallback glyph, if we can't find the glyph of the specified codepoint, then use the fallback glyph + * @param key {codepoint, size, flag} + */ + auto set_fallback_glyph(const GlyphKey& key) noexcept -> void; + + /** + * @brief Set the fallback glyph, if we can't find the glyph of the specified codepoint, then use the fallback glyph + */ + auto set_fallback_glyph(std::uint32_t codepoint, std::uint32_t size, GlyphFlag flag) noexcept -> void; + + [[nodiscard]] auto glyph_of(const GlyphKey& key) noexcept -> const GlyphInfo*; + [[nodiscard]] auto glyph_of(std::uint32_t codepoint, std::uint32_t size, GlyphFlag flag = GlyphFlag::NONE) noexcept -> const GlyphInfo*; + [[nodiscard]] auto glyph_of(std::u32string_view text, std::uint32_t size, GlyphFlag flag = GlyphFlag::NONE) noexcept -> std::vector; + // [[nodiscard]] auto glyph_of(std::string_view text, std::uint32_t size, GlyphFlag flag = GlyphFlag::NONE) noexcept -> std::vector; + + [[nodiscard]] auto glyph_of_or_fallback(const GlyphKey& key) const noexcept -> const GlyphInfo&; + [[nodiscard]] auto glyph_of_or_fallback(std::uint32_t codepoint, std::uint32_t size, GlyphFlag flag = GlyphFlag::NONE) const noexcept -> const GlyphInfo&; + [[nodiscard]] auto glyph_of_or_fallback(std::u32string_view text, std::uint32_t size, GlyphFlag flag = GlyphFlag::NONE) const noexcept -> std::vector>; + // [[nodiscard]] auto glyph_of_or_fallback(std::string_view text, std::uint32_t size, GlyphFlag flag = GlyphFlag::NONE) const noexcept -> std::vector>; + + // /** + // * @brief The minimum space to be occupied if the specified codepoint is to be rendered in its entirety + // * @param codepoint + // * @param size + // * @param flag + // * @return + // */ + // [[nodiscard]] auto size_of(std::uint32_t codepoint, std::uint32_t size, GlyphFlag flag = GlyphFlag::NONE) noexcept -> extent_type; + // + // /** + // * @brief The minimum space to be occupied if the specified text is to be rendered in its entirety + // * @param text + // * @param size + // * @param flag + // * @return + // */ + // [[nodiscard]] auto size_of(std::u32string_view text, std::uint32_t size, GlyphFlag flag = GlyphFlag::NONE) noexcept -> extent_type; + // + // /** + // * @brief The minimum space to be occupied if the specified text is to be rendered in its entirety + // * @param text + // * @param size + // * @param flag + // * @return + // */ + // [[nodiscard]] auto size_of(std::string_view text, std::uint32_t size, GlyphFlag flag = GlyphFlag::NONE) noexcept -> extent_type; + + /** + * @brief Upload all used glyphs to the texture (if it is not already uploaded) + * @note This function is usually called every frame (unless all the needed glyphs have been uploaded to the texture, but it can still be called) to upload all new (previously unused) glyphs to the texture + */ + auto load_all_glyph() noexcept -> void; + + /** + * @brief Upload all texture atlas (if it didn't upload or needs to be re-uploaded) + * @note This function is usually called every frame to upload the texture to the GPU, or to update the texture (if new glyph data is written) + */ + auto upload_all_texture(Renderer& renderer) noexcept -> void; + + // ==================================================================== + // RenderListSharedData + // ==================================================================== + + [[nodiscard]] auto render_list_shared_data() const noexcept -> const RenderListSharedData&; + + // ==================================================================== + // RenderList + // ==================================================================== + + [[nodiscard]] auto render_data() const noexcept -> std::vector; + + // test + [[nodiscard]] auto test_render_list() noexcept -> RenderList&; + }; +} // namespace gal::prometheus::gfx diff --git a/src/gfx/old/font.cpp b/src/gfx/old/font.cpp new file mode 100644 index 00000000..23622886 --- /dev/null +++ b/src/gfx/old/font.cpp @@ -0,0 +1,284 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +#include + +#include + +namespace gal::prometheus::gfx +{ + auto FontGlyphQueue::push(GlyphInfo& info, GlyphParser::ParseResult&& result) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(result.valid()); + + list_.emplace_back(memory::ref(info), std::move(result)); + } + + auto FontGlyphQueue::upload(TextureContext& context) noexcept -> void + { + std::ranges::for_each( + list_, + [&context](element_type& element) noexcept -> void + { + auto& info = element.info.get(); + const auto& result = element.result; + + const auto [texture_atlas_id, uv] = context.upload_parsed_info_to_texture(result); + info.texture_atlas_id = texture_atlas_id; + info.uv = uv; + } + ); + list_.clear(); + } + + Font::Font(name_type name, const font_id_type id) noexcept + : name_{std::move(name)}, + id_{id} {} + + auto Font::name() const noexcept -> std::string_view + { + return name_; + } + + auto Font::id() const noexcept -> font_id_type + { + return id_; + } + + auto Font::get_glyph(const GlyphKey& key) const noexcept -> const GlyphInfo* + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(id_ != invalid_font_id); + + if (const auto it = glyphs_.find(key); it != glyphs_.end()) + { + return std::addressof(it->second); + } + + return nullptr; + } + + auto Font::set_glyph(const GlyphKey& key, const GlyphParser::ParseResult& result) noexcept -> GlyphInfo& + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(id_ != invalid_font_id); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(result.valid()); + + GlyphInfo info{}; + info.rect = result.rect; + info.advance_x = result.advance_x; + info.visible = result.visible; + info.colored = result.colored; + + return glyphs_.insert_or_assign(key, info).first->second; + } + + FontLoadQueue::FontLoadQueue() noexcept + : new_font_index_{0} {} + + auto FontLoadQueue::push(const std::filesystem::path& path) noexcept -> bool + { + std::ifstream file{path, std::ios::binary}; + if (not file.is_open()) + { + // todo: error handling + return false; + } + + file.seekg(0, std::ios::end); + const auto size = file.tellg(); + + auto* data = new font_data_type::element_type[size]; + file.seekg(0, std::ios::beg); + file.read(reinterpret_cast(data), size); + file.close(); + + list_.emplace_back(std::unique_ptr{data}, static_cast(size)); + return true; + } + + auto FontLoadQueue::upload(GlyphParser& parser, const functional::function_reference_wrapper font_dest) noexcept -> void + { + std::ranges::for_each( + std::ranges::subrange{list_.begin() + new_font_index_, list_.end()}, + [&](font_data_type& data) noexcept -> void + { + if (auto result = parser.load({data.data.get(), data.size}); result.valid()) + { + font_dest(Font{std::move(result.name), result.id}); + } + else + { + // todo: error handling + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + } + } + ); + new_font_index_ = static_cast(list_.size()); + } + + Fonts::Fonts() noexcept + : fallback_glyph_{nullptr}, + parser_{nullptr} {} + + auto Fonts::bind_parser(GlyphParser& parser) noexcept -> GlyphParser* + { + return std::exchange(parser_, std::addressof(parser)); + } + + auto Fonts::add_font(const std::filesystem::path& path) noexcept -> bool + { + return font_load_queue_.push(path); + } + + auto Fonts::load_all_font() noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(parser_ != nullptr); + + const auto loader = [this](Font&& font) noexcept -> void + { + font_list_.emplace_back(std::move(font)); + }; + + font_load_queue_.upload(*parser_, loader); + } + + auto Fonts::set_fallback_glyph() noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not font_list_.empty()); + + if (fallback_glyph_ == nullptr) + { + constexpr GlyphKey key{.codepoint = u'\xFFFD', .size = 16u, .flag = GlyphFlag::NONE}; + set_fallback_glyph(key); + } + + if (fallback_glyph_ == nullptr) + { + constexpr GlyphKey key{.codepoint = u'?', .size = 16u, .flag = GlyphFlag::NONE}; + set_fallback_glyph(key); + } + + if (fallback_glyph_ == nullptr) + { + constexpr GlyphKey key{.codepoint = u' ', .size = 16u, .flag = GlyphFlag::NONE}; + set_fallback_glyph(key); + } + + if (fallback_glyph_ == nullptr) + { + // todo: error handling + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + } + } + + auto Fonts::set_fallback_glyph(const GlyphKey& key) noexcept -> void + { + fallback_glyph_ = this->glyph_of(key); + } + + auto Fonts::set_fallback_glyph(const std::uint32_t codepoint, const std::uint32_t size, const GlyphFlag flag) noexcept -> void + { + fallback_glyph_ = this->glyph_of(codepoint, size, flag); + } + + auto Fonts::glyph_of(const GlyphKey& key) noexcept -> const GlyphInfo* + { + for (const auto& font: font_list_) + { + if (const auto* info = font.get_glyph(key); info != nullptr) + { + return info; + } + } + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(parser_ != nullptr); + + for (auto& font: font_list_) + { + if (auto result = parser_->parse(font.id(), key); result.valid()) + { + auto& queue = font_glyph_queue_[std::addressof(font)]; + auto& inserted_info = font.set_glyph(key, result); + + queue.push(inserted_info, std::move(result)); + return std::addressof(inserted_info); + } + } + + // todo: error handling + return nullptr; + } + + auto Fonts::glyph_of(const std::uint32_t codepoint, const std::uint32_t size, const GlyphFlag flag) noexcept -> const GlyphInfo* + { + return this->glyph_of({.codepoint = codepoint, .size = size, .flag = flag}); + } + + auto Fonts::glyph_of(std::u32string_view text, std::uint32_t size, GlyphFlag flag) noexcept -> std::vector + { + std::vector infos; + infos.reserve(text.size()); + + std::ranges::for_each( + text, + [&infos, this, size, flag](const auto codepoint) noexcept -> void + { + const auto* info = this->glyph_of(codepoint, size, flag); + infos.emplace_back(info); + } + ); + + return infos; + } + + auto Fonts::glyph_of_or_fallback(const GlyphKey& key) const noexcept -> const GlyphInfo& + { + for (const auto& font: font_list_) + { + if (const auto* info = font.get_glyph(key); info != nullptr) + { + return *info; + } + } + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(fallback_glyph_ != nullptr); + return *fallback_glyph_; + } + + auto Fonts::glyph_of_or_fallback(const std::uint32_t codepoint, const std::uint32_t size, const GlyphFlag flag) const noexcept -> const GlyphInfo& + { + return this->glyph_of_or_fallback({.codepoint = codepoint, .size = size, .flag = flag}); + } + + auto Fonts::glyph_of_or_fallback(std::u32string_view text, std::uint32_t size, GlyphFlag flag) const noexcept -> std::vector> + { + std::vector> infos; + infos.reserve(text.size()); + + std::ranges::for_each( + text, + [&infos, this, size, flag](const auto codepoint) noexcept -> void + { + const auto& info = this->glyph_of_or_fallback(codepoint, size, flag); + infos.emplace_back(std::ref(info)); + } + ); + + return infos; + } + + auto Fonts::load_all_glyph(TextureContext& context) noexcept -> void + { + std::ranges::for_each( + font_glyph_queue_ | std::views::values, + [&context](auto& queue) noexcept -> void + { + queue.upload(context); + } + ); + font_glyph_queue_.clear(); + } +} // namespace gal::prometheus::gfx diff --git a/src/gfx/old/font.hpp b/src/gfx/old/font.hpp new file mode 100644 index 00000000..632e4309 --- /dev/null +++ b/src/gfx/old/font.hpp @@ -0,0 +1,204 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace gal::prometheus::gfx +{ + class FontGlyphQueue final + { + public: + struct element_type + { + memory::RefWrapper info; + GlyphParser::ParseResult result; + }; + + using list_type = std::vector; + + private: + list_type list_; + + public: + FontGlyphQueue(const FontGlyphQueue&) noexcept = delete; + FontGlyphQueue(FontGlyphQueue&&) noexcept = default; + auto operator=(const FontGlyphQueue&) noexcept -> FontGlyphQueue& = delete; + auto operator=(FontGlyphQueue&&) noexcept -> FontGlyphQueue& = default; + + ~FontGlyphQueue() noexcept = default; + + FontGlyphQueue() noexcept = default; + + auto push(GlyphInfo& info, GlyphParser::ParseResult&& result) noexcept -> void; + + auto upload(TextureContext& context) noexcept -> void; + }; + + class Font final + { + public: + using name_type = std::string; + + private: + name_type name_; + font_id_type id_; + + std::unordered_map glyphs_; + + public: + Font(const Font&) noexcept = delete; + Font(Font&&) noexcept = default; + auto operator=(const Font&) noexcept -> Font& = delete; + auto operator=(Font&&) noexcept -> Font& = default; + + ~Font() noexcept = default; + + Font(name_type name, font_id_type id) noexcept; + + /** + * @brief Font name (obtained on GlyphParser::load) + */ + [[nodiscard]] auto name() const noexcept -> std::string_view; + + /** + * @brief Font id (obtained on GlyphParser::load) + */ + [[nodiscard]] auto id() const noexcept -> font_id_type; + + /** + * @brief Get the glyph information of the specified codepoint, if it can't be found, then return a null pointer + * @param key {codepoint, size, flag} + * @return The glyph information of the specified codepoint, or a null pointer if it can't be found + */ + [[nodiscard]] auto get_glyph(const GlyphKey& key) const noexcept -> const GlyphInfo*; + + /** + * @brief Set the glyph information of the specified codepoint, override if it already exists + * @param key {codepoint, size, flag} + * @param result The parse result of the specified codepoint + * @return GlyphInfo after insertion + */ + [[nodiscard]] auto set_glyph(const GlyphKey& key, const GlyphParser::ParseResult& result) noexcept -> GlyphInfo&; + }; + + class FontLoadQueue final + { + public: + struct font_data_type + { + using element_type = std::uint8_t; + using data_type = std::unique_ptr; + using size_type = std::uint32_t; + + data_type data; + size_type size; + }; + + using list_type = std::vector; + + private: + list_type list_; + list_type::difference_type new_font_index_; + + public: + FontLoadQueue(const FontLoadQueue&) noexcept = delete; + FontLoadQueue(FontLoadQueue&&) noexcept = default; + auto operator=(const FontLoadQueue&) noexcept -> FontLoadQueue& = delete; + auto operator=(FontLoadQueue&&) noexcept -> FontLoadQueue& = default; + + ~FontLoadQueue() noexcept = default; + + FontLoadQueue() noexcept; + + auto push(const std::filesystem::path& path) noexcept -> bool; + + auto upload(GlyphParser& parser, functional::function_reference_wrapper font_dest) noexcept -> void; + }; + + class Fonts final + { + public: + using font_list_type = std::vector; + + using font_glyph_queue_list_type = std::unordered_map; + + private: + font_list_type font_list_; + FontLoadQueue font_load_queue_; + + const GlyphInfo* fallback_glyph_; + font_glyph_queue_list_type font_glyph_queue_; + + GlyphParser* parser_; + + public: + Fonts(const Fonts&) noexcept = delete; + Fonts(Fonts&&) noexcept = default; + auto operator=(const Fonts&) noexcept -> Fonts& = delete; + auto operator=(Fonts&&) noexcept -> Fonts& = default; + + ~Fonts() noexcept = default; + + Fonts() noexcept; + + /** + * @brief Bind parser, default parser is null pointer, must bind parser before loading fonts + * @return The previously bound parser, or nullptr if no parser is bound + */ + auto bind_parser(GlyphParser& parser) noexcept -> GlyphParser*; + + /** + * @brief Load fonts from the specified path, assuming the path is a valid font file + * @param path Font path + * @return Returns true if the file exists and was opened successfully (without checking if it is a valid font file), otherwise returns false + */ + auto add_font(const std::filesystem::path& path) noexcept -> bool; + + /** + * @brief Load the fonts previously added by @c add_font + * @note This function is usually called at initialization time (or at every frame if needed) to load all the required fonts + */ + auto load_all_font() noexcept -> void; + + auto set_fallback_glyph() noexcept -> void; + + /** + * @brief Set the fallback glyph, if we can't find the glyph of the specified codepoint, then use the fallback glyph + * @param key {codepoint, size, flag} + */ + auto set_fallback_glyph(const GlyphKey& key) noexcept -> void; + + /** + * @brief Set the fallback glyph, if we can't find the glyph of the specified codepoint, then use the fallback glyph + */ + auto set_fallback_glyph(std::uint32_t codepoint, std::uint32_t size, GlyphFlag flag) noexcept -> void; + + [[nodiscard]] auto glyph_of(const GlyphKey& key) noexcept -> const GlyphInfo*; + [[nodiscard]] auto glyph_of(std::uint32_t codepoint, std::uint32_t size, GlyphFlag flag) noexcept -> const GlyphInfo*; + [[nodiscard]] auto glyph_of(std::u32string_view text, std::uint32_t size, GlyphFlag flag) noexcept -> std::vector; + + [[nodiscard]] auto glyph_of_or_fallback(const GlyphKey& key) const noexcept -> const GlyphInfo&; + [[nodiscard]] auto glyph_of_or_fallback(std::uint32_t codepoint, std::uint32_t size, GlyphFlag flag) const noexcept -> const GlyphInfo&; + [[nodiscard]] auto glyph_of_or_fallback(std::u32string_view text, std::uint32_t size, GlyphFlag flag) const noexcept -> std::vector>; + + /** + * @brief Upload all used glyphs to the texture (if it is not already uploaded) + * @note This function is usually called every frame (unless all the needed glyphs have been uploaded to the texture, but it can still be called) to upload all new (previously unused) glyphs to the texture + */ + auto load_all_glyph(TextureContext& context) noexcept -> void; + }; +} // namespace gal::prometheus::gfx diff --git a/src/gfx/old/gfx.hpp b/src/gfx/old/gfx.hpp new file mode 100644 index 00000000..27860b69 --- /dev/null +++ b/src/gfx/old/gfx.hpp @@ -0,0 +1,20 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include + +#include +#include +#include + +#include + +#include +#include +#include + +#include diff --git a/src/gfx/old/glyph.cpp b/src/gfx/old/glyph.cpp new file mode 100644 index 00000000..31d93d3e --- /dev/null +++ b/src/gfx/old/glyph.cpp @@ -0,0 +1,11 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +namespace gal::prometheus::gfx +{ + GlyphParser::~GlyphParser() noexcept = default; +} diff --git a/src/gfx/old/glyph.hpp b/src/gfx/old/glyph.hpp new file mode 100644 index 00000000..3fc89e9f --- /dev/null +++ b/src/gfx/old/glyph.hpp @@ -0,0 +1,150 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include +#include + +namespace gal::prometheus::gfx +{ + /** + * @brief Glyph key + */ + class GlyphKey final + { + public: + std::uint32_t codepoint; + std::uint32_t size : 26; + GlyphFlag flag : 6; + + [[nodiscard]] constexpr auto operator==(const GlyphKey& other) const noexcept -> bool + { + return codepoint == other.codepoint and size == other.size and flag == other.flag; + } + + struct hasher + { + [[nodiscard]] auto operator()(const GlyphKey& key) const noexcept -> std::size_t + { + return std::hash{}(key.codepoint) ^ std::hash{}(key.size) ^ std::hash{}(static_cast(key.flag)); + } + }; + }; + + /** + * @brief Glyph info (bitmap info & texture info) + */ + class GlyphInfo final + { + public: + using value_type = extent_type::value_type; + using uv_type = primitive::basic_rect_2d; + + // ============= + // Data filled when loading glyph + // ============= + + // Bitmap infos of this glyph + rect_type rect{-1, -1, -1, -1}; + value_type advance_x{-1}; + bool visible{false}; + bool colored{false}; + + // ============= + // Data filled when writing texture + // ============= + + // The id of the texture atlas where the glyph is located + // This id is present if and only if the glyph is in a texture atlas, otherwise it is invalid_texture_atlas_id + texture_atlas_id_type texture_atlas_id{invalid_texture_atlas_id}; + // The uv coordinate of the glyph in the texture atlas + uv_type uv{-1, -1, -1, -1}; + }; + + class GlyphParser + { + public: + class [[nodiscard]] LoadResult final + { + public: + std::string name; + font_id_type id; + + [[nodiscard]] explicit operator bool() const noexcept + { + return id != invalid_font_id; + } + + [[nodiscard]] auto valid() const noexcept -> bool + { + return operator bool(); + } + }; + + class [[nodiscard]] ParseResult final + { + public: + // We could have just returned a GlyphInfo, + // but to make the purpose of the interface clearer, + // we specify that we only return part of the GlyphInfo (see GlyphInfo->'Data filled when loading glyph') + // GlyphInfo info; + + // Bitmap infos of this glyph + rect_type rect{-1, -1, -1, -1}; + GlyphInfo::value_type advance_x{-1}; + bool visible{false}; + bool colored{false}; + + // todo: Borrows a memory region from the texture to write to, rather than having it allocated by the parser + Texture::data_type data{nullptr}; + + [[nodiscard]] explicit operator bool() const noexcept + { + return data != nullptr; + } + + [[nodiscard]] auto valid() const noexcept -> bool + { + return operator bool(); + } + }; + + GlyphParser(const GlyphParser&) noexcept = delete; + GlyphParser(GlyphParser&&) noexcept = default; + auto operator=(const GlyphParser&) noexcept -> GlyphParser& = delete; + auto operator=(GlyphParser&&) noexcept -> GlyphParser& = default; + + virtual ~GlyphParser() noexcept; + + GlyphParser() noexcept = default; + + [[nodiscard]] virtual auto ready() noexcept -> bool = 0; + + /** + * @brief Load font data from @c data, get all glyph data, return id of font + * @param data Font data + * @return id of the font, or invalid_font_id if failed to load + * @note Font data *should not* be released unless the target font is no longer needed (to call @c parse) + */ + [[nodiscard]] virtual auto load(std::span data) noexcept -> LoadResult = 0; + + /** + * @brief Determines whether the target font contains the glyphs of the specified codepoint + * @param id The id returned by loading the font from the previous load + * @param codepoint The codepoint of the glyph to be checked + * @return Exists or not + */ + [[nodiscard]] virtual auto has_glyph(font_id_type id, std::uint32_t codepoint) const noexcept -> bool = 0; + + /** + * @brief Get the glyph information of the specified size (and style) of the target codepoint, if it does not exist then data is a null pointer + * @param id The id returned by loading the font from the previous load + * @param key {codepoint, size, flag} + * @return The glyph information of the specified size (and style) of the target codepoint + */ + [[nodiscard]] virtual auto parse(font_id_type id, const GlyphKey& key) noexcept -> ParseResult = 0; + }; +} diff --git a/src/gfx/old/glyph_parser_freetype.cpp b/src/gfx/old/glyph_parser_freetype.cpp new file mode 100644 index 00000000..ee30e5ab --- /dev/null +++ b/src/gfx/old/glyph_parser_freetype.cpp @@ -0,0 +1,279 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +#if defined(GAL_PROMETHEUS_GFX_GLYPH_PARSER_FREETYPE) + +#include +#include + +namespace +{ + [[nodiscard]] auto ft_size_to_float(const FT_Pos size) noexcept -> float + { + return static_cast((size + 63) >> 6); + } +} // namespace + +namespace gal::prometheus::gfx +{ + class FreeTypeGlyphParser::Library final + { + public: + FT_Library library{nullptr}; + }; + + class FreeTypeGlyphParser::FontInfo final + { + public: + FT_Face face{nullptr}; + + float ascender{0}; + float descender{0}; + float line_spacing{0}; + float line_gap{0}; + float max_advance_width{0}; + + auto set_pixel_height(const std::size_t height) noexcept -> bool + { + FT_Size_RequestRec request{.type = FT_SIZE_REQUEST_TYPE_NOMINAL, .width = 0, .height = static_cast(height) * 64, .horiResolution = 0, .vertResolution = 0}; + + if (const auto error = FT_Request_Size(face, &request); error != FT_Err_Ok) + { + return false; + } + + const auto& metrics = face->size->metrics; + + ascender = ft_size_to_float(metrics.ascender); + descender = ft_size_to_float(metrics.descender); + line_spacing = ft_size_to_float(metrics.height); + line_gap = ft_size_to_float(metrics.height - metrics.ascender + metrics.descender); + max_advance_width = ft_size_to_float(metrics.max_advance); + + return true; + } + }; + + FreeTypeGlyphParser::~FreeTypeGlyphParser() noexcept + { + std::ranges::for_each( + infos_, + [](auto& info) noexcept -> void + { + ::FT_Done_Face(info.face); + } + ); + + FT_Done_FreeType(library_->library); + } + + FreeTypeGlyphParser::FreeTypeGlyphParser() noexcept + : library_{memory::make_unique()} + { + if (const auto error = FT_Init_FreeType(&library_->library); error != FT_Err_Ok) + { + // todo: error handling + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + } + } + + auto FreeTypeGlyphParser::ready() noexcept -> bool + { + return library_ != nullptr; + } + + // auto FreeTypeGlyphParser::load(const std::filesystem::path& path) noexcept -> LoadResult + // { + // const auto path_string = path.string(); + // + // LoadResult invalid_result{.name = {}, .id = invalid_font_id}; + // + // FT_Face face = nullptr; + // if (const auto error = FT_New_Face(library_->library, path_string.data(), 0, &face); error != FT_Err_Ok) + // { + // return invalid_result; + // } + // + // if (const auto error = FT_Select_Charmap(face, FT_ENCODING_UNICODE); error != FT_Err_Ok) + // { + // FT_Done_Face(face); + // return invalid_result; + // } + // + // const auto id = infos_.size(); + // + // auto& info = infos_.emplace_back(); + // info.face = face; + // + // return {.name = face->family_name, .id = static_cast(id)}; + // } + + auto FreeTypeGlyphParser::load(const std::span data) noexcept -> LoadResult + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(data.data() != nullptr); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not data.empty()); + + LoadResult invalid_result{.name = {}, .id = invalid_font_id}; + + FT_Face face = nullptr; + if (const auto error = FT_New_Memory_Face(library_->library, data.data(), static_cast(data.size()), 0, &face); error != FT_Err_Ok) + { + return invalid_result; + } + + if (const auto error = FT_Select_Charmap(face, FT_ENCODING_UNICODE); error != FT_Err_Ok) + { + FT_Done_Face(face); + return invalid_result; + } + + const auto id = infos_.size(); + + auto& info = infos_.emplace_back(); + info.face = face; + + return {.name = face->family_name, .id = static_cast(id)}; + } + + auto FreeTypeGlyphParser::has_glyph(const font_id_type id, const std::uint32_t codepoint) const noexcept -> bool + { + if (id >= infos_.size()) + { + return false; + } + + const auto& info = infos_[id]; + if (const auto char_index = FT_Get_Char_Index(info.face, codepoint); char_index == 0) + { + return false; + } + + return true; + } + + auto FreeTypeGlyphParser::parse(const font_id_type id, const GlyphKey& key) noexcept -> ParseResult + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(id < infos_.size()); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(key.codepoint != 0); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(key.size != 0); + + ParseResult invalid_result{}; + + auto& info = infos_[id]; + const auto& face = info.face; + + const auto char_index = FT_Get_Char_Index(face, key.codepoint); + if (char_index == 0) + { + return invalid_result; + } + + info.set_pixel_height(key.size); + + if (const auto error = FT_Load_Glyph(face, char_index, FT_LOAD_DEFAULT); error != FT_Err_Ok) + { + return invalid_result; + } + + const auto& slot = face->glyph; + + if (std::to_underlying(key.flag) & GlyphFlag::BOLD) + { + FT_GlyphSlot_Embolden(slot); + } + if (std::to_underlying(key.flag) & GlyphFlag::ITALIC) + { + FT_GlyphSlot_Oblique(slot); + } + + if (const auto error = FT_Render_Glyph(slot, FT_RENDER_MODE_NORMAL); error != FT_Err_Ok) + { + return invalid_result; + } + + const auto& bitmap = face->glyph->bitmap; + + const point_type point{static_cast(slot->bitmap_left), static_cast(slot->bitmap_top)}; + const extent_type size{static_cast(bitmap.width), static_cast(bitmap.rows)}; + const std::size_t data_length = static_cast(bitmap.width) * bitmap.rows; + + ParseResult result{ + .rect = {point, size}, + .advance_x = ft_size_to_float(slot->advance.x), + .visible = size.width > 0 and size.height > 0, + .colored = bitmap.pixel_mode == FT_PIXEL_MODE_BGRA, + .data = std::make_unique_for_overwrite(data_length) + }; + + { + const auto* source = bitmap.buffer; + auto* dest = result.data.get(); + + if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) + { + for (std::uint32_t y = 0; y < bitmap.rows; ++y) + { + for (std::uint32_t x = 0; x < bitmap.width; ++x) + { + const auto a = source[x]; + const auto color = + // A + a << 24 | + // B + std::uint32_t{0xff} << 16 | + // G + std::uint32_t{0xff} << 8 | + // R + std::uint32_t{0xff}; + dest[x] = color; + } + + source += bitmap.pitch; + dest += bitmap.width; + } + } + else if (bitmap.pixel_mode == FT_PIXEL_MODE_MONO) + { + for (std::uint32_t y = 0; y < bitmap.rows; ++y) + { + const std::uint8_t* p = source; + std::uint8_t bits = 0; + + for (std::uint32_t x = 0; x < bitmap.width; ++x) + { + if ((x & 7) == 0) + { + bits = *p; + p += 1; + } + + const auto a = (bits & 0x80) ? std::uint32_t{0xff} : std::uint32_t{0}; + const auto color = + // A + a << 24 | + // B + std::uint32_t{0xff} << 16 | + // G + std::uint32_t{0xff} << 8 | + // R + std::uint32_t{0xff}; + dest[x] = color; + + bits <<= 1; + } + + source += bitmap.pitch; + dest += bitmap.width; + } + } + } + + return result; + } +} // namespace gal::prometheus::gfx + +#endif diff --git a/src/gfx/old/glyph_parser_freetype.hpp b/src/gfx/old/glyph_parser_freetype.hpp new file mode 100644 index 00000000..41a37392 --- /dev/null +++ b/src/gfx/old/glyph_parser_freetype.hpp @@ -0,0 +1,52 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +// todo +#define GAL_PROMETHEUS_GFX_GLYPH_PARSER_FREETYPE + +#if defined(GAL_PROMETHEUS_GFX_GLYPH_PARSER_FREETYPE) + +#include + +#include + +#include + +namespace gal::prometheus::gfx +{ + class FreeTypeGlyphParser final : public GlyphParser + { + class Library; + class FontInfo; + + public: + using infos_type = std::vector; + + private: + memory::UniquePointer library_; + infos_type infos_; + + public: + FreeTypeGlyphParser(const FreeTypeGlyphParser&) noexcept = delete; + FreeTypeGlyphParser(FreeTypeGlyphParser&&) noexcept = default; + auto operator=(const FreeTypeGlyphParser&) noexcept -> FreeTypeGlyphParser& = delete; + auto operator=(FreeTypeGlyphParser&&) noexcept -> FreeTypeGlyphParser& = default; + ~FreeTypeGlyphParser() noexcept override; + + FreeTypeGlyphParser() noexcept; + + auto ready() noexcept -> bool override; + + auto load(std::span data) noexcept -> LoadResult override; + + [[nodiscard]] auto has_glyph(font_id_type id, std::uint32_t codepoint) const noexcept -> bool override; + + auto parse(font_id_type id, const GlyphKey& key) noexcept -> ParseResult override; + }; +} // namespace gal::prometheus::gfx + +#endif diff --git a/src/gfx/old/render_list.cpp b/src/gfx/old/render_list.cpp new file mode 100644 index 00000000..6b1d9416 --- /dev/null +++ b/src/gfx/old/render_list.cpp @@ -0,0 +1,2401 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +#include + +#include +#include +#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE + +namespace +{ + using namespace gal::prometheus; + using namespace gfx; + + // @see https://stackoverflow.com/a/2244088/15194693 + // Number of segments (N) is calculated using equation: + // N = ceil ( pi / acos(1 - error / r) ) where r > 0 and error <= r + [[nodiscard]] constexpr auto circle_segments_calc(const float radius, const float max_error) noexcept -> auto + { + constexpr auto circle_segments_roundup_to_even = [](const auto v) noexcept -> auto + { + return (v + 1) / 2 * 2; + }; + + return std::ranges::clamp( + circle_segments_roundup_to_even(static_cast(std::ceil(std::numbers::pi_v / std::acos(1 - std::ranges::min(radius, max_error) / radius)))), + RenderListSharedData::circle_segments_min, + RenderListSharedData::circle_segments_max + ); + } + + [[nodiscard]] constexpr auto circle_segments_calc_radius(const std::size_t n, const float max_error) noexcept -> auto + { + return max_error / (1 - math::cos(std::numbers::pi_v / std::ranges::max(static_cast(n), std::numbers::pi_v))); + } + + [[nodiscard]] constexpr auto circle_segments_calc_error(const std::size_t n, const float radius) noexcept -> auto + { + return (1 - math::cos(std::numbers::pi_v / std::ranges::max(static_cast(n), std::numbers::pi_v))) / radius; + } + + template + [[nodiscard]] constexpr auto vertex_sample_points_calc() noexcept -> RenderListSharedData::vertex_sample_points_type + { + return [](std::index_sequence) noexcept -> RenderListSharedData::vertex_sample_points_type + { + constexpr auto make_point = []() noexcept -> point_type + { + const auto a = static_cast(I) / static_cast(N) * 2 * std::numbers::pi_v; + return {math::cos(a), -math::sin(a)}; + }; + + return {{make_point.template operator()()...}}; + }(std::make_index_sequence{}); + } + + [[nodiscard]] constexpr auto range_of_arc(const RenderArcFlag flag) noexcept -> std::pair + { + static_assert(RenderListSharedData::vertex_sample_points_count % 12 == 0); + constexpr auto factor = static_cast(RenderListSharedData::vertex_sample_points_count / 12); + + switch (flag) + { + case RenderArcFlag::Q1: + { + return std::make_pair(0 * factor, 3 * factor); + } + case RenderArcFlag::Q2: + { + return std::make_pair(3 * factor, 6 * factor); + } + case RenderArcFlag::Q3: + { + return std::make_pair(6 * factor, 9 * factor); + } + case RenderArcFlag::Q4: + { + return std::make_pair(9 * factor, 12 * factor); + } + case RenderArcFlag::TOP: + { + return std::make_pair(0 * factor, 6 * factor); + } + case RenderArcFlag::BOTTOM: + { + return std::make_pair(6 * factor, 12 * factor); + } + case RenderArcFlag::LEFT: + { + return std::make_pair(3 * factor, 9 * factor); + } + case RenderArcFlag::RIGHT: + { + return std::make_pair(9 * factor, 15 * factor); + } + case RenderArcFlag::ALL: + { + return std::make_pair(0 * factor, 12 * factor); + } + case RenderArcFlag::Q1_CLOCK_WISH: + { + return std::make_pair(3 * factor, 0 * factor); + } + case RenderArcFlag::Q2_CLOCK_WISH: + { + return std::make_pair(6 * factor, 3 * factor); + } + case RenderArcFlag::Q3_CLOCK_WISH: + { + return std::make_pair(9 * factor, 6 * factor); + } + case RenderArcFlag::Q4_CLOCK_WISH: + { + return std::make_pair(12 * factor, 9 * factor); + } + case RenderArcFlag::TOP_CLOCK_WISH: + { + return std::make_pair(6 * factor, 0 * factor); + } + case RenderArcFlag::BOTTOM_CLOCK_WISH: + { + return std::make_pair(12 * factor, 6 * factor); + } + case RenderArcFlag::LEFT_CLOCK_WISH: + { + return std::make_pair(9 * factor, 3 * factor); + } + case RenderArcFlag::RIGHT_CLOCK_WISH: + { + return std::make_pair(15 * factor, 9 * factor); + } + case RenderArcFlag::ALL_CLOCK_WISH: + { + return std::make_pair(12 * factor, 0 * factor); + } + default: // NOLINT(clang-diagnostic-covered-switch-default) + { + GAL_PROMETHEUS_ERROR_UNREACHABLE(); + } + } + } + + [[nodiscard]] constexpr auto to_fixed_rect_corner_flag(const RenderFlag flag) noexcept -> RenderFlag + { + using enum RenderFlag; + + if ((flag & ROUND_CORNER_MASK) == NONE) + { + return ROUND_CORNER_ALL | flag; + } + + return flag; + } + + [[nodiscard]] constexpr auto to_fixed_normal(const float x, const float y) noexcept -> std::pair + { + if (const auto d = math::pow(x, 2) + math::pow(y, 2); d > 1e-6f) + { + // fixme + const auto inv_len = [d] + { + // #if defined(__AVX512F__) + // __m512 d_v = _mm512_set1_ps(d); + // __m512 inv_len_v = _mm512_rcp14_ps(d_v); + // return _mm512_cvtss_f32(inv_len_v); + // #elif defined(__AVX__) + // __m256 d_v = _mm256_set1_ps(d); + // __m256 inv_len_v = _mm256_rcp_ps(d_v); + // return _mm256_cvtss_f32(inv_len_v); + // #elif defined(__SSE4_1__) or defined(__SSE3__) or defined(__SSE__) + // __m128 d_v = _mm_set_ss(d); + // __m128 inv_len_v = _mm_rcp_ss(d_v); + // return _mm_cvtss_f32(inv_len_v); + // #else + return 1.0f / d; + // #endif + }(); + + return {x * inv_len, y * inv_len}; + } + + return {x, y}; + } + + // fixme + constexpr std::size_t bezier_curve_casteljau_max_level = 10; + + constexpr auto bezier_cubic_calc = [](const point_type& p1, const point_type& p2, const point_type& p3, const point_type& p4, const float tolerance) noexcept -> point_type + { + const auto u = 1.f - tolerance; + + const auto w1 = math::pow(u, 3); + const auto w2 = 3 * math::pow(u, 2) * tolerance; + const auto w3 = 3 * u * math::pow(tolerance, 2); + const auto w4 = math::pow(tolerance, 3); + + return {p1.x * w1 + p2.x * w2 + p3.x * w3 + p4.x * w4, p1.y * w1 + p2.y * w2 + p3.y * w3 + p4.y * w4}; + }; + + constexpr auto bezier_quadratic_calc = [](const point_type& p1, const point_type& p2, const point_type& p3, const float tolerance) noexcept -> point_type + { + const auto u = 1.f - tolerance; + + const auto w1 = math::pow(u, 2); + const auto w2 = 2 * u * tolerance; + const auto w3 = math::pow(tolerance, 2); + + return {p1.x * w1 + p2.x * w2 + p3.x * w3, p1.y * w1 + p2.y * w2 + p3.y * w3}; + }; + + class RenderDataAppender final + { + public: + using size_type = RenderData::size_type; + + using command_type = RenderData::command_type; + + using vertex_list_type = RenderData::vertex_list_type; + using index_list_type = RenderData::index_list_type; + + private: + // command_type::element_count + memory::RefWrapper element_count_; + // RenderList::vertex_list + memory::RefWrapper vertex_list_; + // RenderList::index_list_; + memory::RefWrapper index_list_; + + public: + RenderDataAppender(command_type& command, vertex_list_type& vertex_list, index_list_type& index_list) noexcept + : element_count_{command.element_count}, + vertex_list_{vertex_list}, + index_list_{index_list} {} + + [[nodiscard]] auto vertex_count() const noexcept -> size_type + { + const auto& list = vertex_list_.get(); + const auto size = list.size(); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(size < std::numeric_limits::max(), "Too many vertex!"); + + return static_cast(size); + } + + auto reserve(const size_type vertex_count, const size_type index_count) noexcept -> void + { + auto& element_count = element_count_.get(); + auto& vertex = vertex_list_.get(); + auto& index = index_list_.get(); + + element_count += index_count; + vertex.reserve(vertex.size() + vertex_count); + index.reserve(index.size() + index_count); + } + + auto reserve(const std::size_t vertex_count, const std::size_t index_count) noexcept -> void + { + reserve(static_cast(vertex_count), static_cast(index_count)); + } + + auto add_vertex(const point_type& point, const uv_type& uv, color_type color) noexcept -> void + { + auto& list = vertex_list_.get(); + + list.emplace_back(point, uv, color); + } + + auto add_index(const index_type a, const index_type b, const index_type c) noexcept -> void + { + auto& list = index_list_.get(); + + list.push_back(a); + list.push_back(b); + list.push_back(c); + } + }; +} // namespace + +namespace gal::prometheus::gfx +{ + RenderListSharedData::RenderListSharedData() noexcept + : circle_segment_counts{}, + vertex_sample_points{vertex_sample_points_calc()}, + circle_segment_max_error{0}, + arc_fast_radius_cutoff{0}, + curve_tessellation_tolerance{1.25f} + { + set_circle_tessellation_max_error(.3f); + } + + auto RenderListSharedData::circle_auto_segment_count(const float radius) const noexcept -> circle_segment_count_type + { + // ceil to never reduce accuracy + if (const auto radius_index = static_cast(radius + .999999f); radius_index < circle_segment_counts.size()) + { + return circle_segment_counts[radius_index]; + } + return static_cast(circle_segments_calc(radius, circle_segment_max_error)); + } + + auto RenderListSharedData::vertex_sample_point(const std::size_t index) const noexcept -> const point_type& + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(index < vertex_sample_points.size()); + + return vertex_sample_points[index]; + } + + auto RenderListSharedData::set_circle_tessellation_max_error(const float max_error) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(max_error > .0f); + + if (circle_segment_max_error == max_error) // NOLINT(clang-diagnostic-float-equal) + { + return; + } + + for (auto [index, count]: circle_segment_counts | std::views::enumerate) + { + const auto radius = static_cast(index); + count = static_cast(circle_segments_calc(radius, max_error)); + } + + circle_segment_max_error = max_error; + arc_fast_radius_cutoff = circle_segments_calc_radius(vertex_sample_points_count, max_error); + } + + auto RenderListSharedData::set_curve_tessellation_tolerance(const float tolerance) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(tolerance > .0f); + + curve_tessellation_tolerance = tolerance; + } + + class RenderList::Drawer + { + public: + memory::RefWrapper self; + + using path_list_type = std::vector; + + path_list_type path_list; + + [[nodiscard]] auto make_appender() noexcept -> RenderDataAppender + { + auto& render_list = self.get(); + + return {render_list.command_list_.back(), render_list.vertex_list_, render_list.index_list_}; + } + + auto draw_polygon_line(const color_type color, const RenderFlag render_flag, const float thickness) noexcept -> void + { + const auto path_point_count = path_list.size(); + const auto& path_point = path_list; + + if (path_point_count < 2 or color.alpha == 0) + { + return; + } + + const auto& render_list = self.get(); + const auto& shared_data = render_list.shared_data(); + auto appender = make_appender(); + + const auto is_closed = (render_flag & RenderFlag::CLOSED) != RenderFlag::NONE; + const auto segments_count = is_closed ? path_point_count : path_point_count - 1; + + const auto vertex_count = segments_count * 4; + const auto index_count = segments_count * 6; + appender.reserve(vertex_count, index_count); + + for (std::decay_t i = 0; i < segments_count; ++i) + { + const auto n = (i + 1) % path_point_count; + + const auto& p1 = path_point[i]; + const auto& p2 = path_point[n]; + + auto [normalized_x, normalized_y] = math::normalize(p2.x - p1.x, p2.y - p1.y); + normalized_x *= (thickness * .5f); + normalized_y *= (thickness * .5f); + + const auto current_vertex_index = static_cast(appender.vertex_count()); + const auto& opaque_uv = shared_data.white_pixel_uv; + + appender.add_vertex(p1 + point_type{normalized_y, -normalized_x}, opaque_uv, color); + appender.add_vertex(p2 + point_type{normalized_y, -normalized_x}, opaque_uv, color); + appender.add_vertex(p2 + point_type{-normalized_y, normalized_x}, opaque_uv, color); + appender.add_vertex(p1 + point_type{-normalized_y, normalized_x}, opaque_uv, color); + + appender.add_index(current_vertex_index + 0, current_vertex_index + 1, current_vertex_index + 2); + appender.add_index(current_vertex_index + 0, current_vertex_index + 2, current_vertex_index + 3); + } + } + + auto draw_polygon_line_aa(const color_type color, const RenderFlag render_flag, float thickness) noexcept -> void + { + const auto path_point_count = path_list.size(); + const auto& path_point = path_list; + + if (path_point_count < 2 or color.alpha == 0) + { + return; + } + + const auto& render_list = self.get(); + const auto render_list_flag = render_list.render_list_flag_; + const auto& shared_data = render_list.shared_data(); + auto appender = make_appender(); + + const auto& opaque_uv = shared_data.white_pixel_uv; + const auto transparent_color = color.transparent(); + + const auto is_closed = (render_flag & RenderFlag::CLOSED) != RenderFlag::NONE; + const auto segments_count = is_closed ? path_point_count : path_point_count - 1; + const auto is_thick_line = thickness > 1.f; + + thickness = std::ranges::max(thickness, 1.f); + const auto thickness_integer = static_cast(thickness); + const auto thickness_fractional = thickness - static_cast(thickness_integer); + + const auto is_use_texture = + ((render_list_flag & RenderListFlag::ANTI_ALIASED_LINE_USE_TEXTURE) == RenderListFlag::ANTI_ALIASED_LINE_USE_TEXTURE and + (thickness_integer < RenderListSharedData::baked_line_uv_count) and (thickness_fractional <= .00001f)); + + const auto vertex_cont = is_use_texture ? (path_point_count * 2) : (is_thick_line ? path_point_count * 4 : path_point_count * 3); + const auto index_count = is_use_texture ? (segments_count * 6) : (is_thick_line ? segments_count * 18 : segments_count * 12); + appender.reserve(vertex_cont, index_count); + + // The first items are normals at each line point, then after that there are either 2 or 4 temp points for each line point + path_list_type temp_buffer{}; + temp_buffer.resize(path_point_count * ((is_use_texture or not is_thick_line) ? 3 : 5)); + auto temp_buffer_normals = std::span{temp_buffer.begin(), path_point_count}; + auto temp_buffer_points = std::span{temp_buffer.begin() + static_cast(path_point_count), temp_buffer.end()}; + + // Calculate normals (tangents) for each line segment + for (std::decay_t i = 0; i < segments_count; ++i) + { + const auto n = (i + 1) % path_point_count; + const auto d = path_point[n] - path_point[i]; + + const auto [normalized_x, normalized_y] = math::normalize(d.x, d.y); + temp_buffer_normals[i].x = normalized_y; + temp_buffer_normals[i].y = -normalized_x; + } + + if (not is_closed) + { + temp_buffer_normals[temp_buffer_normals.size() - 1] = temp_buffer_normals[temp_buffer_normals.size() - 2]; + } + + // If we are drawing a one-pixel-wide line without a texture, or a textured line of any width, we only need 2 or 3 vertices per point + if (is_use_texture or not is_thick_line) + { + // [PATH 1] Texture-based lines (thick or non-thick) + + // The width of the geometry we need to draw - this is essentially pixels for the line itself, plus "one pixel" for AA + const auto half_draw_size = is_use_texture ? ((thickness * .5f) + 1.f) : 1.f; + + // If line is not closed, the first and last points need to be generated differently as there are no normals to blend + if (not is_closed) + { + temp_buffer_points[0] = path_point[0] + temp_buffer_normals[0] * half_draw_size; + temp_buffer_points[1] = path_point[0] - temp_buffer_normals[0] * half_draw_size; + temp_buffer_points[(path_point_count - 1) * 2 + 0] = path_point[path_point_count - 1] + temp_buffer_normals[path_point_count - 1] * half_draw_size; + temp_buffer_points[(path_point_count - 1) * 2 + 1] = path_point[path_point_count - 1] - temp_buffer_normals[path_point_count - 1] * half_draw_size; + } + + const auto current_vertex_index = static_cast(appender.vertex_count()); + + // Generate the indices to form a number of triangles for each line segment, and the vertices for the line edges + // This takes points n and n+1 and writes into n+1, with the first point in a closed line being generated from the final one (as n+1 wraps) + auto vertex_index_for_start = current_vertex_index; + for (std::decay_t first_point_of_segment = 0; first_point_of_segment < segments_count; ++first_point_of_segment) + { + const auto second_point_of_segment = (first_point_of_segment + 1) % path_point_count; + const auto vertex_index_for_end = static_cast( + // closed + (first_point_of_segment + 1) == path_point_count ? current_vertex_index : (vertex_index_for_start + (is_use_texture ? 2 : 3)) + ); + + // Average normals + const auto d = (temp_buffer_normals[first_point_of_segment] + temp_buffer_normals[second_point_of_segment]) * .5f; + // dm_x, dm_y are offset to the outer edge of the AA area + auto [dm_x, dm_y] = to_fixed_normal(d.x, d.y); + dm_x *= half_draw_size; + dm_y *= half_draw_size; + + // Add temporary vertexes for the outer edges + temp_buffer_points[second_point_of_segment * 2 + 0] = path_point[second_point_of_segment] + point_type{dm_x, dm_y}; + temp_buffer_points[second_point_of_segment * 2 + 1] = path_point[second_point_of_segment] - point_type{dm_x, dm_y}; + + if (is_use_texture) + { + // Add indices for two triangles + + // right + appender.add_index(vertex_index_for_end + 0, vertex_index_for_start + 0, vertex_index_for_start + 1); + // left + appender.add_index(vertex_index_for_end + 1, vertex_index_for_start + 1, vertex_index_for_end + 0); + } + else + { + // Add indexes for four triangles + + // right 1 + appender.add_index(vertex_index_for_end + 0, vertex_index_for_start + 0, vertex_index_for_start + 2); + // right 2 + appender.add_index(vertex_index_for_start + 2, vertex_index_for_end + 2, vertex_index_for_end + 0); + // left 1 + appender.add_index(vertex_index_for_end + 1, vertex_index_for_start + 1, vertex_index_for_start + 0); + // left 2 + appender.add_index(vertex_index_for_start + 0, vertex_index_for_end + 0, vertex_index_for_end + 1); + } + + vertex_index_for_start = vertex_index_for_end; + } + + // Add vertexes for each point on the line + if (is_use_texture) + { + const auto& uv = shared_data.baked_line_uvs[thickness_integer]; + + const auto uv0 = uv.left_top(); + const auto uv1 = uv.right_bottom(); + for (std::decay_t i = 0; i < path_point_count; ++i) + { + // left-side outer edge + appender.add_vertex(temp_buffer_points[i * 2 + 0], uv0, color); + // right-side outer edge + appender.add_vertex(temp_buffer_points[i * 2 + 1], uv1, color); + } + } + else + { + // If we're not using a texture, we need the center vertex as well + for (std::decay_t i = 0; i < path_point_count; ++i) + { + // center of line + appender.add_vertex(path_point[i], opaque_uv, color); + // left-side outer edge + appender.add_vertex(temp_buffer_points[i * 2 + 0], opaque_uv, transparent_color); + // right-side outer edge + appender.add_vertex(temp_buffer_points[i * 2 + 1], opaque_uv, transparent_color); + } + } + } + else + { + // [PATH 2] Non-texture-based lines (non-thick) + + // we need to draw the solid line core and thus require four vertices per point + const auto half_inner_thickness = (thickness - 1.f) * .5f; + + // If line is not closed, the first and last points need to be generated differently as there are no normals to blend + if (not is_closed) + { + const auto point_last = path_point_count - 1; + temp_buffer_points[0] = path_point[0] + temp_buffer_normals[0] * (half_inner_thickness + 1.f); + temp_buffer_points[1] = path_point[0] + temp_buffer_normals[0] * (half_inner_thickness + 0.f); + temp_buffer_points[2] = path_point[0] - temp_buffer_normals[0] * (half_inner_thickness + 0.f); + temp_buffer_points[3] = path_point[0] - temp_buffer_normals[0] * (half_inner_thickness + 1.f); + temp_buffer_points[point_last * 4 + 0] = path_point[point_last] + temp_buffer_normals[point_last] * (half_inner_thickness + 1.f); + temp_buffer_points[point_last * 4 + 1] = path_point[point_last] + temp_buffer_normals[point_last] * (half_inner_thickness + 0.f); + temp_buffer_points[point_last * 4 + 2] = path_point[point_last] - temp_buffer_normals[point_last] * (half_inner_thickness + 0.f); + temp_buffer_points[point_last * 4 + 3] = path_point[point_last] - temp_buffer_normals[point_last] * (half_inner_thickness + 1.f); + } + + const auto current_vertex_index = static_cast(appender.vertex_count()); + + // Generate the indices to form a number of triangles for each line segment, and the vertices for the line edges + // This takes points n and n+1 and writes into n+1, with the first point in a closed line being generated from the final one (as n+1 wraps) + auto vertex_index_for_start = current_vertex_index; + for (std::decay_t first_point_of_segment = 0; first_point_of_segment < segments_count; ++first_point_of_segment) + { + const auto second_point_of_segment = (first_point_of_segment + 1) % path_point_count; + const auto vertex_index_for_end = static_cast((first_point_of_segment + 1) == path_point_count ? current_vertex_index : (vertex_index_for_start + 4)); + + // Average normals + const auto d = (temp_buffer_normals[first_point_of_segment] + temp_buffer_normals[second_point_of_segment]) * .5f; + const auto [dm_x, dm_y] = to_fixed_normal(d.x, d.y); + const auto dm_out_x = dm_x * (half_inner_thickness + 1.f); + const auto dm_out_y = dm_y * (half_inner_thickness + 1.f); + const auto dm_in_x = dm_x * (half_inner_thickness + 0.f); + const auto dm_in_y = dm_y * (half_inner_thickness + 0.f); + + // Add temporary vertices + temp_buffer_points[second_point_of_segment * 4 + 0] = path_point[second_point_of_segment] + point_type{dm_out_x, dm_out_y}; + temp_buffer_points[second_point_of_segment * 4 + 1] = path_point[second_point_of_segment] + point_type{dm_in_x, dm_in_y}; + temp_buffer_points[second_point_of_segment * 4 + 2] = path_point[second_point_of_segment] - point_type{dm_in_x, dm_in_y}; + temp_buffer_points[second_point_of_segment * 4 + 3] = path_point[second_point_of_segment] - point_type{dm_out_x, dm_out_y}; + + // Add indexes + appender.add_index(vertex_index_for_end + 1, vertex_index_for_end + 1, vertex_index_for_start + 2); + appender.add_index(vertex_index_for_start + 2, vertex_index_for_end + 2, vertex_index_for_end + 1); + appender.add_index(vertex_index_for_end + 1, vertex_index_for_start + 1, vertex_index_for_start + 0); + appender.add_index(vertex_index_for_start + 0, vertex_index_for_end + 0, vertex_index_for_end + 1); + appender.add_index(vertex_index_for_end + 2, vertex_index_for_start + 2, vertex_index_for_start + 3); + appender.add_index(vertex_index_for_start + 3, vertex_index_for_end + 3, vertex_index_for_end + 2); + + vertex_index_for_start = vertex_index_for_end; + } + + // Add vertices + for (std::decay_t i = 0; i < path_point_count; ++i) + { + appender.add_vertex(temp_buffer_points[i * 4 + 0], opaque_uv, transparent_color); + appender.add_vertex(temp_buffer_points[i * 4 + 1], opaque_uv, color); + appender.add_vertex(temp_buffer_points[i * 4 + 2], opaque_uv, color); + appender.add_vertex(temp_buffer_points[i * 4 + 2], opaque_uv, transparent_color); + } + } + } + + auto draw_convex_polygon_line_filled(const color_type color) noexcept -> void + { + const auto path_point_count = path_list.size(); + const auto& path_point = path_list; + + if (path_point_count < 3 or color.alpha == 0) + { + return; + } + + const auto& render_list = self.get(); + const auto& shared_data = render_list.shared_data(); + auto appender = make_appender(); + + const auto vertex_count = path_point_count; + const auto index_count = (path_point_count - 2) * 3; + appender.reserve(vertex_count, index_count); + + const auto current_vertex_index = static_cast(appender.vertex_count()); + const auto& opaque_uv = shared_data.white_pixel_uv; + + std::ranges::for_each( + path_point, + [&](const point_type& point) noexcept -> void + { + appender.add_vertex(point, opaque_uv, color); + } + ); + for (index_type i = 2; std::cmp_less(i, path_point_count); ++i) + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_index + i - 1 >= current_vertex_index); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_index + i >= current_vertex_index); + + appender.add_index(current_vertex_index + 0, current_vertex_index + i - 1, current_vertex_index + i); + } + } + + auto draw_convex_polygon_line_filled_aa(const color_type color) noexcept -> void + { + const auto path_point_count = path_list.size(); + const auto& path_point = path_list; + + if (path_point_count < 3 or color.alpha == 0) + { + return; + } + + const auto& render_list = self.get(); + const auto& shared_data = render_list.shared_data(); + auto appender = make_appender(); + + const auto& opaque_uv = shared_data.white_pixel_uv; + const auto transparent_color = color.transparent(); + + const auto vertex_count = path_point_count * 2; + const auto index_count = (path_point_count - 2) * 3 + path_point_count * 6; + appender.reserve(vertex_count, index_count); + + const auto current_vertex_inner_index = static_cast(appender.vertex_count()); + const auto current_vertex_outer_index = static_cast(appender.vertex_count() + 1); + + // Add indexes for fill + for (index_type i = 2; std::cmp_less(i, path_point_count); ++i) + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_inner_index + static_cast((i - 1) << 1) >= current_vertex_inner_index); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_inner_index + static_cast(i << 1) >= current_vertex_inner_index); + + appender.add_index(current_vertex_inner_index + 0, current_vertex_inner_index + static_cast((i - 1) << 1), current_vertex_inner_index + static_cast(i << 1)); + } + + path_list_type temp_buffer{}; + temp_buffer.resize(path_point_count); + auto temp_buffer_normals = std::span{temp_buffer.begin(), path_point_count}; + + for (auto i = path_point_count - 1, n = static_cast(0); n < path_point_count; i = n++) + { + const auto d = path_point[n] - path_point[i]; + + const auto [normalized_x, normalized_y] = math::normalize(d.x, d.y); + temp_buffer_normals[i].x = normalized_y; + temp_buffer_normals[i].y = -normalized_x; + } + for (auto i = path_point_count - 1, n = static_cast(0); n < path_point_count; i = n++) + { + // Average normals + const auto d = (temp_buffer_normals[n] + temp_buffer_normals[i]) * .5f; + auto [dm_x, dm_y] = to_fixed_normal(d.x, d.y); + dm_x *= .5f; + dm_y *= .5f; + + // inner + appender.add_vertex(path_point[n] - point_type{dm_x, dm_y}, opaque_uv, color); + // outer + appender.add_vertex(path_point[n] + point_type{dm_x, dm_y}, opaque_uv, transparent_color); + + // Add indexes for fringes + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_inner_index + static_cast(n << 1) >= current_vertex_inner_index); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_inner_index + static_cast(i << 1) >= current_vertex_inner_index); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_outer_index + static_cast(i << 1) >= current_vertex_outer_index); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_outer_index + static_cast(i << 1) >= current_vertex_outer_index); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_outer_index + static_cast(n << 1) >= current_vertex_outer_index); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_inner_index + static_cast(n << 1) >= current_vertex_inner_index); + + appender.add_index( + current_vertex_inner_index + static_cast(n << 1), + current_vertex_inner_index + static_cast(i << 1), + current_vertex_outer_index + static_cast(i << 1) + ); + appender.add_index( + current_vertex_outer_index + static_cast(i << 1), + current_vertex_outer_index + static_cast(n << 1), + current_vertex_inner_index + static_cast(n << 1) + ); + } + } + + // clang-format off + auto draw_rect_filled( + const rect_type& rect, + const uv_type& uv, + const color_type color_left_top, + const color_type color_right_top, + const color_type color_left_bottom, + const color_type color_right_bottom + ) noexcept -> void + // clang-format on + { + auto appender = make_appender(); + + // two triangle without path + constexpr size_type vertex_count = 4; + constexpr size_type index_count = 6; + appender.reserve(vertex_count, index_count); + + const auto current_vertex_index = static_cast(appender.vertex_count()); + + appender.add_vertex(rect.left_top(), uv, color_left_top); + appender.add_vertex(rect.right_top(), uv, color_right_top); + appender.add_vertex(rect.right_bottom(), uv, color_right_bottom); + appender.add_vertex(rect.left_bottom(), uv, color_left_bottom); + + appender.add_index(current_vertex_index + 0, current_vertex_index + 1, current_vertex_index + 2); + appender.add_index(current_vertex_index + 0, current_vertex_index + 2, current_vertex_index + 3); + } + + // clang-format off + auto draw_rect_filled( + const rect_type& rect, + const color_type color_left_top, + const color_type color_right_top, + const color_type color_left_bottom, + const color_type color_right_bottom + ) noexcept -> void + // clang-format on + { + const auto& render_list = self.get(); + const auto& shared_data = render_list.shared_data(); + const auto& opaque_uv = shared_data.white_pixel_uv; + + draw_rect_filled(rect, opaque_uv, color_left_top, color_right_top, color_left_bottom, color_right_bottom); + } + + // clang-format off + auto draw_text( + const std::string_view utf8_text, + const std::uint32_t font_size, + const point_type& p, + const color_type color, + const float wrap_width, + const GlyphFlag flag + ) noexcept -> void + // clang-format on + { + // todo: + // The texture used for glyphs may not be the default texture, then we need to switch the texture, + // but at this time, the glyph information may not be written to the texture atlas (e.g. the first frame), then we can't know the ID of the texture atlas + + // note: + // Line break ('\n') have no glyph information (nullptr), meaning we need to skip it + + auto& render_list = self.get(); + auto& render_context = render_list.render_context_.get(); + + const auto utf32_text = chars::convert(utf8_text); + const auto& glyphs = render_context.glyph_of(utf32_text, font_size, flag); + + if (std::ranges::any_of( + glyphs, + [](const auto* glyph) noexcept -> bool + { + return glyph and glyph->texture_atlas_id == invalid_texture_atlas_id; + } + ) + ) + { + // skip this frame? + return; + } + + const auto visible_glyph_count = std::ranges::count_if( + glyphs, + [](const auto* glyph) noexcept -> bool + { + if (glyph == nullptr) + { + return false; + } + + if (not glyph->visible) + { + return false; + } + + return true; + } + ); + + // two triangle without path + const auto vertex_count = std::size_t{4} * visible_glyph_count; + const auto index_count = std::size_t{6} * visible_glyph_count; + auto appender = make_appender(); + appender.reserve(vertex_count, index_count); + + const auto wrap_pos_x = p.x + wrap_width; + const auto line_height = static_cast(font_size); + auto cursor = p + point_type{0, line_height}; + + for (const auto [codepoint, glyph]: std::views::zip(utf32_text, glyphs)) + { + if (glyph == nullptr) + { + if (codepoint == U'\n') + { + cursor.x = p.x; + cursor.y += line_height; + } + } + else if (glyph->visible) + { + const auto& atlas = render_context.atlas_of(*glyph); + + const auto new_texture = render_list.this_command_texture_ != atlas.id(); + + if (new_texture) + { + render_list.push_texture(atlas.id()); + } + + const auto glyph_advance_x = glyph->advance_x; + + if (cursor.x + glyph_advance_x > wrap_pos_x) + { + cursor.x = p.x; + cursor.y += line_height; + } + + const rect_type char_rect + { + cursor + point_type{glyph->rect.left_top().x, -glyph->rect.left_top().y}, + glyph->rect.size() + }; + const auto color_may_colored = glyph->colored ? color.transparent() : color; + const auto current_vertex_index = static_cast(appender.vertex_count()); + + appender.add_vertex(char_rect.left_top(), glyph->uv.left_top(), color_may_colored); + appender.add_vertex(char_rect.right_top(), glyph->uv.right_top(), color_may_colored); + appender.add_vertex(char_rect.right_bottom(), glyph->uv.right_bottom(), color_may_colored); + appender.add_vertex(char_rect.left_bottom(), glyph->uv.left_bottom(), color_may_colored); + + appender.add_index(current_vertex_index + 0, current_vertex_index + 1, current_vertex_index + 2); + appender.add_index(current_vertex_index + 0, current_vertex_index + 2, current_vertex_index + 3); + + if (new_texture) + { + render_list.pop_texture(); + } + + cursor.x += glyph_advance_x; + } + } + } + + // clang-format off + auto draw_text_size( + const std::string_view utf8_text, + const std::uint32_t font_size, + const float wrap_width, + const GlyphFlag flag + ) noexcept -> extent_type + // clang-format on + { + auto& render_list = self.get(); + auto& render_context = render_list.render_context_.get(); + + const auto utf32_text = chars::convert(utf8_text); + const auto& glyphs = render_context.glyph_of(utf32_text, font_size, flag); + + if (std::ranges::any_of( + glyphs, + [](const auto* glyph) noexcept -> bool + { + return glyph and glyph->texture_atlas_id == invalid_texture_atlas_id; + } + ) + ) + { + // skip this frame? + return {0, 0}; + } + + const auto line_height = static_cast(font_size); + + float max_width = 0; + float current_width = 0; + float total_height = line_height; + + for (const auto [codepoint, glyph]: std::views::zip(utf32_text, glyphs)) + { + if (glyph == nullptr) + { + if (codepoint == U'\n') + { + max_width = std::ranges::max(max_width, current_width); + current_width = 0; + total_height += line_height; + } + } + else if (glyph->visible) + { + if (const auto glyph_advance_x = glyph->advance_x; current_width + glyph_advance_x > wrap_width) + { + max_width = std::ranges::max(max_width, current_width); + current_width = glyph_advance_x; + total_height += line_height; + } + else + { + current_width += glyph_advance_x; + } + } + } + + max_width = std::ranges::max(max_width, current_width); + + return {max_width, total_height}; + } + + // clang-format off + auto draw_image( + const texture_id_type texture_id, + const point_type& display_p1, + const point_type& display_p2, + const point_type& display_p3, + const point_type& display_p4, + const uv_type& uv_p1, + const uv_type& uv_p2, + const uv_type& uv_p3, + const uv_type& uv_p4, + const color_type color + ) noexcept -> void + // clang-format on + { + auto& render_list = self.get(); + + const auto new_texture = render_list.this_command_texture_ != texture_id; + + if (new_texture) + { + render_list.push_texture(texture_id); + } + + auto appender = make_appender(); + + // two triangle without path + constexpr size_type vertex_count = 4; + constexpr size_type index_count = 6; + appender.reserve(vertex_count, index_count); + + const auto current_vertex_index = static_cast(appender.vertex_count()); + + appender.add_vertex(display_p1, uv_p1, color); + appender.add_vertex(display_p2, uv_p2, color); + appender.add_vertex(display_p3, uv_p3, color); + appender.add_vertex(display_p4, uv_p4, color); + + appender.add_index(current_vertex_index + 0, current_vertex_index + 1, current_vertex_index + 2); + appender.add_index(current_vertex_index + 0, current_vertex_index + 2, current_vertex_index + 3); + + if (new_texture) + { + render_list.pop_texture(); + } + } + + // clang-format off + auto draw_image_rounded( + const texture_id_type texture_id, + const rect_type& display_rect, + const rect_type& uv_rect, + const color_type color, + float rounding, + RenderFlag flag + ) noexcept -> void + // clang-format on + { + // @see `path_rect` + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(display_rect.valid() and not display_rect.empty()); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(uv_rect.valid() and not uv_rect.empty()); + + if (rounding >= .5f) + { + flag = to_fixed_rect_corner_flag(flag); + + const auto v = (flag & RenderFlag::ROUND_CORNER_TOP) == RenderFlag::ROUND_CORNER_TOP or (flag & RenderFlag::ROUND_CORNER_BOTTOM) == RenderFlag::ROUND_CORNER_BOTTOM; + const auto h = (flag & RenderFlag::ROUND_CORNER_LEFT) == RenderFlag::ROUND_CORNER_LEFT or (flag & RenderFlag::ROUND_CORNER_RIGHT) == RenderFlag::ROUND_CORNER_RIGHT; + + rounding = std::ranges::min(rounding, display_rect.width() * (v ? .5f : 1.f) - 1.f); + rounding = std::ranges::min(rounding, display_rect.height() * (h ? .5f : 1.f) - 1.f); + } + + if (rounding < .5f or (RenderFlag::ROUND_CORNER_MASK & flag) == RenderFlag::ROUND_CORNER_NONE) + { + draw_image( + texture_id, + display_rect.left_top(), + display_rect.right_top(), + display_rect.right_bottom(), + display_rect.left_bottom(), + uv_rect.left_top(), + uv_rect.right_top(), + uv_rect.right_bottom(), + uv_rect.left_bottom(), + color + ); + } + else + { + auto& render_list = self.get(); + + const auto new_texture = render_list.this_command_texture_ != texture_id; + + if (new_texture) + { + render_list.push_texture(texture_id); + } + + const auto rounding_left_top = (flag & RenderFlag::ROUND_CORNER_LEFT_TOP) != RenderFlag::NONE ? rounding : 0; + const auto rounding_right_top = (flag & RenderFlag::ROUND_CORNER_RIGHT_TOP) != RenderFlag::NONE ? rounding : 0; + const auto rounding_left_bottom = (flag & RenderFlag::ROUND_CORNER_LEFT_BOTTOM) != RenderFlag::NONE ? rounding : 0; + const auto rounding_right_bottom = (flag & RenderFlag::ROUND_CORNER_RIGHT_BOTTOM) != RenderFlag::NONE ? rounding : 0; + + path_arc_fast({display_rect.left_top() + point_type{rounding_left_top, rounding_left_top}, rounding_left_top}, RenderArcFlag::Q2_CLOCK_WISH); + path_arc_fast({display_rect.right_top() + point_type{-rounding_right_top, rounding_right_top}, rounding_right_top}, RenderArcFlag::Q1_CLOCK_WISH); + path_arc_fast({display_rect.right_bottom() + point_type{-rounding_right_bottom, -rounding_right_bottom}, rounding_right_bottom}, RenderArcFlag::Q4_CLOCK_WISH); + path_arc_fast({display_rect.left_bottom() + point_type{rounding_left_bottom, -rounding_left_bottom}, rounding_left_bottom}, RenderArcFlag::Q3_CLOCK_WISH); + + const auto before_vertex_count = render_list.vertex_list_.size(); + // draw + path_stroke(color); + const auto after_vertex_count = render_list.vertex_list_.size(); + + // set uv manually + + const auto display_size = display_rect.size(); + const auto uv_size = uv_rect.size(); + const auto scale = uv_size / display_size; + + auto it = render_list.vertex_list_.begin() + static_cast(before_vertex_count); + const auto end = render_list.vertex_list_.begin() + static_cast(after_vertex_count); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(it < end); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(end == render_list.vertex_list_.end()); + + // note: linear uv + const auto uv_min = uv_rect.left_top(); + // const auto uv_max = uv_rect.right_bottom(); + while (it != end) + { + const auto v = uv_min + (it->position - display_rect.left_top()) * scale; + + it->uv = { + // std::ranges::clamp(v.x, uv_min.x, uv_max.x), + v.x, + // std::ranges::clamp(v.y, uv_min.y, uv_max.y) + v.y + }; + it += 1; + } + + if (new_texture) + { + render_list.pop_texture(); + } + } + } + + auto path_clear() noexcept -> void + { + path_list.clear(); + } + + auto path_reserve(const std::size_t size) noexcept -> void + { + path_list.reserve(size); + } + + auto path_reserve_extra(const std::size_t size) noexcept -> void + { + path_reserve(path_list.size() + size); + } + + auto path_pin(const point_type& point) noexcept -> void + { + path_list.push_back(point); + } + + auto path_stroke(const color_type color, const RenderFlag flag, const float thickness) noexcept -> void + { + if (const auto render_list_flag = self.get().render_list_flag_; (render_list_flag & RenderListFlag::ANTI_ALIASED_LINE) != RenderListFlag::NONE) + { + draw_polygon_line_aa(color, flag, thickness); + } + else + { + draw_polygon_line(color, flag, thickness); + } + + path_clear(); + } + + auto path_stroke(const color_type color) noexcept -> void + { + if (const auto render_list_flag = self.get().render_list_flag_; (render_list_flag & RenderListFlag::ANTI_ALIASED_FILL) != RenderListFlag::NONE) + { + draw_convex_polygon_line_filled_aa(color); + } + else + { + draw_convex_polygon_line_filled(color); + } + + path_clear(); + } + + auto path_arc_fast(const circle_type& circle, const int from, const int to) noexcept -> void + { + const auto& [center, radius] = circle; + + if (radius < .5f) + { + path_pin(center); + return; + } + + const auto& render_list = self.get(); + const auto& shared_data = render_list.shared_data(); + + // Calculate arc auto segment step size + auto step = RenderListSharedData::vertex_sample_points_count / shared_data.circle_auto_segment_count(radius); + // Make sure we never do steps larger than one quarter of the circle + step = std::clamp(step, static_cast(1), RenderListSharedData::vertex_sample_points_count / 4); + + const auto sample_range = math::abs(to - from); + const auto next_step = step; + + auto extra_max_sample = false; + if (step > 1) + { + const auto overstep = sample_range % step; + if (overstep > 0) + { + extra_max_sample = true; + + // When we have overstepped to avoid awkwardly looking one long line and one tiny one at the end, + // distribute first step range evenly between them by reducing first step size. + step -= (step - overstep) / 2; + } + + path_reserve_extra(sample_range / step + 1 + (overstep > 0)); + } + else + { + path_reserve_extra(sample_range + 1); + } + + auto sample_index = from; + if (sample_index < 0 or std::cmp_greater_equal(sample_index, RenderListSharedData::vertex_sample_points_count)) + { + sample_index = sample_index % static_cast(RenderListSharedData::vertex_sample_points_count); + if (sample_index < 0) + { + sample_index += static_cast(RenderListSharedData::vertex_sample_points_count); + } + } + + if (to >= from) + { + for (int i = from; i <= to; i += static_cast(step), sample_index += static_cast(step), step = next_step) + { + // a_step is clamped to vertex_sample_points_count, so we have guaranteed that it will not wrap over range twice or more + if (std::cmp_greater_equal(sample_index, RenderListSharedData::vertex_sample_points_count)) + { + sample_index -= static_cast(RenderListSharedData::vertex_sample_points_count); + } + + const auto& sample_point = shared_data.vertex_sample_point(sample_index); + + path_pin({center + sample_point * radius}); + } + } + else + { + for (int i = from; i >= to; i -= static_cast(step), sample_index -= static_cast(step), step = next_step) + { + // a_step is clamped to vertex_sample_points_count, so we have guaranteed that it will not wrap over range twice or more + if (sample_index < 0) + { + sample_index += static_cast(RenderListSharedData::vertex_sample_points_count); + } + + const auto& sample_point = shared_data.vertex_sample_point(sample_index); + + path_pin({center + sample_point * radius}); + } + } + + if (extra_max_sample) + { + auto normalized_max_sample_index = to % static_cast(RenderListSharedData::vertex_sample_points_count); + if (normalized_max_sample_index < 0) + { + normalized_max_sample_index += RenderListSharedData::vertex_sample_points_count; + } + + const auto& sample_point = shared_data.vertex_sample_point(normalized_max_sample_index); + + path_pin({center + sample_point * radius}); + } + } + + auto path_arc_fast(const circle_type& circle, const RenderArcFlag flag) noexcept -> void + { + const auto [from, to] = range_of_arc(flag); + + return path_arc_fast(circle, from, to); + } + + auto path_arc_n(const circle_type& circle, const float from, const float to, const std::uint32_t segments) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(to > from); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(from >= 0); + + const auto& [center, radius] = circle; + + if (radius < .5f) + { + path_pin(center); + return; + } + + path_reserve_extra(segments); + for (std::uint32_t i = 0; i < segments; ++i) + { + const auto a = from + static_cast(i) / static_cast(segments) * (to - from); + path_pin({center + point_type{math::cos(a), math::sin(a)} * radius}); + } + } + + auto path_arc(const circle_type& circle, const float from, const float to) noexcept -> void + { + const auto& [center, radius] = circle; + + if (radius < .5f) + { + path_pin(center); + return; + } + + const auto& render_list = self.get(); + const auto& shared_data = render_list.shared_data(); + + // Automatic segment count + if (radius <= shared_data.arc_fast_radius_cutoff) + { + const auto is_reversed = to < from; + + // We are going to use precomputed values for mid-samples. + // Determine first and last sample in lookup table that belong to the arc + const auto sample_from_f = RenderListSharedData::vertex_sample_points_count * from / (std::numbers::pi_v * 2); + const auto sample_to_f = RenderListSharedData::vertex_sample_points_count * to / (std::numbers::pi_v * 2); + + const auto sample_from = is_reversed ? static_cast(math::floor(sample_from_f)) : static_cast(math::ceil(sample_from_f)); + const auto sample_to = is_reversed ? static_cast(math::ceil(sample_to_f)) : static_cast(math::floor(sample_to_f)); + const auto sample_mid = is_reversed ? static_cast(std::ranges::max(sample_from - sample_to, 0)) : static_cast(std::ranges::max(sample_to - sample_from, 0)); + + const auto segment_from_angle = static_cast(sample_from) * std::numbers::pi_v * 2 / RenderListSharedData::vertex_sample_points_count; + const auto segment_to_angle = static_cast(sample_to) * std::numbers::pi_v * 2 / RenderListSharedData::vertex_sample_points_count; + + const auto emit_start = math::abs(segment_from_angle - from) >= 1e-5f; + const auto emit_end = math::abs(to - segment_to_angle) >= 1e-5f; + + if (emit_start) + { + // The quadrant must be the same, otherwise it is not continuous with the path drawn by `path_arc_fast`. + path_pin({center + point_type{math::cos(from), -math::sin(from)} * radius}); + } + if (sample_mid > 0) + { + path_arc_fast(circle, sample_from, sample_to); + } + if (emit_end) + { + // The quadrant must be the same, otherwise it is not continuous with the path drawn by `path_arc_fast`. + path_pin({center + point_type{math::cos(to), -math::sin(to)} * radius}); + } + } + else + { + const auto arc_length = to - from; + const auto circle_segment_count = shared_data.circle_auto_segment_count(radius); + const auto arc_segment_count = std::ranges::max( + static_cast(math::ceil(static_cast(circle_segment_count) * arc_length / (std::numbers::pi_v * 2))), + static_cast(std::numbers::pi_v * 2 / arc_length) + ); + path_arc_n(circle, from, to, arc_segment_count); + } + } + + auto path_arc_elliptical_n(const ellipse_type& ellipse, const float from, const float to, const std::uint32_t segments) noexcept -> void + { + const auto& [center, radius, rotation] = ellipse; + const auto cos_theta = math::cos(rotation); + const auto sin_theta = math::sin(rotation); + + path_reserve_extra(segments); + for (std::uint32_t i = 0; i < segments; ++i) + { + const auto a = from + static_cast(i) / static_cast(segments) * (to - from); + const auto offset = point_type{math::cos(a), math::sin(a)} * radius; + const auto prime_x = offset.x * cos_theta - offset.y * sin_theta; + const auto prime_y = offset.x * sin_theta + offset.y * cos_theta; + path_pin({center + point_type{prime_x, prime_y}}); + } + } + + auto path_quadrilateral(const point_type& p1, const point_type& p2, const point_type& p3, const point_type& p4) noexcept -> void + { + path_pin(p1); + path_pin(p2); + path_pin(p3); + path_pin(p4); + } + + auto path_rect(const rect_type& rect, float rounding, RenderFlag flag) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(rect.valid() and not rect.empty()); + + if (rounding >= .5f) + { + flag = to_fixed_rect_corner_flag(flag); + + const auto v = (flag & RenderFlag::ROUND_CORNER_TOP) == RenderFlag::ROUND_CORNER_TOP or (flag & RenderFlag::ROUND_CORNER_BOTTOM) == RenderFlag::ROUND_CORNER_BOTTOM; + const auto h = (flag & RenderFlag::ROUND_CORNER_LEFT) == RenderFlag::ROUND_CORNER_LEFT or (flag & RenderFlag::ROUND_CORNER_RIGHT) == RenderFlag::ROUND_CORNER_RIGHT; + + rounding = std::ranges::min(rounding, rect.width() * (v ? .5f : 1.f) - 1.f); + rounding = std::ranges::min(rounding, rect.height() * (h ? .5f : 1.f) - 1.f); + } + + if (rounding < .5f or (RenderFlag::ROUND_CORNER_MASK & flag) == RenderFlag::ROUND_CORNER_NONE) + { + path_quadrilateral(rect.left_top(), rect.right_top(), rect.right_bottom(), rect.left_bottom()); + } + else + { + const auto rounding_left_top = (flag & RenderFlag::ROUND_CORNER_LEFT_TOP) != RenderFlag::NONE ? rounding : 0; + const auto rounding_right_top = (flag & RenderFlag::ROUND_CORNER_RIGHT_TOP) != RenderFlag::NONE ? rounding : 0; + const auto rounding_left_bottom = (flag & RenderFlag::ROUND_CORNER_LEFT_BOTTOM) != RenderFlag::NONE ? rounding : 0; + const auto rounding_right_bottom = (flag & RenderFlag::ROUND_CORNER_RIGHT_BOTTOM) != RenderFlag::NONE ? rounding : 0; + + path_arc_fast({rect.left_top() + point_type{rounding_left_top, rounding_left_top}, rounding_left_top}, RenderArcFlag::Q2_CLOCK_WISH); + path_arc_fast({rect.right_top() + point_type{-rounding_right_top, rounding_right_top}, rounding_right_top}, RenderArcFlag::Q1_CLOCK_WISH); + path_arc_fast({rect.right_bottom() + point_type{-rounding_right_bottom, -rounding_right_bottom}, rounding_right_bottom}, RenderArcFlag::Q4_CLOCK_WISH); + path_arc_fast({rect.left_bottom() + point_type{rounding_left_bottom, -rounding_left_bottom}, rounding_left_bottom}, RenderArcFlag::Q3_CLOCK_WISH); + } + } + + // clang-format off + auto path_bezier_cubic_curve_casteljau( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const point_type& p4, + const float tessellation_tolerance, + const std::size_t level + ) noexcept -> void + // clang-format on + { + const auto dx = p4.x - p1.x; + const auto dy = p4.y - p1.y; + const auto d2 = math::abs((p2.x - p4.x) * dy - (p2.y - p4.y) * dx); + const auto d3 = math::abs((p3.x - p4.x) * dy - (p3.y - p4.y) * dx); + + if (math::pow(d2 + d3, 2) < tessellation_tolerance * (math::pow(dx, 2) + math::pow(dy, 2))) + { + path_pin(p4); + } + else if (level < bezier_curve_casteljau_max_level) + { + const auto p_12 = (p1 + p2) * .5f; + const auto p_23 = (p2 + p3) * .5f; + const auto p_34 = (p3 + p4) * .5f; + const auto p_123 = (p_12 + p_23) * .5f; + const auto p_234 = (p_23 + p_34) * .5f; + const auto p_1234 = (p_123 + p_234) * .5f; + + path_bezier_cubic_curve_casteljau(p1, p_12, p_123, p_1234, tessellation_tolerance, level + 1); + path_bezier_cubic_curve_casteljau(p_1234, p_234, p_34, p4, tessellation_tolerance, level + 1); + } + } + + // clang-format off + auto path_bezier_quadratic_curve_casteljau( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const float tessellation_tolerance, + const std::size_t level + ) noexcept -> void + // clang-format on + { + const auto dx = p3.x - p1.x; + const auto dy = p3.y - p1.y; + const auto det = (p2.x - p3.x) * dy - (p2.y - p3.y) * dx; + + if (math::pow(det, 2) * 4.f < tessellation_tolerance * (math::pow(dx, 2) + math::pow(dy, 2))) + { + path_pin(p3); + } + else if (level < bezier_curve_casteljau_max_level) + { + const auto p_12 = (p1 + p2) * .5f; + const auto p_23 = (p2 + p3) * .5f; + const auto p_123 = (p_12 + p_23) * .5f; + + path_bezier_quadratic_curve_casteljau(p1, p_12, p_123, tessellation_tolerance, level + 1); + path_bezier_quadratic_curve_casteljau(p_123, p_23, p3, tessellation_tolerance, level + 1); + } + } + + auto path_bezier_curve(const point_type& p1, const point_type& p2, const point_type& p3, const point_type& p4, const std::uint32_t segments) noexcept -> void + { + const auto& render_list = self.get(); + const auto& shared_data = render_list.shared_data(); + + path_pin(p1); + if (segments == 0) + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(shared_data.curve_tessellation_tolerance > 0); + + path_reserve_extra(bezier_curve_casteljau_max_level * 2); + // auto-tessellated + path_bezier_cubic_curve_casteljau(p1, p2, p3, p4, shared_data.curve_tessellation_tolerance, 0); + } + else + { + path_reserve_extra(segments); + const auto step = 1.f / static_cast(segments); + for (std::uint32_t i = 1; i <= segments; ++i) + { + path_pin(bezier_cubic_calc(p1, p2, p3, p4, step * static_cast(i))); + } + } + } + + auto path_bezier_quadratic_curve(const point_type& p1, const point_type& p2, const point_type& p3, const std::uint32_t segments) noexcept -> void + { + const auto& render_list = self.get(); + const auto& shared_data = render_list.shared_data(); + + path_pin(p1); + if (segments == 0) + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(shared_data.curve_tessellation_tolerance > 0); + + path_reserve_extra(bezier_curve_casteljau_max_level * 2); + // auto-tessellated + path_bezier_quadratic_curve_casteljau(p1, p2, p3, shared_data.curve_tessellation_tolerance, 0); + } + else + { + path_reserve_extra(segments); + const auto step = 1.f / static_cast(segments); + for (std::uint32_t i = 1; i <= segments; ++i) + { + path_pin(bezier_quadratic_calc(p1, p2, p3, step * static_cast(i))); + } + } + } + }; + + auto RenderList::push_command() noexcept -> void + { + // Fixme: If the window boundary is smaller than the rect boundary, the rect will no longer be valid. + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not this_command_clip_rect_.empty() and this_command_clip_rect_.valid()); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(this_command_texture_ != invalid_texture_id); + + command_list_.emplace_back( + command_type{ + .scissor = this_command_scissor_, + .texture = this_command_texture_, + .index_offset = static_cast(index_list_.size()), + // set by draw_xxx + .element_count = 0 + } + ); + } + + auto RenderList::on_scissor_changed() noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not command_list_.empty()); + + const auto command_count = command_list_.size(); + const auto& [current_scissor, current_texture, current_index_offset, current_element_count] = command_list_.back(); + + if (current_element_count != 0) + { + if (current_scissor != this_command_scissor_) + { + push_command(); + + return; + } + } + + // try to merge with previous command if it matches, else use current command + if (command_count > 1) + { + if (const auto& [previous_scissor, previous_texture, previous_index_offset, previous_element_count] = command_list_[command_count - 2]; + current_element_count == 0 and (this_command_scissor_ == previous_scissor and this_command_texture_ == previous_texture) and + // sequential + current_index_offset == previous_index_offset + previous_element_count) + { + command_list_.pop_back(); + return; + } + } + + command_list_.back().scissor = this_command_scissor_; + } + + auto RenderList::on_texture_changed() noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not command_list_.empty()); + + const auto command_count = command_list_.size(); + const auto& [current_scissor, current_texture, current_index_offset, current_element_count] = command_list_.back(); + + if (current_element_count != 0) + { + if (current_texture != this_command_texture_) + { + push_command(); + + return; + } + } + + // try to merge with previous command if it matches, else use current command + if (command_count > 1) + { + if (const auto& [previous_scissor, previous_texture, previous_index_offset, previous_element_count] = command_list_[command_count - 2]; + current_element_count == 0 and (this_command_scissor_ == previous_scissor and this_command_texture_ == previous_texture) and + // sequential + current_index_offset == previous_index_offset + previous_element_count) + { + command_list_.pop_back(); + return; + } + } + + command_list_.back().texture = this_command_texture_; + } + + auto RenderList::shared_data() const noexcept -> const RenderListSharedData& + { + return render_context_.get().render_list_shared_data(); + } + + auto RenderList::default_texture() const noexcept -> texture_id_type + { + return render_context_.get().root_texture(); + } + + RenderList::RenderList(const RenderListFlag flag, RenderContext& render_context) noexcept + : render_list_flag_{flag}, + render_context_{render_context}, + this_command_scissor_{0, 0, 0, 0}, + this_command_texture_{invalid_texture_id} {} + + auto RenderList::reset() noexcept -> void + { + command_list_.clear(); + vertex_list_.clear(); + index_list_.clear(); + + // we don't know the size of the clip rect, so we need the user to set it + this_command_scissor_ = {}; + // the first texture is always the (default) font texture + this_command_texture_ = default_texture(); + + // we always have a command ready in the buffer + command_list_.emplace_back( + command_type{ + .scissor = this_command_scissor_, + .texture = this_command_texture_, + .index_offset = static_cast(index_list_.size()), + // set by subsequent draw_xxx + .element_count = 0, + } + ); + } + + auto RenderList::render_data() const noexcept -> RenderData + { + return {.vertex_list = vertex_list_, .index_list = index_list_, .command_list = command_list_}; + } + + auto RenderList::push_scissor(const rect_type& rect, const bool intersect_with_current_scissor) noexcept -> rect_type& + { + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not rect.empty() and rect.valid()); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not command_list_.empty()); + + const auto& [current_scissor, current_texture, current_index_offset, current_element_count] = command_list_.back(); + + this_command_scissor_ = intersect_with_current_scissor ? rect.combine_min(current_scissor) : rect; + + on_scissor_changed(); + return command_list_.back().scissor; + } + + auto RenderList::pop_scissor() noexcept -> void + { + // at least one command + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(command_list_.size() > 1); + this_command_scissor_ = command_list_[command_list_.size() - 2].scissor; + + on_scissor_changed(); + } + + auto RenderList::push_texture(const texture_id_type texture) noexcept -> void + { + this_command_texture_ = texture; + + on_texture_changed(); + } + + auto RenderList::pop_texture() noexcept -> void + { + // at least one command + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(command_list_.size() > 1); + this_command_texture_ = command_list_[command_list_.size() - 2].texture; + + on_texture_changed(); + } + + // clang-format off + auto RenderList::line( + const point_type& from, + const point_type& to, + const color_type color, + const float thickness + ) noexcept -> void + // clang-format on + { + if (color.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_pin(from); + drawer.path_pin(to); + + drawer.path_stroke(color, RenderFlag::NONE, thickness); + } + + // clang-format off + auto RenderList::triangle( + const point_type& a, + const point_type& b, + const point_type& c, + const color_type color, + const float thickness + ) noexcept -> void + // clang-format on + { + if (color.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_pin(a); + drawer.path_pin(b); + drawer.path_pin(c); + + drawer.path_stroke(color, RenderFlag::CLOSED, thickness); + } + + // clang-format off + auto RenderList::triangle_filled( + const point_type& a, + const point_type& b, + const point_type& c, + const color_type color + ) noexcept -> void + // clang-format on + { + if (color.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_pin(a); + drawer.path_pin(b); + drawer.path_pin(c); + + drawer.path_stroke(color); + } + + // clang-format off + auto RenderList::rect( + const rect_type& rect, + const color_type color, + const float rounding, + const RenderFlag flag, + const float thickness + ) noexcept -> void + // clang-format on + { + if (color.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_rect(rect, rounding, flag); + + drawer.path_stroke(color, RenderFlag::CLOSED, thickness); + } + + // clang-format off + auto RenderList::rect( + const point_type& left_top, + const point_type& right_bottom, + const color_type color, + const float rounding, + const RenderFlag flag, + const float thickness + ) noexcept -> void + // clang-format on + { + return rect({left_top, right_bottom}, color, rounding, flag, thickness); + } + + // clang-format off + auto RenderList::rect_filled( + const rect_type& rect, + const color_type color, + const float rounding, + const RenderFlag flag + ) noexcept -> void + // clang-format on + { + if (color.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + if (rounding < .5f or (RenderFlag::ROUND_CORNER_MASK & flag) == RenderFlag::ROUND_CORNER_NONE) + { + drawer.draw_rect_filled(rect, color, color, color, color); + } + else + { + drawer.path_rect(rect, rounding, flag); + drawer.path_stroke(color); + } + } + + // clang-format off + auto RenderList::rect_filled( + const point_type& left_top, + const point_type& right_bottom, + const color_type color, + const float rounding, + const RenderFlag flag + ) noexcept -> void + // clang-format on + { + return rect_filled({left_top, right_bottom}, color, rounding, flag); + } + + // clang-format off + auto RenderList::rect_filled( + const rect_type& rect, + const color_type color_left_top, + const color_type color_right_top, + const color_type color_left_bottom, + const color_type color_right_bottom + ) noexcept -> void + // clang-format on + { + if (color_left_top.alpha == 0 or color_right_top.alpha == 0 or color_left_bottom.alpha == 0 or color_right_bottom.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.draw_rect_filled(rect, color_left_top, color_right_top, color_left_bottom, color_right_bottom); + } + + // clang-format off + auto RenderList::rect_filled( + const point_type& left_top, + const point_type& right_bottom, + const color_type color_left_top, + const color_type color_right_top, + const color_type color_left_bottom, + const color_type color_right_bottom + ) noexcept -> void + // clang-format on + { + return rect_filled({left_top, right_bottom}, color_left_top, color_right_top, color_left_bottom, color_right_bottom); + } + + // clang-format off + auto RenderList::quadrilateral( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const point_type& p4, + const color_type color, + const float thickness + ) noexcept -> void + // clang-format on + { + if (color.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_quadrilateral(p1, p2, p3, p4); + + drawer.path_stroke(color, RenderFlag::CLOSED, thickness); + } + + // clang-format off + auto RenderList::quadrilateral_filled( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const point_type& p4, + const color_type color + ) noexcept -> void + // clang-format on + { + if (color.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_quadrilateral(p1, p2, p3, p4); + + drawer.path_stroke(color); + } + + // clang-format off + auto RenderList::circle_n( + const circle_type& circle, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept -> void + // clang-format on + { + if (color.alpha == 0 or circle.radius < .5f or segments < 3) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_arc_n(circle, 0, std::numbers::pi_v * 2, segments); + + drawer.path_stroke(color, RenderFlag::CLOSED, thickness); + } + + // clang-format off + auto RenderList::circle_n( + const point_type& center, + const float radius, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept -> void + // clang-format on + { + return circle_n({center, radius}, color, segments, thickness); + } + + // clang-format off + auto RenderList::ellipse_n( + const ellipse_type& ellipse, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept -> void + // clang-format on + { + if (color.alpha == 0 or ellipse.radius.width < .5f or ellipse.radius.height < .5f or segments < 3) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_arc_elliptical_n(ellipse, 0, std::numbers::pi_v * 2, segments); + + drawer.path_stroke(color, RenderFlag::CLOSED, thickness); + } + + // clang-format off + void RenderList::ellipse_n( + const point_type& center, + const extent_type& radius, + const float rotation, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept + // clang-format on + { + return ellipse_n({center, radius, rotation}, color, segments, thickness); + } + + // clang-format off + auto RenderList::circle_n_filled( + const circle_type& circle, + const color_type color, + const std::uint32_t segments + ) noexcept -> void + // clang-format on + { + if (color.alpha == 0 or circle.radius < .5f or segments < 3) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_arc_n(circle, 0, std::numbers::pi_v * 2, segments); + + drawer.path_stroke(color); + } + + // clang-format off + auto RenderList::circle_n_filled( + const point_type& center, + const float radius, + const color_type color, + const std::uint32_t segments + ) noexcept -> void + // clang-format on + { + return circle_n_filled({center, radius}, color, segments); + } + + // clang-format off + auto RenderList::ellipse_n_filled( + const ellipse_type& ellipse, + const color_type color, + const std::uint32_t segments + ) noexcept -> void + // clang-format on + { + if (color.alpha == 0 or ellipse.radius.width < .5f or ellipse.radius.height < .5f or segments < 3) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_arc_elliptical_n(ellipse, 0, std::numbers::pi_v * 2, segments); + + drawer.path_stroke(color); + } + + // clang-format off + auto RenderList::ellipse_n_filled( + const point_type& center, + const extent_type& radius, + const float rotation, + const color_type color, + const std::uint32_t segments + ) noexcept -> void + // clang-format on + { + return ellipse_n_filled({center, radius, rotation}, color, segments); + } + + // clang-format off + void RenderList::circle( + const circle_type& circle, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept + // clang-format on + { + if (color.alpha == 0 or circle.radius < .5f) + { + return; + } + + if (segments == 0) + { + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_arc_fast(circle, 0, RenderListSharedData::vertex_sample_points_count - 1); + + drawer.path_stroke(color, RenderFlag::CLOSED, thickness); + } + else + { + const auto clamped_segments = std::ranges::clamp(segments, RenderListSharedData::circle_segments_min, RenderListSharedData::circle_segments_max); + + circle_n(circle, color, clamped_segments, thickness); + } + } + + // clang-format off + auto RenderList::circle( + const point_type& center, + const float radius, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept -> void + // clang-format on + { + return circle({center, radius}, color, segments, thickness); + } + + // clang-format off + auto RenderList::circle_filled( + const circle_type& circle, + const color_type color, + const std::uint32_t segments + ) noexcept -> void + // clang-format on + { + if (color.alpha == 0 or circle.radius < .5f) + { + return; + } + + if (segments == 0) + { + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_arc_fast(circle, 0, RenderListSharedData::vertex_sample_points_count - 1); + + drawer.path_stroke(color); + } + else + { + const auto clamped_segments = std::ranges::clamp(segments, RenderListSharedData::circle_segments_min, RenderListSharedData::circle_segments_max); + + circle_n_filled(circle, color, clamped_segments); + } + } + + // clang-format off + auto RenderList::circle_filled( + const point_type& center, + const float radius, + const color_type color, + const std::uint32_t segments + ) noexcept -> void + // clang-format on + { + circle_filled({center, radius}, color, segments); + } + + // clang-format off + auto RenderList::ellipse( + const ellipse_type& ellipse, + const color_type color, + std::uint32_t segments, + const float thickness + ) noexcept -> void + // clang-format on + { + if (color.alpha == 0 or ellipse.radius.width < .5f or ellipse.radius.height < .5f) + { + return; + } + + if (segments == 0) + { + // fixme + segments = shared_data().circle_auto_segment_count(std::ranges::max(ellipse.radius.width, ellipse.radius.height)); + } + + ellipse_n(ellipse, color, segments, thickness); + } + + // clang-format off + auto RenderList::ellipse( + const point_type& center, + const extent_type& radius, + const float rotation, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept -> void + // clang-format on + { + return ellipse({center, radius, rotation}, color, segments, thickness); + } + + // clang-format off + auto RenderList::ellipse_filled( + const ellipse_type& ellipse, + const color_type color, + std::uint32_t segments + ) noexcept -> void + // clang-format on + { + if (color.alpha == 0 or ellipse.radius.width < .5f or ellipse.radius.height < .5f) + { + return; + } + + if (segments == 0) + { + // fixme + segments = shared_data().circle_auto_segment_count(std::ranges::max(ellipse.radius.width, ellipse.radius.height)); + } + + ellipse_n_filled(ellipse, color, segments); + } + + // clang-format off + auto RenderList::ellipse_filled( + const point_type& center, + const extent_type& radius, + const float rotation, + const color_type color, + const std::uint32_t segments + ) noexcept -> void + // clang-format on + { + return ellipse_filled({center, radius, rotation}, color, segments); + } + + // clang-format off + auto RenderList::bezier_cubic( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const point_type& p4, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept -> void + // clang-format on + { + if (color.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_bezier_curve(p1, p2, p3, p4, segments); + + drawer.path_stroke(color, RenderFlag::NONE, thickness); + } + + // clang-format off + auto RenderList::bezier_quadratic( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept -> void + // clang-format on + { + if (color.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_bezier_quadratic_curve(p1, p2, p3, segments); + + drawer.path_stroke(color, RenderFlag::NONE, thickness); + } + + // clang-format off + auto RenderList::text( + const std::string_view utf8_text, + const std::uint32_t font_size, + const point_type& point, + const color_type color, + const float wrap_width + ) noexcept -> void + // clang-format on + { + return this->text(utf8_text, font_size, point, color, GlyphFlag::NONE, wrap_width); + } + + // clang-format off + auto RenderList::text( + const std::string_view utf8_text, + const std::uint32_t font_size, + const point_type& point, + const color_type color, + const GlyphFlag flag, + const float wrap_width + ) noexcept -> void + // clang-format on + { + if (color.alpha == 0) + { + return; + } + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(wrap_width > 0); + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.draw_text(utf8_text, font_size, point, color, wrap_width, flag); + } + + // clang-format off + auto RenderList::text_size( + const std::string_view utf8_text, + const std::uint32_t font_size, + const float wrap_width + ) noexcept -> extent_type + // clang-format on + { + return this->text_size(utf8_text, font_size, GlyphFlag::NONE, wrap_width); + } + + // clang-format off + auto RenderList::text_size( + const std::string_view utf8_text, + const std::uint32_t font_size, + const GlyphFlag flag, + const float wrap_width + ) noexcept -> extent_type + // clang-format on + { + Drawer drawer{.self = *this, .path_list = {}}; + + return drawer.draw_text_size(utf8_text, font_size, wrap_width, flag); + } + + // clang-format off + auto RenderList::image( + const texture_id_type texture_id, + const point_type& display_p1, + const point_type& display_p2, + const point_type& display_p3, + const point_type& display_p4, + const uv_type& uv_p1, + const uv_type& uv_p2, + const uv_type& uv_p3, + const uv_type& uv_p4, + const color_type color + ) noexcept -> void + // clang-format on + { + if (color.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.draw_image(texture_id, display_p1, display_p2, display_p3, display_p4, uv_p1, uv_p2, uv_p3, uv_p4, color); + } + + // clang-format off + auto RenderList::image( + const texture_id_type texture_id, + const rect_type& display_rect, + const rect_type& uv_rect, + const color_type color + ) noexcept -> void + // clang-format on + { + image(texture_id, + display_rect.left_top(), + display_rect.right_top(), + display_rect.right_bottom(), + display_rect.left_bottom(), + uv_rect.left_top(), + uv_rect.right_top(), + uv_rect.right_bottom(), + uv_rect.left_bottom(), + color); + } + + // clang-format off + auto RenderList::image( + const texture_id_type texture_id, + const point_type& display_left_top, + const point_type& display_right_bottom, + const uv_type& uv_left_top, + const uv_type& uv_right_bottom, + const color_type color + ) noexcept -> void + // clang-format on + { + image(texture_id, {display_left_top, display_right_bottom}, {uv_left_top, uv_right_bottom}, color); + } + + // clang-format off + auto RenderList::image_rounded( + const texture_id_type texture_id, + const rect_type& display_rect, + const float rounding, + const RenderFlag flag, + const rect_type& uv_rect, + const color_type color + ) noexcept -> void + // clang-format on + { + if (color.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.draw_image_rounded(texture_id, display_rect, uv_rect, color, rounding, flag); + } + + // clang-format off + auto RenderList::image_rounded( + const texture_id_type texture_id, + const point_type& display_left_top, + const point_type& display_right_bottom, + const float rounding, + const RenderFlag flag, + const uv_type& uv_left_top, + const uv_type& uv_right_bottom, + const color_type color + ) noexcept -> void + // clang-format on + { + image_rounded(texture_id, {display_left_top, display_right_bottom}, rounding, flag, {uv_left_top, uv_right_bottom}, color); + } +} // namespace gal::prometheus::gfx diff --git a/src/gfx/old/render_list.hpp b/src/gfx/old/render_list.hpp new file mode 100644 index 00000000..ace212b8 --- /dev/null +++ b/src/gfx/old/render_list.hpp @@ -0,0 +1,475 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include + +#include + +namespace gal::prometheus::gfx +{ + class RenderListSharedData + { + public: + using circle_segment_count_type = std::uint8_t; + constexpr static std::size_t circle_segment_counts_count = 64; + using circle_segment_counts_type = std::array; + + constexpr static std::uint32_t circle_segments_min = 4; + constexpr static std::uint32_t circle_segments_max = 512; + + constexpr static std::size_t vertex_sample_points_count = 48; + using vertex_sample_points_type = std::array; + + constexpr static std::size_t baked_line_uv_count = 64; + using baked_line_uvs_type = std::array; + + circle_segment_counts_type circle_segment_counts; + + vertex_sample_points_type vertex_sample_points; + + baked_line_uvs_type baked_line_uvs; + point_type white_pixel_uv; + + // Maximum error (in pixels) allowed when using `circle`/`circle_filled` or drawing rounded corner rectangles with no explicit segment count specified. + // Decrease for higher quality but more geometry. + float circle_segment_max_error; + // Cutoff radius after which arc drawing will fall back to slower `path_arc` + float arc_fast_radius_cutoff; + // Tessellation tolerance when using `path_bezier_curve` without a specific number of segments. + // Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. + float curve_tessellation_tolerance; + + // -------------------------------------------------- + + RenderListSharedData() noexcept; + + // -------------------------------------------------- + + [[nodiscard]] auto circle_auto_segment_count(float radius) const noexcept -> circle_segment_count_type; + + [[nodiscard]] auto vertex_sample_point(std::size_t index) const noexcept -> const point_type&; + + // -------------------------------------------------- + + auto set_circle_tessellation_max_error(float max_error) noexcept -> void; + + auto set_curve_tessellation_tolerance(float tolerance) noexcept -> void; + }; + + class RenderData final + { + public: + using size_type = std::uint32_t; + + struct [[nodiscard]] command_type + { + rect_type scissor; + texture_id_type texture; + + // ======================= + + // set by RenderList::index_list.size() + // start offset in @c RenderList::index_list + size_type index_offset; + // set by RenderList::draw_xxx + // number of indices (multiple of 3) to be rendered as triangles + size_type element_count; + }; + + using vertex_list_type = std::vector; + using index_list_type = std::vector; + using command_list_type = std::vector; + + std::reference_wrapper vertex_list; + std::reference_wrapper index_list; + std::reference_wrapper command_list; + }; + + class RenderList final + { + public: + using size_type = RenderData::size_type; + + using command_type = RenderData::command_type; + + using vertex_list_type = RenderData::vertex_list_type; + using index_list_type = RenderData::index_list_type; + using command_list_type = RenderData::command_list_type; + + constexpr static float text_no_auto_wrap = 9999999.f; + + private: + class Drawer; + + RenderListFlag render_list_flag_; + memory::RefWrapper render_context_; + + // vertex_list: v1-v2-v3-v4 + v5-v6-v7-v8 + v9-v10-v11 => rect0 + rect1(clipped by rect0) + triangle0(clipped by rect1) + // index_list: 0/1/2-0/2/3 + 4/5/6-4/6/7 + 8/9/10 + // command_list: + // 0: .scissor = {0, 0, root_window_width, root_window_height}, .index_offset = 0, .element_count = root_window_element_count + 6 (two triangles => 0/1/2-0/2/3) + // 1: .scissor = {max(rect0.left, rect1.left), max(rect0.top, rect1.top), min(rect0.right, rect1.right), min(rect0.bottom, rect1.bottom)}, .index_offset = root_window_element_count + 6, .element_count = 6 (two triangles => 4/5/6-4/6/7) + // 2: .scissor = {...}, .index_offset = root_window_element_count + 12, .element_count = 3 (one triangle => 8/9/10) + command_list_type command_list_; + vertex_list_type vertex_list_; + index_list_type index_list_; + + rect_type this_command_scissor_; + texture_id_type this_command_texture_; + + auto push_command() noexcept -> void; + + auto on_scissor_changed() noexcept -> void; + auto on_texture_changed() noexcept -> void; + + [[nodiscard]] auto shared_data() const noexcept -> const RenderListSharedData&; + + [[nodiscard]] auto default_texture() const noexcept -> texture_id_type; + + public: + RenderList(RenderListFlag flag, RenderContext& render_context) noexcept; + + auto reset() noexcept -> void; + + // ---------------------------------------------------------------------------- + // RENDER DATA + + [[nodiscard]] auto render_data() const noexcept -> RenderData; + + // ---------------------------------------------------------------------------- + // SCISSOR & TEXTURE + + auto push_scissor(const rect_type& rect, bool intersect_with_current_scissor) noexcept -> rect_type&; + + auto pop_scissor() noexcept -> void; + + auto push_texture(texture_id_type texture) noexcept -> void; + + auto pop_texture() noexcept -> void; + + // clang-format off + + // ---------------------------------------------------------------------------- + // PRIMITIVE + + auto line( + const point_type& from, + const point_type& to, + color_type color, + float thickness = 1.f + ) noexcept -> void; + + auto triangle( + const point_type& a, + const point_type& b, + const point_type& c, + color_type color, + float thickness = 1.f + ) noexcept -> void; + + auto triangle_filled( + const point_type& a, + const point_type& b, + const point_type& c, + color_type color + ) noexcept -> void; + + auto rect( + const rect_type& rect, + color_type color, + float rounding = .0f, + RenderFlag flag = RenderFlag::ROUND_CORNER_ALL, + float thickness = 1.f + ) noexcept -> void; + + auto rect( + const point_type& left_top, + const point_type& right_bottom, + color_type color, + float rounding = .0f, + RenderFlag flag = RenderFlag::ROUND_CORNER_ALL, + float thickness = 1.f + ) noexcept -> void; + + auto rect_filled( + const rect_type& rect, + color_type color, + float rounding = .0f, + RenderFlag flag = RenderFlag::ROUND_CORNER_ALL + ) noexcept -> void; + + auto rect_filled( + const point_type& left_top, + const point_type& right_bottom, + color_type color, + float rounding = .0f, + RenderFlag flag = RenderFlag::ROUND_CORNER_ALL + ) noexcept -> void; + + auto rect_filled( + const rect_type& rect, + color_type color_left_top, + color_type color_right_top, + color_type color_left_bottom, + color_type color_right_bottom + ) noexcept -> void; + + auto rect_filled( + const point_type& left_top, + const point_type& right_bottom, + color_type color_left_top, + color_type color_right_top, + color_type color_left_bottom, + color_type color_right_bottom + ) noexcept -> void; + + auto quadrilateral( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const point_type& p4, + color_type color, + float thickness = 1.f + ) noexcept -> void; + + auto quadrilateral_filled( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const point_type& p4, + color_type color + ) noexcept -> void; + + auto circle_n( + const circle_type& circle, + color_type color, + std::uint32_t segments, + float thickness = 1.f + ) noexcept -> void; + + auto circle_n( + const point_type& center, + float radius, + color_type color, + std::uint32_t segments, + float thickness = 1.f + ) noexcept -> void; + + auto ellipse_n( + const ellipse_type& ellipse, + color_type color, + std::uint32_t segments, + float thickness = 1.f + ) noexcept -> void; + + auto ellipse_n( + const point_type& center, + const extent_type& radius, + float rotation, + color_type color, + std::uint32_t segments, + float thickness = 1.f + ) noexcept -> void; + + auto circle_n_filled( + const circle_type& circle, + color_type color, + std::uint32_t segments + ) noexcept -> void; + + auto circle_n_filled( + const point_type& center, + float radius, + color_type color, + std::uint32_t segments + ) noexcept -> void; + + auto ellipse_n_filled( + const ellipse_type& ellipse, + color_type color, + std::uint32_t segments + ) noexcept -> void; + + auto ellipse_n_filled( + const point_type& center, + const extent_type& radius, + float rotation, + color_type color, + std::uint32_t segments + ) noexcept -> void; + + auto circle( + const circle_type& circle, + color_type color, + std::uint32_t segments = 0, + float thickness = 1.f + ) noexcept -> void; + + auto circle( + const point_type& center, + float radius, + color_type color, + std::uint32_t segments = 0, + float thickness = 1.f + ) noexcept -> void; + + auto circle_filled( + const circle_type& circle, + color_type color, + std::uint32_t segments = 0 + ) noexcept -> void; + + auto circle_filled( + const point_type& center, + float radius, + color_type color, + std::uint32_t segments = 0 + ) noexcept -> void; + + auto ellipse( + const ellipse_type& ellipse, + color_type color, + std::uint32_t segments = 0, + float thickness = 1.f + ) noexcept -> void; + + auto ellipse( + const point_type& center, + const extent_type& radius, + float rotation, + color_type color, + std::uint32_t segments = 0, + float thickness = 1.f + ) noexcept -> void; + + auto ellipse_filled( + const ellipse_type& ellipse, + color_type color, + std::uint32_t segments = 0 + ) noexcept -> void; + + auto ellipse_filled( + const point_type& center, + const extent_type& radius, + float rotation, + color_type color, + std::uint32_t segments = 0 + ) noexcept -> void; + + auto bezier_cubic( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const point_type& p4, + color_type color, + std::uint32_t segments = 0, + float thickness = 1.f + ) noexcept -> void; + + auto bezier_quadratic( + const point_type& p1, + const point_type& p2, + const point_type& p3, + color_type color, + std::uint32_t segments = 0, + float thickness = 1.f + ) noexcept -> void; + + // ---------------------------------------------------------------------------- + // TEXT + + auto text( + std::string_view utf8_text, + std::uint32_t font_size, + const point_type& point, + color_type color, + float wrap_width = text_no_auto_wrap + ) noexcept -> void; + + auto text( + std::string_view utf8_text, + std::uint32_t font_size, + const point_type& point, + color_type color, + GlyphFlag flag, + float wrap_width = text_no_auto_wrap + ) noexcept -> void; + + // fixme: There is no suitable place to implement this function, this function must be highly consistent with the implementation when drawing text + auto text_size( + std::string_view utf8_text, + std::uint32_t font_size, + float wrap_width = text_no_auto_wrap + ) noexcept -> extent_type; + + // fixme: There is no suitable place to implement this function, this function must be highly consistent with the implementation when drawing text + auto text_size( + std::string_view utf8_text, + std::uint32_t font_size, + GlyphFlag flag, + float wrap_width = text_no_auto_wrap + ) noexcept -> extent_type; + + // ---------------------------------------------------------------------------- + // IMAGE + + // p1________ p2 + // | | + // | | + // p4|_______| p3 + auto image( + texture_id_type texture_id, + const point_type& display_p1, + const point_type& display_p2, + const point_type& display_p3, + const point_type& display_p4, + const uv_type& uv_p1 = {0, 0}, + const uv_type& uv_p2 = {1, 0}, + const uv_type& uv_p3 = {1, 1}, + const uv_type& uv_p4 = {0, 1}, + color_type color = primitive::colors::white + ) noexcept -> void; + + auto image( + texture_id_type texture_id, + const rect_type& display_rect, + const rect_type& uv_rect = {0, 0, 1, 1}, + color_type color = primitive::colors::white + ) noexcept -> void; + + auto image( + texture_id_type texture_id, + const point_type& display_left_top, + const point_type& display_right_bottom, + const uv_type& uv_left_top = {0, 0}, + const uv_type& uv_right_bottom = {1, 1}, + color_type color = primitive::colors::white + ) noexcept -> void; + + auto image_rounded( + texture_id_type texture_id, + const rect_type& display_rect, + float rounding = .0f, + RenderFlag flag = RenderFlag::NONE, + const rect_type& uv_rect = {0, 0, 1, 1}, + color_type color = primitive::colors::white + ) noexcept -> void; + + auto image_rounded( + texture_id_type texture_id, + const point_type& display_left_top, + const point_type& display_right_bottom, + float rounding = .0f, + RenderFlag flag = RenderFlag::NONE, + const uv_type& uv_left_top = {0, 0}, + const uv_type& uv_right_bottom = {1, 1}, + color_type color = primitive::colors::white + ) noexcept -> void; + + // clang-format on + }; +} // namespace gal::prometheus::gfx diff --git a/src/gfx/old/renderer.cpp b/src/gfx/old/renderer.cpp new file mode 100644 index 00000000..1b413b25 --- /dev/null +++ b/src/gfx/old/renderer.cpp @@ -0,0 +1,51 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include +#include + +namespace gal::prometheus::gfx +{ + Renderer::~Renderer() noexcept = default; + + auto Renderer::create() noexcept -> bool + { + return do_create(); + } + + auto Renderer::destroy() noexcept -> void + { + return do_destroy(); + } + + auto Renderer::ready() const noexcept -> bool + { + return do_ready(); + } + + auto Renderer::create_texture(const Texture::data_view_type data, const Texture::size_type size) noexcept -> texture_id_type + { + return do_create_texture(data, size); + } + + auto Renderer::update_texture(const Texture& texture) noexcept -> void + { + do_update_texture(texture); + } + + auto Renderer::destroy_texture(const texture_id_type texture_id) noexcept -> void + { + do_destroy_texture(texture_id); + } + + // ReSharper disable once CppParameterMayBeConstPtrOrRef + auto Renderer::present(RenderContext& renderer_context, const rect_type& display_area) noexcept -> void + { + // todo: We can explicitly upload all glyphs to texture here, so that we don't need to call TextureContext::upload_all_font_face in RenderContext::end_frame + // renderer_context.upload_all_font_face(); + + do_present(renderer_context, display_area); + } +} diff --git a/src/gfx/old/renderer.hpp b/src/gfx/old/renderer.hpp new file mode 100644 index 00000000..489cb357 --- /dev/null +++ b/src/gfx/old/renderer.hpp @@ -0,0 +1,49 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include + +namespace gal::prometheus::gfx +{ + class Renderer + { + public: + Renderer(const Renderer&) noexcept = delete; + Renderer(Renderer&&) noexcept = default; + auto operator=(const Renderer&) noexcept -> Renderer& = delete; + auto operator=(Renderer&&) noexcept -> Renderer& = default; + + virtual ~Renderer() noexcept; + + protected: + Renderer() noexcept = default; + + public: + auto create() noexcept -> bool; + auto destroy() noexcept -> void; + + [[nodiscard]] auto ready() const noexcept -> bool; + + auto create_texture(Texture::data_view_type data, Texture::size_type size) noexcept -> texture_id_type; + auto update_texture(const Texture& texture) noexcept -> void; + auto destroy_texture(texture_id_type texture_id) noexcept -> void; + + auto present(RenderContext& renderer_context, const rect_type& display_area) noexcept -> void; + + private: + [[nodiscard]] virtual auto do_create() noexcept -> bool = 0; + virtual auto do_destroy() noexcept -> void = 0; + + [[nodiscard]] virtual auto do_ready() const noexcept -> bool = 0; + + virtual auto do_create_texture(Texture::data_view_type data, Texture::size_type size) noexcept -> texture_id_type = 0; + virtual auto do_update_texture(const Texture& texture) noexcept -> void = 0; + virtual auto do_destroy_texture(texture_id_type texture_id) noexcept -> void = 0; + + virtual auto do_present(const RenderContext& render_context, const rect_type& display_area) noexcept -> void = 0; + }; +} // namespace gal::prometheus::gfx diff --git a/src/gfx/old/renderer_dx11.cpp b/src/gfx/old/renderer_dx11.cpp new file mode 100644 index 00000000..776f3317 --- /dev/null +++ b/src/gfx/old/renderer_dx11.cpp @@ -0,0 +1,840 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +#if defined(GAL_PROMETHEUS_GFX_RENDER_DX11) + +#include + +#if GAL_PROMETHEUS_COMPILER_DEBUG +#define GAL_PROMETHEUS_GFX_DEBUG +#include +#endif + +#include + +#include +#include +#include +#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE + +#include +#include + +namespace +{ + using namespace gal::prometheus; + using namespace gfx; + + auto check_hr_error( + const HRESULT result +#if defined(GAL_PROMETHEUS_GFX_DEBUG) + , + const std::source_location& location = std::source_location::current() +#endif + ) noexcept -> bool + { + if (SUCCEEDED(result)) + { + return true; + } + +#if defined(GAL_PROMETHEUS_GFX_DEBUG) + + const _com_error err{result}; + std::println(stderr, "Error: {} --- at {}:{}", err.ErrorMessage(), location.file_name(), location.line()); + + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + +#else + + GAL_PROMETHEUS_COMPILER_UNREACHABLE(); + +#endif + + return false; + } + + using projection_matrix_type = float[4][4]; + + [[nodiscard]] auto id_to_gpu_handle(const texture_id_type id) noexcept -> ID3D11ShaderResourceView* + { + return reinterpret_cast(id); // NOLINT(performance-no-int-to-ptr) + } + + [[nodiscard]] auto gpu_handle_to_id(const ID3D11ShaderResourceView* srv) noexcept -> texture_id_type + { + return reinterpret_cast(srv); + } +} + +namespace gal::prometheus::gfx +{ + auto Dx11Renderer::create_blend_state() noexcept -> bool + { + constexpr D3D11_RENDER_TARGET_BLEND_DESC render_target + { + .BlendEnable = TRUE, + .SrcBlend = D3D11_BLEND_SRC_ALPHA, + .DestBlend = D3D11_BLEND_INV_SRC_ALPHA, + .BlendOp = D3D11_BLEND_OP_ADD, + .SrcBlendAlpha = D3D11_BLEND_ONE, + .DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA, + .BlendOpAlpha = D3D11_BLEND_OP_ADD, + .RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL + }; + constexpr D3D11_BLEND_DESC blend_desc + { + .AlphaToCoverageEnable = FALSE, + .IndependentBlendEnable = FALSE, + .RenderTarget = + { + render_target, + } + }; + + return check_hr_error(device_->CreateBlendState(&blend_desc, blend_state_.ReleaseAndGetAddressOf())); + } + + auto Dx11Renderer::create_rasterizer_state() noexcept -> bool + { + constexpr D3D11_RASTERIZER_DESC rasterizer_desc + { + .FillMode = D3D11_FILL_SOLID, + .CullMode = D3D11_CULL_NONE, + .FrontCounterClockwise = FALSE, + .DepthBias = 0, + .DepthBiasClamp = 0, + .SlopeScaledDepthBias = 0, + .DepthClipEnable = TRUE, + .ScissorEnable = TRUE, + .MultisampleEnable = TRUE, + .AntialiasedLineEnable = TRUE + }; + + return check_hr_error(device_->CreateRasterizerState(&rasterizer_desc, rasterizer_state_.ReleaseAndGetAddressOf())); + } + + auto Dx11Renderer::create_depth_stencil_state() noexcept -> bool + { + constexpr D3D11_DEPTH_STENCIL_DESC depth_stencil_desc + { + .DepthEnable = FALSE, + .DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL, + .DepthFunc = D3D11_COMPARISON_ALWAYS, + .StencilEnable = FALSE, + .StencilReadMask = 0, + .StencilWriteMask = 0, + .FrontFace = {.StencilFailOp = D3D11_STENCIL_OP_KEEP, .StencilDepthFailOp = D3D11_STENCIL_OP_KEEP, .StencilPassOp = D3D11_STENCIL_OP_KEEP, .StencilFunc = D3D11_COMPARISON_ALWAYS}, + .BackFace = {.StencilFailOp = D3D11_STENCIL_OP_KEEP, .StencilDepthFailOp = D3D11_STENCIL_OP_KEEP, .StencilPassOp = D3D11_STENCIL_OP_KEEP, .StencilFunc = D3D11_COMPARISON_ALWAYS} + }; + + return check_hr_error(device_->CreateDepthStencilState(&depth_stencil_desc, depth_stencil_state_.ReleaseAndGetAddressOf())); + } + + auto Dx11Renderer::create_vertex_shader() noexcept -> bool + { + constexpr char shader_code[] + { + "cbuffer vertexBuffer : register(b0)" + "{" + " float4x4 ProjectionMatrix;" + "};" + "struct VS_INPUT" + "{" + " float2 pos : POSITION;" + " float4 col : COLOR0;" + " float2 uv : TEXCOORD0;" + "};" + "struct PS_INPUT" + "{" + " float4 pos : SV_POSITION;" + " float4 col : COLOR0;" + " float2 uv : TEXCOORD0;" + "};" + "PS_INPUT main(VS_INPUT input)" + "{" + " PS_INPUT output;" + " output.pos = mul(ProjectionMatrix, float4(input.pos.xy, 0.f, 1.f));" + " output.col = input.col;" + " output.uv = input.uv;" + " return output;" + "}" + }; + + ComPtr shader_blob; + ComPtr error_message; + + if (const auto result = D3DCompile( + shader_code, + sizeof(shader_code), + nullptr, + nullptr, + nullptr, + "main", + "vs_5_0", + 0, + 0, + shader_blob.GetAddressOf(), + error_message.GetAddressOf() + ); + FAILED(result)) + { + std::println( + stderr, + "D3DCompile failed: {} -- at {}:{}", + static_cast(error_message->GetBufferPointer()), + std::source_location::current().file_name(), + std::source_location::current().line() + ); + + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + return false; + } + + if (not check_hr_error( + device_->CreateVertexShader( + shader_blob->GetBufferPointer(), + shader_blob->GetBufferSize(), + nullptr, + vertex_shader_.ReleaseAndGetAddressOf() + ) + )) + { + return false; + } + + // vertex input layout + constexpr D3D11_INPUT_ELEMENT_DESC input_element_desc[]{ + { + .SemanticName = "POSITION", + .SemanticIndex = 0, + .Format = DXGI_FORMAT_R32G32_FLOAT, + .InputSlot = 0, + .AlignedByteOffset = static_cast(offsetof(vertex_type, position)), + .InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA, + .InstanceDataStepRate = 0 + }, + { + .SemanticName = "COLOR", + .SemanticIndex = 0, + .Format = DXGI_FORMAT_R8G8B8A8_UNORM, + .InputSlot = 0, + .AlignedByteOffset = static_cast(offsetof(vertex_type, color)), + .InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA, + .InstanceDataStepRate = 0 + }, + { + .SemanticName = "TEXCOORD", + .SemanticIndex = 0, + .Format = DXGI_FORMAT_R32G32_FLOAT, + .InputSlot = 0, + .AlignedByteOffset = static_cast(offsetof(vertex_type, uv)), + .InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA, + .InstanceDataStepRate = 0 + }, + }; + if (not check_hr_error( + device_->CreateInputLayout( + input_element_desc, + static_cast(std::ranges::size(input_element_desc)), + shader_blob->GetBufferPointer(), + shader_blob->GetBufferSize(), + vertex_input_layout_.ReleaseAndGetAddressOf() + ) + )) + { + return false; + } + + // constant buffer + constexpr D3D11_BUFFER_DESC constant_buffer_desc{ + .ByteWidth = sizeof(projection_matrix_type), + .Usage = D3D11_USAGE_DYNAMIC, + .BindFlags = D3D11_BIND_CONSTANT_BUFFER, + .CPUAccessFlags = D3D11_CPU_ACCESS_WRITE, + .MiscFlags = 0, + .StructureByteStride = 0 + }; + if (not check_hr_error( + device_->CreateBuffer( + &constant_buffer_desc, + nullptr, + vertex_projection_matrix_.ReleaseAndGetAddressOf() + ) + )) + { + return false; + } + + return true; + } + + auto Dx11Renderer::create_pixel_shader() noexcept -> bool + { + constexpr char shader_code[] + { + "struct PS_INPUT" + "{" + " float4 pos : SV_POSITION;" + " float4 col : COLOR0;" + " float2 uv : TEXCOORD0;" + "};" + "sampler sampler0;" + "Texture2D texture0;" + "float4 main(PS_INPUT input) : SV_Target" + "{" + " float4 out_col = texture0.Sample(sampler0, input.uv);" + " return input.col * out_col;" + "}" + }; + + ComPtr shader_blob; + ComPtr error_message; + + if (const auto result = D3DCompile( + shader_code, + sizeof(shader_code), + nullptr, + nullptr, + nullptr, + "main", + "ps_5_0", + 0, + 0, + shader_blob.GetAddressOf(), + error_message.GetAddressOf() + ); + FAILED(result)) + { + std::println( + stderr, + "D3DCompile failed: {} -- at {}:{}", + static_cast(error_message->GetBufferPointer()), + std::source_location::current().file_name(), + std::source_location::current().line() + ); + + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + return false; + } + + if (not check_hr_error( + device_->CreatePixelShader( + shader_blob->GetBufferPointer(), + shader_blob->GetBufferSize(), + nullptr, + pixel_shader_.ReleaseAndGetAddressOf() + ) + )) + { + return false; + } + + // Create pixel shader texture sampler + constexpr D3D11_SAMPLER_DESC sampler_desc + { + .Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR, + .AddressU = D3D11_TEXTURE_ADDRESS_WRAP, + .AddressV = D3D11_TEXTURE_ADDRESS_WRAP, + .AddressW = D3D11_TEXTURE_ADDRESS_WRAP, + .MipLODBias = 0, + .MaxAnisotropy = 0, + .ComparisonFunc = D3D11_COMPARISON_ALWAYS, + .BorderColor = {0, 0, 0, 0}, + .MinLOD = 0, + .MaxLOD = 0 + }; + if (not check_hr_error( + device_->CreateSamplerState( + &sampler_desc, + pixel_font_sampler_.ReleaseAndGetAddressOf() + ) + )) + { + return false; + } + + return false; + } + + auto Dx11Renderer::upload_texture( + const Texture::data_view_type data, + const Texture::size_type size, + const D3D11_USAGE usage, + const std::uint32_t bind_flags, + const std::uint32_t cpu_access_flags, + const std::uint32_t misc_flags, + const bool record_resource + ) noexcept -> texture_id_type + { + const D3D11_TEXTURE2D_DESC texture_2d_desc + { + .Width = static_cast(size.width), + .Height = static_cast(size.height), + .MipLevels = 1, + .ArraySize = 1, + .Format = DXGI_FORMAT_R8G8B8A8_UNORM, + .SampleDesc = {.Count = 1, .Quality = 0}, + .Usage = usage, + .BindFlags = bind_flags, + .CPUAccessFlags = cpu_access_flags, + .MiscFlags = misc_flags + }; + + const D3D11_SUBRESOURCE_DATA subresource_data + { + .pSysMem = data.data(), + .SysMemPitch = static_cast(size.width * 4), + .SysMemSlicePitch = 0 + }; + + ID3D11Texture2D* texture_2d; + if (not check_hr_error( + device_->CreateTexture2D( + &texture_2d_desc, + &subresource_data, + &texture_2d + ) + )) + { + return invalid_texture_id; + } + + const D3D11_SHADER_RESOURCE_VIEW_DESC shader_resource_view_desc + { + .Format = texture_2d_desc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = + { + .MostDetailedMip = 0, + .MipLevels = texture_2d_desc.MipLevels + } + }; + + ID3D11ShaderResourceView* srv = nullptr; + if (not check_hr_error( + device_->CreateShaderResourceView( + texture_2d, + &shader_resource_view_desc, + &srv + ) + )) + { + return false; + } + + if (record_resource) + { + textures_.insert_or_assign(srv, texture_2d); + } + else + { + texture_2d->Release(); + } + + return gpu_handle_to_id(srv); + } + + Dx11Renderer::Dx11Renderer() noexcept + : device_{nullptr}, + device_immediate_context_{nullptr}, + blend_state_{nullptr}, + rasterizer_state_{nullptr}, + depth_stencil_state_{nullptr}, + vertex_shader_{nullptr}, + vertex_input_layout_{nullptr}, + vertex_projection_matrix_{nullptr}, + pixel_shader_{nullptr}, + pixel_font_sampler_{nullptr}, + render_buffer_{} {} + + Dx11Renderer::Dx11Renderer(ID3D11Device* device, ID3D11DeviceContext* device_immediate_context) noexcept + : Dx11Renderer{} + { + bind_device(device); + bind_device_context(device_immediate_context); + } + + Dx11Renderer::Dx11Renderer(ComPtr device, ComPtr device_immediate_context) noexcept + : Dx11Renderer{} + { + bind_device(std::move(device)); + bind_device_context(std::move(device_immediate_context)); + } + + auto Dx11Renderer::bind_device(ID3D11Device* device) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(device != nullptr); + device_ = device; + } + + auto Dx11Renderer::bind_device(ComPtr device) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(device != nullptr); + device_ = std::move(device); + } + + auto Dx11Renderer::bind_device_context(ID3D11DeviceContext* device_immediate_context) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(device_immediate_context != nullptr); + device_immediate_context_ = device_immediate_context; + } + + auto Dx11Renderer::bind_device_context(ComPtr device_immediate_context) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(device_immediate_context != nullptr); + device_immediate_context_ = std::move(device_immediate_context); + } + + auto Dx11Renderer::do_create() noexcept -> bool + { + if (not create_blend_state()) + { + return false; + } + if (not create_rasterizer_state()) + { + return false; + } + if (not create_depth_stencil_state()) + { + return false; + } + if (not create_vertex_shader()) + { + return false; + } + if (not create_pixel_shader()) + { + return false; + } + + return true; + } + + auto Dx11Renderer::do_destroy() noexcept -> void + { + // ComPtr + blend_state_ = nullptr; + rasterizer_state_ = nullptr; + depth_stencil_state_ = nullptr; + vertex_shader_ = nullptr; + vertex_input_layout_ = nullptr; + vertex_projection_matrix_ = nullptr; + pixel_shader_ = nullptr; + pixel_font_sampler_ = nullptr; + render_buffer_.index = nullptr; + render_buffer_.index_count = 0; + render_buffer_.vertex = nullptr; + render_buffer_.vertex_count = 0; + + // RAW + std::ranges::for_each( + textures_, + [](auto& kv) noexcept -> void + { + kv.first->Release(); + kv.second->Release(); + } + ); + textures_.clear(); + + // ComPtr + // device_immediate_context_->ClearState(); + // device_immediate_context_->Flush(); + device_immediate_context_ = nullptr; + device_ = nullptr; + } + + auto Dx11Renderer::do_ready() const noexcept -> bool + { + if (device_ == nullptr or device_immediate_context_ == nullptr) + { + return false; + } + + if (blend_state_ == nullptr or rasterizer_state_ == nullptr or depth_stencil_state_ == nullptr) + { + return false; + } + + if (vertex_shader_ == nullptr or vertex_input_layout_ == nullptr or vertex_projection_matrix_ == nullptr) + { + return false; + } + + if (pixel_shader_ == nullptr or pixel_font_sampler_ == nullptr) + { + return false; + } + + return true; + } + + auto Dx11Renderer::do_create_texture(const Texture::data_view_type data, const Texture::size_type size) noexcept -> texture_id_type + { + return upload_texture(data, size, D3D11_USAGE_DYNAMIC, D3D11_BIND_SHADER_RESOURCE, D3D11_CPU_ACCESS_WRITE, 0); + } + + auto Dx11Renderer::do_update_texture(const Texture& texture) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(texture.uploaded(), "Create texture first!"); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(texture.dirty(), "No need to update texture!"); + + auto* srv = id_to_gpu_handle(texture.id()); + const auto it = textures_.find(srv); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(it != textures_.end(), "Invalid texture id"); + + auto* texture_2d = it->second; + D3D11_MAPPED_SUBRESOURCE mapped_resource{}; + if (const auto result = device_immediate_context_->Map( + texture_2d, + 0, + D3D11_MAP_WRITE_DISCARD, + 0, + &mapped_resource + ); result != S_OK) + { + // todo: error handling + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + return; + } + + const auto* source = texture.data().data(); + const auto source_length = texture.area_size(); + std::ranges::copy(source, source + source_length, static_cast(mapped_resource.pData)); + + device_immediate_context_->Unmap(texture_2d, 0); + } + + auto Dx11Renderer::do_destroy_texture(const texture_id_type texture_id) noexcept -> void + { + auto* srv = id_to_gpu_handle(texture_id); + + if (const auto it = textures_.find(srv); it != textures_.end()) + { + srv->Release(); + it->first->Release(); + it->second->Release(); + + textures_.erase(it); + } + } + + auto Dx11Renderer::do_present(const RenderContext& renderer_context, const rect_type& display_area) noexcept -> void + { + const auto all_render_data = renderer_context.render_data(); + // const auto [display_x, display_y] = display_area.point; + const auto [display_width, display_height] = display_area.extent; + + auto& [this_frame_index_buffer, this_frame_index_count, this_frame_vertex_buffer, this_frame_vertex_count] = render_buffer_; + + const auto [total_vertex_count, total_index_count] = [&]() noexcept + { + struct sum + { + UINT vertex; + UINT index; + }; + + return std::ranges::fold_left( + all_render_data, + sum{.vertex = 0, .index = 0}, + [](const sum s, const RenderData& render_data) noexcept -> sum + { + const auto vertex_list = render_data.vertex_list.get(); + const auto index_list = render_data.index_list.get(); + + return {.vertex = s.vertex + static_cast(vertex_list.size()), .index = s.index + static_cast(index_list.size())}; + } + ); + }(); + + if (not this_frame_vertex_buffer or total_vertex_count > this_frame_vertex_count) + { + // todo: grow factor + this_frame_vertex_count = total_vertex_count + 5000; + + const D3D11_BUFFER_DESC buffer_desc{ + .ByteWidth = static_cast(this_frame_vertex_count * sizeof(vertex_type)), + .Usage = D3D11_USAGE_DYNAMIC, + .BindFlags = D3D11_BIND_VERTEX_BUFFER, + .CPUAccessFlags = D3D11_CPU_ACCESS_WRITE, + .MiscFlags = 0, + .StructureByteStride = 0 + }; + check_hr_error(device_->CreateBuffer(&buffer_desc, nullptr, this_frame_vertex_buffer.ReleaseAndGetAddressOf())); + } + if (not this_frame_index_buffer or total_index_count > this_frame_index_count) + { + // todo: grow factor + this_frame_index_count = total_index_count + 10000; + + const D3D11_BUFFER_DESC buffer_desc{ + .ByteWidth = static_cast(this_frame_index_count * sizeof(index_type)), + .Usage = D3D11_USAGE_DYNAMIC, + .BindFlags = D3D11_BIND_INDEX_BUFFER, + .CPUAccessFlags = D3D11_CPU_ACCESS_WRITE, + .MiscFlags = 0, + .StructureByteStride = 0 + }; + check_hr_error(device_->CreateBuffer(&buffer_desc, nullptr, this_frame_index_buffer.ReleaseAndGetAddressOf())); + } + + // Upload vertex/index data into a single contiguous GPU buffer + { + D3D11_MAPPED_SUBRESOURCE mapped_vertex_resource; + D3D11_MAPPED_SUBRESOURCE mapped_index_resource; + check_hr_error(device_immediate_context_->Map(this_frame_vertex_buffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped_vertex_resource)); + check_hr_error(device_immediate_context_->Map(this_frame_index_buffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped_index_resource)); + + auto* mapped_vertex = static_cast(mapped_vertex_resource.pData); + auto* mapped_index = static_cast(mapped_index_resource.pData); + + UINT vertex_offset = 0; + UINT index_offset = 0; + + std::ranges::for_each( + all_render_data, + [&](const RenderData& render_data) noexcept -> void + { + const auto vertex_list = render_data.vertex_list.get(); + const auto index_list = render_data.index_list.get(); + + // std::ranges::transform( + // vertex_list, + // mapped_vertex + vertex_offset, + // [](const vertex_type& vertex) noexcept -> vertex_type + // { + // // return { + // // .position = {vertex.position.x, vertex.position.y}, + // // .uv = {vertex.uv.x, vertex.uv.y}, + // // .color = vertex.color.to(primitive::color_format) + // // }; + // return std::bit_cast(vertex); + // } + // ); + std::ranges::copy(vertex_list, mapped_vertex + vertex_offset); + // std::ranges::transform( + // index_list, + // mapped_index + index_offset, + // [vertex_offset](const index_type index) noexcept -> index_type + // { + // return static_cast(index + vertex_offset); + // } + // ); + std::ranges::copy(index_list, mapped_index + index_offset); + + vertex_offset += static_cast(vertex_list.size()); + index_offset += static_cast(index_list.size()); + } + ); + + device_immediate_context_->Unmap(this_frame_vertex_buffer.Get(), 0); + device_immediate_context_->Unmap(this_frame_index_buffer.Get(), 0); + } + + // Setup orthographic projection matrix into our constant buffer + { + D3D11_MAPPED_SUBRESOURCE mapped_resource; + check_hr_error(device_immediate_context_->Map(vertex_projection_matrix_.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped_resource)); + + auto* mapped_projection_matrix = static_cast(mapped_resource.pData); + + constexpr auto left = 0.f; + const auto right = display_width; + constexpr auto top = 0.f; + const auto bottom = display_height; + + const projection_matrix_type mvp{ + {2.0f / (right - left), 0.0f, 0.0f, 0.0f}, + {0.0f, 2.0f / (top - bottom), 0.0f, 0.0f}, + {0.0f, 0.0f, 0.5f, 0.0f}, + {(right + left) / (left - right), (top + bottom) / (bottom - top), 0.5f, 1.0f}, + }; + std::memcpy(mapped_projection_matrix, &mvp, sizeof(projection_matrix_type)); + + device_immediate_context_->Unmap(vertex_projection_matrix_.Get(), 0); + } + + // Setup viewport + { + const D3D11_VIEWPORT viewport{ + .TopLeftX = .0f, + .TopLeftY = .0f, + .Width = display_width, + .Height = display_height, + .MinDepth = 0, + .MaxDepth = 1 + }; + device_immediate_context_->RSSetViewports(1, &viewport); + } + + // Bind shader and vertex buffers + constexpr UINT stride = sizeof(vertex_type); + constexpr UINT offset = 0; + device_immediate_context_->IASetInputLayout(vertex_input_layout_.Get()); + device_immediate_context_->IASetVertexBuffers(0, 1, this_frame_vertex_buffer.GetAddressOf(), &stride, &offset); + device_immediate_context_->IASetIndexBuffer( + this_frame_index_buffer.Get(), + // ReSharper disable once CppUnreachableCode + sizeof(index_type) == 2 ? DXGI_FORMAT_R16_UINT : DXGI_FORMAT_R32_UINT, + 0 + ); + device_immediate_context_->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + device_immediate_context_->VSSetShader(vertex_shader_.Get(), nullptr, 0); + device_immediate_context_->VSSetConstantBuffers(0, 1, vertex_projection_matrix_.GetAddressOf()); + device_immediate_context_->PSSetShader(pixel_shader_.Get(), nullptr, 0); + device_immediate_context_->PSSetSamplers(0, 1, pixel_font_sampler_.GetAddressOf()); + device_immediate_context_->DSSetShader(nullptr, nullptr, 0); + device_immediate_context_->HSSetShader(nullptr, nullptr, 0); + device_immediate_context_->GSSetShader(nullptr, nullptr, 0); + device_immediate_context_->CSSetShader(nullptr, nullptr, 0); + + // Setup render state + constexpr float blend_factor[]{0, 0, 0, 0}; + device_immediate_context_->OMSetBlendState(blend_state_.Get(), blend_factor, (std::numeric_limits::max)()); + device_immediate_context_->OMSetDepthStencilState(depth_stencil_state_.Get(), 0); + device_immediate_context_->RSSetState(rasterizer_state_.Get()); + + UINT total_index_offset = 0; + std::ranges::for_each( + all_render_data, + [this, &total_index_offset](const RenderData& render_data) noexcept -> void + { + const auto vertex_list = render_data.vertex_list.get(); + const auto index_list = render_data.index_list.get(); + + for (const auto& command_list = render_data.command_list.get(); + const auto& [scissor, texture, index_offset, element_count]: command_list) + { + const auto [point, extent] = scissor; + const D3D11_RECT rect + { + static_cast(point.x), + static_cast(point.y), + static_cast(point.x + extent.width), + static_cast(point.y + extent.height) + }; + device_immediate_context_->RSSetScissorRects(1, &rect); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(texture != invalid_texture_id); + auto* srv = id_to_gpu_handle(texture); + device_immediate_context_->PSSetShaderResources(0, 1, &srv); + + const auto this_index_offset = static_cast(total_index_offset + index_offset); + // device_immediate_context_->DrawIndexed(static_cast(element_count), this_index_offset, 0); + device_immediate_context_->DrawIndexedInstanced(static_cast(element_count), 1, this_index_offset, 0, 0); + } + + total_index_offset += static_cast(index_list.size()); + } + ); + } +} + +#endif diff --git a/src/gfx/old/renderer_dx11.hpp b/src/gfx/old/renderer_dx11.hpp new file mode 100644 index 00000000..37cf27b8 --- /dev/null +++ b/src/gfx/old/renderer_dx11.hpp @@ -0,0 +1,97 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +// todo +#define GAL_PROMETHEUS_GFX_RENDER_DX11 + +#if defined(GAL_PROMETHEUS_GFX_RENDER_DX11) + +#include + +#include + +#include +#include + +namespace gal::prometheus::gfx +{ + using Microsoft::WRL::ComPtr; + + class Dx11Renderer final : public Renderer + { + public: + using textures_type = std::unordered_map; + + private: + struct render_buffer_type + { + ComPtr index; + UINT index_count; + ComPtr vertex; + UINT vertex_count; + }; + + ComPtr device_; + ComPtr device_immediate_context_; + + ComPtr blend_state_; + ComPtr rasterizer_state_; + ComPtr depth_stencil_state_; + + ComPtr vertex_shader_; + ComPtr vertex_input_layout_; + ComPtr vertex_projection_matrix_; + + ComPtr pixel_shader_; + ComPtr pixel_font_sampler_; + + textures_type textures_; + + render_buffer_type render_buffer_; + + [[nodiscard]] auto create_blend_state() noexcept -> bool; + [[nodiscard]] auto create_rasterizer_state() noexcept -> bool; + [[nodiscard]] auto create_depth_stencil_state() noexcept -> bool; + + [[nodiscard]] auto create_vertex_shader() noexcept -> bool; + [[nodiscard]] auto create_pixel_shader() noexcept -> bool; + + [[nodiscard]] auto upload_texture( + Texture::data_view_type data, + Texture::size_type size, + D3D11_USAGE usage, + std::uint32_t bind_flags, + std::uint32_t cpu_access_flags, + std::uint32_t misc_flags, + bool record_resource = true + ) noexcept -> texture_id_type; + + public: + Dx11Renderer() noexcept; + Dx11Renderer(ID3D11Device* device, ID3D11DeviceContext* device_immediate_context) noexcept; + Dx11Renderer(ComPtr device, ComPtr device_immediate_context) noexcept; + + auto bind_device(ID3D11Device* device) noexcept -> void; + auto bind_device(ComPtr device) noexcept -> void; + auto bind_device_context(ID3D11DeviceContext* device_immediate_context) noexcept -> void; + auto bind_device_context(ComPtr device_immediate_context) noexcept -> void; + + private: + auto do_create() noexcept -> bool override; + auto do_destroy() noexcept -> void override; + + [[nodiscard]] auto do_ready() const noexcept -> bool override; + + auto do_create_texture(Texture::data_view_type data, Texture::size_type size) noexcept -> texture_id_type override; + auto do_update_texture(const Texture& texture) noexcept -> void override; + auto do_destroy_texture(texture_id_type texture_id) noexcept -> void override; + + auto do_present(const RenderContext& renderer_context, const rect_type& display_area) noexcept -> void override; + }; +} + +#endif diff --git a/src/gfx/old/renderer_dx12.cpp b/src/gfx/old/renderer_dx12.cpp new file mode 100644 index 00000000..649f1615 --- /dev/null +++ b/src/gfx/old/renderer_dx12.cpp @@ -0,0 +1,1026 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +#if defined(GAL_PROMETHEUS_GFX_RENDER_DX12) + +#include + +#if GAL_PROMETHEUS_COMPILER_DEBUG +#define GAL_PROMETHEUS_GFX_DEBUG +#include +#endif + +#include + +#include +#include +#include +#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE + +#include +#include + +namespace +{ + using namespace gal::prometheus; + using namespace gfx; + + auto check_hr_error( + const HRESULT result +#if defined(GAL_PROMETHEUS_GFX_DEBUG) + , + const std::source_location& location = std::source_location::current() +#endif + ) noexcept -> bool + { + if (SUCCEEDED(result)) + { + return true; + } + +#if defined(GAL_PROMETHEUS_GFX_DEBUG) + + const _com_error err{result}; + std::println(stderr, "Error: {} --- at {}:{}", err.ErrorMessage(), location.file_name(), location.line()); + + GAL_PROMETHEUS_COMPILER_DEBUG_TRAP(); + +#else + + GAL_PROMETHEUS_COMPILER_UNREACHABLE(); + +#endif + + return false; + } + + using projection_matrix_type = float[4][4]; + + constexpr std::size_t gpu_handle_to_id_offset = 0x1000'0000; + + [[nodiscard]] auto id_to_gpu_handle(const texture_id_type id) noexcept -> std::size_t + { + return id - gpu_handle_to_id_offset; + } + + [[nodiscard]] auto gpu_handle_to_id(const std::size_t index) noexcept -> texture_id_type + { + return index + gpu_handle_to_id_offset; + } +} + +namespace gal::prometheus::gfx +{ + auto Dx12Renderer::create_root_signature() noexcept -> bool + { + // [0] projection_matrix + constexpr D3D12_ROOT_PARAMETER param_0 + { + .ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS, + .Constants = {.ShaderRegister = 0, .RegisterSpace = 0, .Num32BitValues = sizeof(projection_matrix_type) / sizeof(float)}, + .ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX + }; + // [1] texture + constexpr D3D12_DESCRIPTOR_RANGE range + { + .RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV, + .NumDescriptors = 1, + .BaseShaderRegister = 0, + .RegisterSpace = 0, + .OffsetInDescriptorsFromTableStart = 0 + }; + const D3D12_ROOT_PARAMETER param_1 + { + .ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE, + .DescriptorTable = {.NumDescriptorRanges = 1, .pDescriptorRanges = &range}, + .ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL + }; + // @see Dx12Renderer::do_present -> `command_list_->SetGraphicsRootXxx` + const D3D12_ROOT_PARAMETER params[]{param_0, param_1}; + + // Bi-linear sampling is required by default + constexpr D3D12_STATIC_SAMPLER_DESC static_sampler_desc + { + .Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR, + .AddressU = D3D12_TEXTURE_ADDRESS_MODE_WRAP, + .AddressV = D3D12_TEXTURE_ADDRESS_MODE_WRAP, + .AddressW = D3D12_TEXTURE_ADDRESS_MODE_WRAP, + .MipLODBias = .0f, + .MaxAnisotropy = 0, + .ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS, + .BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK, + .MinLOD = .0f, + .MaxLOD = .0f, + .ShaderRegister = 0, + .RegisterSpace = 0, + .ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL + }; + + const D3D12_ROOT_SIGNATURE_DESC root_signature_desc + { + .NumParameters = static_cast(std::ranges::size(params)), + .pParameters = params, + .NumStaticSamplers = 1, + .pStaticSamplers = &static_sampler_desc, + .Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT | D3D12_ROOT_SIGNATURE_FLAG_DENY_HULL_SHADER_ROOT_ACCESS | + D3D12_ROOT_SIGNATURE_FLAG_DENY_DOMAIN_SHADER_ROOT_ACCESS | D3D12_ROOT_SIGNATURE_FLAG_DENY_GEOMETRY_SHADER_ROOT_ACCESS + }; + + auto d3d12_dll = GetModuleHandleW(L"d3d12.dll"); + if (d3d12_dll == nullptr) + { + d3d12_dll = LoadLibraryW(L"d3d12.dll"); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(d3d12_dll != nullptr); + } + + const auto serialize_root_signature_function = + reinterpret_cast(GetProcAddress(d3d12_dll, "D3D12SerializeRootSignature")); // NOLINT(clang-diagnostic-cast-function-type-strict) + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(serialize_root_signature_function != nullptr); + + ComPtr blob = nullptr; + if (not check_hr_error(serialize_root_signature_function(&root_signature_desc, D3D_ROOT_SIGNATURE_VERSION_1, blob.GetAddressOf(), nullptr))) + { + return false; + } + if (not check_hr_error(device_->CreateRootSignature(0, blob->GetBufferPointer(), blob->GetBufferSize(), IID_PPV_ARGS(root_signature_.GetAddressOf())))) + { + return false; + } + + return true; + } + + auto Dx12Renderer::create_pipeline_state() noexcept -> bool + { + // Create the vertex shader + auto vertex_shader_blob = []() -> ComPtr + { + constexpr static char shader[]{ + "cbuffer vertexBuffer : register(b0)" + "{" + " float4x4 ProjectionMatrix;" + "};" + "struct VS_INPUT" + "{" + " float2 pos : POSITION;" + " float4 col : COLOR0;" + " float2 uv : TEXCOORD0;" + "};" + "struct PS_INPUT" + "{" + " float4 pos : SV_POSITION;" + " float4 col : COLOR0;" + " float2 uv : TEXCOORD0;" + "};" + "PS_INPUT main(VS_INPUT input)" + "{" + " PS_INPUT output;" + " output.pos = mul(ProjectionMatrix, float4(input.pos.xy, 0.f, 1.f));" + " output.col = input.col;" + " output.uv = input.uv;" + " return output;" + "}" + }; + + ComPtr blob; + if (not check_hr_error(D3DCompile(shader, std::ranges::size(shader), nullptr, nullptr, nullptr, "main", "vs_5_0", 0, 0, blob.GetAddressOf(), nullptr))) + { + return nullptr; + } + + return blob; + }(); + + // Create the pixel shader + auto pixel_shader_blob = []() -> ComPtr + { + constexpr static char shader[]{ + "struct PS_INPUT" + "{" + " float4 pos : SV_POSITION;" + " float4 col : COLOR0;" + " float2 uv : TEXCOORD0;" + "};" + "sampler sampler0;" + "Texture2D texture0;" + "float4 main(PS_INPUT input) : SV_Target" + "{" + " float4 out_col = texture0.Sample(sampler0, input.uv);" + " return input.col * out_col;" + "}" + }; + + ComPtr blob; + if (not check_hr_error(D3DCompile(shader, std::ranges::size(shader), nullptr, nullptr, nullptr, "main", "ps_5_0", 0, 0, blob.GetAddressOf(), nullptr))) + { + return nullptr; + } + + return blob; + }(); + + if (vertex_shader_blob == nullptr or pixel_shader_blob == nullptr) + { + return false; + } + + // Create the blending setup + constexpr D3D12_RENDER_TARGET_BLEND_DESC render_target_blend_desc + { + .BlendEnable = true, + .LogicOpEnable = false, + .SrcBlend = D3D12_BLEND_SRC_ALPHA, + .DestBlend = D3D12_BLEND_INV_SRC_ALPHA, + .BlendOp = D3D12_BLEND_OP_ADD, + .SrcBlendAlpha = D3D12_BLEND_ONE, + .DestBlendAlpha = D3D12_BLEND_INV_SRC_ALPHA, + .BlendOpAlpha = D3D12_BLEND_OP_ADD, + .LogicOp = D3D12_LOGIC_OP_CLEAR, + .RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL + }; + constexpr D3D12_BLEND_DESC blend_desc + { + .AlphaToCoverageEnable = FALSE, + .IndependentBlendEnable = FALSE, + .RenderTarget = {render_target_blend_desc} + }; + + // Create the rasterizer state + constexpr D3D12_RASTERIZER_DESC rasterizer_desc + { + .FillMode = D3D12_FILL_MODE_SOLID, + .CullMode = D3D12_CULL_MODE_NONE, + .FrontCounterClockwise = FALSE, + .DepthBias = D3D12_DEFAULT_DEPTH_BIAS, + .DepthBiasClamp = D3D12_DEFAULT_DEPTH_BIAS_CLAMP, + .SlopeScaledDepthBias = D3D12_DEFAULT_SLOPE_SCALED_DEPTH_BIAS, + .DepthClipEnable = TRUE, + .MultisampleEnable = FALSE, + .AntialiasedLineEnable = FALSE, + .ForcedSampleCount = 0, + .ConservativeRaster = D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF + }; + + // Create depth-stencil State + constexpr D3D12_DEPTH_STENCIL_DESC depth_stencil_desc + { + .DepthEnable = FALSE, + .DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL, + .DepthFunc = D3D12_COMPARISON_FUNC_ALWAYS, + .StencilEnable = FALSE, + .StencilReadMask = 0, + .StencilWriteMask = 0, + .FrontFace = + { + .StencilFailOp = D3D12_STENCIL_OP_KEEP, + .StencilDepthFailOp = D3D12_STENCIL_OP_KEEP, + .StencilPassOp = D3D12_STENCIL_OP_KEEP, + .StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS + }, + .BackFace = + { + .StencilFailOp = D3D12_STENCIL_OP_KEEP, + .StencilDepthFailOp = D3D12_STENCIL_OP_KEEP, + .StencilPassOp = D3D12_STENCIL_OP_KEEP, + .StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS + } + }; + + // Create the input layout + constexpr D3D12_INPUT_ELEMENT_DESC input_element_desc[] + { + { + .SemanticName = "POSITION", + .SemanticIndex = 0, + .Format = DXGI_FORMAT_R32G32_FLOAT, + .InputSlot = 0, + .AlignedByteOffset = static_cast(offsetof(vertex_type, position)), + .InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, + .InstanceDataStepRate = 0 + }, + { + .SemanticName = "TEXCOORD", + .SemanticIndex = 0, + .Format = DXGI_FORMAT_R32G32_FLOAT, + .InputSlot = 0, + .AlignedByteOffset = static_cast(offsetof(vertex_type, uv)), + .InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, + .InstanceDataStepRate = 0 + }, + { + .SemanticName = "COLOR", + .SemanticIndex = 0, + .Format = DXGI_FORMAT_R8G8B8A8_UNORM, + .InputSlot = 0, + .AlignedByteOffset = static_cast(offsetof(vertex_type, color)), + .InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, + .InstanceDataStepRate = 0 + }, + }; + + D3D12_GRAPHICS_PIPELINE_STATE_DESC pipeline_state_desc + { + .pRootSignature = root_signature_.Get(), + .VS = {.pShaderBytecode = vertex_shader_blob->GetBufferPointer(), .BytecodeLength = vertex_shader_blob->GetBufferSize()}, + .PS = {.pShaderBytecode = pixel_shader_blob->GetBufferPointer(), .BytecodeLength = pixel_shader_blob->GetBufferSize()}, + .DS = {.pShaderBytecode = nullptr, .BytecodeLength = 0}, + .HS = {.pShaderBytecode = nullptr, .BytecodeLength = 0}, + .GS = {.pShaderBytecode = nullptr, .BytecodeLength = 0}, + .StreamOutput = {.pSODeclaration = nullptr, .NumEntries = 0, .pBufferStrides = nullptr, .NumStrides = 0, .RasterizedStream = 0}, + .BlendState = blend_desc, + .SampleMask = UINT_MAX, + .RasterizerState = rasterizer_desc, + .DepthStencilState = depth_stencil_desc, + .InputLayout = {.pInputElementDescs = input_element_desc, .NumElements = static_cast(std::ranges::size(input_element_desc))}, + .IBStripCutValue = D3D12_INDEX_BUFFER_STRIP_CUT_VALUE_DISABLED, + .PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE, + .NumRenderTargets = 1, + .RTVFormats = {DXGI_FORMAT_R8G8B8A8_UNORM}, + .DSVFormat = {}, + .SampleDesc = {.Count = 1, .Quality = 0}, + .NodeMask = 1, + .CachedPSO = {.pCachedBlob = nullptr, .CachedBlobSizeInBytes = 0}, + .Flags = D3D12_PIPELINE_STATE_FLAG_NONE + }; + + return check_hr_error(device_->CreateGraphicsPipelineState(&pipeline_state_desc, IID_PPV_ARGS(pipeline_state_.GetAddressOf()))); + } + + auto Dx12Renderer::create_srv_descriptor_heap(const UINT num) noexcept -> bool + { + const D3D12_DESCRIPTOR_HEAP_DESC desc{ + .Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, + .NumDescriptors = num, + .Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE, + .NodeMask = 0 + }; + return check_hr_error(device_->CreateDescriptorHeap(&desc, IID_PPV_ARGS(srv_descriptor_heap_.ReleaseAndGetAddressOf()))); + } + + auto Dx12Renderer::upload_texture( + const std::size_t index, + const Texture::data_view_type data, + const Texture::size_type size, + const bool record_resource + ) noexcept -> texture_id_type + { + constexpr D3D12_HEAP_PROPERTIES heap_properties{ + .Type = D3D12_HEAP_TYPE_DEFAULT, + .CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN, + .MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN, + .CreationNodeMask = 0, + .VisibleNodeMask = 0 + }; + + const D3D12_RESOURCE_DESC resource_desc{ + .Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D, + .Alignment = 0, + .Width = static_cast(size.width), + .Height = static_cast(size.height), + .DepthOrArraySize = 1, + .MipLevels = 1, + .Format = DXGI_FORMAT_R8G8B8A8_UNORM, + .SampleDesc = {.Count = 1, .Quality = 0}, + .Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN, + .Flags = D3D12_RESOURCE_FLAG_NONE + }; + + ComPtr texture_2d; + if (not check_hr_error( + device_->CreateCommittedResource( + &heap_properties, + D3D12_HEAP_FLAG_NONE, + &resource_desc, + D3D12_RESOURCE_STATE_COPY_DEST, + nullptr, + IID_PPV_ARGS(texture_2d.GetAddressOf()) + ) + )) + { + return invalid_texture_id; + } + + const auto upload_pitch = (static_cast(size.width * 4) + D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1u) & ~(D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1u); + const auto upload_size = static_cast(size.height) * upload_pitch; + + constexpr D3D12_HEAP_PROPERTIES upload_heap_properties{ + .Type = D3D12_HEAP_TYPE_UPLOAD, + .CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN, + .MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN, + .CreationNodeMask = 0, + .VisibleNodeMask = 0 + }; + + const D3D12_RESOURCE_DESC upload_resource_desc{ + .Dimension = D3D12_RESOURCE_DIMENSION_BUFFER, + .Alignment = 0, + .Width = static_cast(upload_size), + .Height = 1, + .DepthOrArraySize = 1, + .MipLevels = 1, + .Format = DXGI_FORMAT_UNKNOWN, + .SampleDesc = {.Count = 1, .Quality = 0}, + .Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR, + .Flags = D3D12_RESOURCE_FLAG_NONE + }; + + ComPtr upload_buffer; + check_hr_error( + device_->CreateCommittedResource( + &upload_heap_properties, + D3D12_HEAP_FLAG_NONE, + &upload_resource_desc, + D3D12_RESOURCE_STATE_GENERIC_READ, + nullptr, + IID_PPV_ARGS(upload_buffer.GetAddressOf()) + ) + ); + + void* mapped_data = nullptr; + const D3D12_RANGE range{.Begin = 0, .End = upload_size}; + if (not check_hr_error(upload_buffer->Map(0, &range, &mapped_data))) + { + return invalid_texture_id; + } + for (UINT i = 0; i < static_cast(size.height); ++i) + { + auto* dest = static_cast(mapped_data) + static_cast(upload_pitch * i); + auto* source = reinterpret_cast(data.data()) + static_cast(size.width * i * 4); + const auto length = size.width * 4; + std::memcpy(dest, source, length); + } + upload_buffer->Unmap(0, &range); + + const D3D12_TEXTURE_COPY_LOCATION source_location{ + .pResource = upload_buffer.Get(), + .Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT, + .PlacedFootprint = + { + .Offset = 0, + .Footprint = + { + .Format = DXGI_FORMAT_R8G8B8A8_UNORM, + .Width = static_cast(size.width), + .Height = static_cast(size.height), + .Depth = 1, + .RowPitch = upload_pitch + } + } + }; + + const D3D12_TEXTURE_COPY_LOCATION dest_location{ + .pResource = texture_2d.Get(), + .Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX, + .SubresourceIndex = 0 + }; + + const D3D12_RESOURCE_BARRIER barrier{ + .Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, + .Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE, + .Transition = + { + .pResource = texture_2d.Get(), + .Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, + .StateBefore = D3D12_RESOURCE_STATE_COPY_DEST, + .StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE + } + }; + + ComPtr command_allocator; + if (not check_hr_error( + device_->CreateCommandAllocator( + D3D12_COMMAND_LIST_TYPE_DIRECT, + IID_PPV_ARGS(command_allocator.GetAddressOf()) + ) + )) + { + return invalid_texture_id; + } + + ComPtr command_list; + if (not check_hr_error( + device_->CreateCommandList( + 0, + D3D12_COMMAND_LIST_TYPE_DIRECT, + command_allocator.Get(), + nullptr, + IID_PPV_ARGS(command_list.GetAddressOf()) + ) + )) + { + return invalid_texture_id; + } + + command_list->CopyTextureRegion(&dest_location, 0, 0, 0, &source_location, nullptr); + command_list->ResourceBarrier(1, &barrier); + if (not check_hr_error(command_list->Close())) + { + return invalid_texture_id; + } + + constexpr D3D12_COMMAND_QUEUE_DESC command_queue_desc{ + .Type = D3D12_COMMAND_LIST_TYPE_DIRECT, + .Priority = 0, + .Flags = D3D12_COMMAND_QUEUE_FLAG_NONE, + .NodeMask = 1 + }; + + ComPtr command_queue; + if (not check_hr_error( + device_->CreateCommandQueue( + &command_queue_desc, + IID_PPV_ARGS(command_queue.GetAddressOf()) + ) + )) + { + return invalid_texture_id; + } + + ID3D12CommandList* command_lists[]{command_list.Get()}; + command_queue->ExecuteCommandLists(1, command_lists); + + ComPtr fence; + if (not check_hr_error( + device_->CreateFence( + 0, + D3D12_FENCE_FLAG_NONE, + IID_PPV_ARGS(fence.GetAddressOf()) + ) + )) + { + return invalid_texture_id; + } + + constexpr UINT64 fence_value = 1; + if (not check_hr_error(command_queue->Signal(fence.Get(), fence_value))) + { + return invalid_texture_id; + } + if (fence->GetCompletedValue() < fence_value) + { + HANDLE event = CreateEvent(nullptr, FALSE, FALSE, nullptr); + if (not check_hr_error(fence->SetEventOnCompletion(fence_value, event))) + { + return invalid_texture_id; + } + WaitForSingleObject(event, INFINITE); + CloseHandle(event); + } + + // Create texture view + const D3D12_SHADER_RESOURCE_VIEW_DESC resource_view_desc{ + .Format = DXGI_FORMAT_R8G8B8A8_UNORM, + .ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D, + .Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING, + .Texture2D = {.MostDetailedMip = 0, .MipLevels = resource_desc.MipLevels, .PlaneSlice = 0, .ResourceMinLODClamp = .0f} + }; + + const auto increment_size = device_->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); + + auto cpu_handle = srv_descriptor_heap_->GetCPUDescriptorHandleForHeapStart(); + auto gpu_handle = srv_descriptor_heap_->GetGPUDescriptorHandleForHeapStart(); + + cpu_handle.ptr += index * increment_size; + gpu_handle.ptr += index * increment_size; + + device_->CreateShaderResourceView(texture_2d.Get(), &resource_view_desc, cpu_handle); + + if (record_resource) + { + textures_[index] = {.resource = texture_2d, .cpu = cpu_handle, .gpu = gpu_handle}; + } + else + { + // ComPtr + texture_2d = nullptr; + } + + return gpu_handle_to_id(index); + } + + Dx12Renderer::Dx12Renderer() noexcept + : device_{nullptr}, + command_list_{nullptr}, + root_signature_{nullptr}, + pipeline_state_{nullptr}, + srv_descriptor_heap_{nullptr}, + srv_max_size_{8}, + render_buffer_{} {} + + Dx12Renderer::Dx12Renderer(ID3D12Device* device, ID3D12GraphicsCommandList* command_list) noexcept + { + bind_device(device); + bind_command_list(command_list); + } + + Dx12Renderer::Dx12Renderer(ComPtr device, ComPtr command_list) noexcept + { + bind_device(std::move(device)); + bind_command_list(std::move(command_list)); + } + + auto Dx12Renderer::bind_device(ID3D12Device* device) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(device != nullptr); + device_ = device; + } + + auto Dx12Renderer::bind_device(ComPtr device) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(device != nullptr); + device_ = std::move(device); + } + + auto Dx12Renderer::bind_command_list(ID3D12GraphicsCommandList* command_list) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(command_list != nullptr); + command_list_ = command_list; + } + + auto Dx12Renderer::bind_command_list(ComPtr command_list) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(command_list != nullptr); + command_list_ = std::move(command_list); + } + + auto Dx12Renderer::do_create() noexcept -> bool + { + if (not create_root_signature()) + { + return false; + } + if (not create_pipeline_state()) + { + return false; + } + if (not create_srv_descriptor_heap(srv_max_size_)) + { + return false; + } + textures_.resize(srv_max_size_); + + return true; + } + + auto Dx12Renderer::do_destroy() noexcept -> void + { + // ComPtr + textures_.clear(); + + render_buffer_.vertex = nullptr; + render_buffer_.index = nullptr; + + // command_list_->ClearState(pipeline_state_.Get()); + // std::ignore = command_list_->Close(); + + srv_descriptor_heap_ = nullptr; + pipeline_state_ = nullptr; + root_signature_ = nullptr; + + command_list_ = nullptr; + device_ = nullptr; + } + + auto Dx12Renderer::do_ready() const noexcept -> bool + { + if (device_ == nullptr or command_list_ == nullptr) + { + return false; + } + + if (root_signature_ == nullptr or pipeline_state_ == nullptr or srv_descriptor_heap_ == nullptr) + { + return false; + } + + return true; + } + + auto Dx12Renderer::do_create_texture(const Texture::data_view_type data, const Texture::size_type size) noexcept -> texture_id_type + { + auto it = std::ranges::find(textures_, nullptr, &texture_type::resource); + if (it == textures_.end()) + { + // full, expand heap + const auto old_size = srv_max_size_; + const auto new_size = static_cast(static_cast(srv_max_size_) * 1.5f); + + srv_max_size_ = new_size; + textures_.resize(srv_max_size_); + it = textures_.begin() + old_size; + + const auto old_heap = srv_descriptor_heap_; + + const auto create_srv_descriptor_heap_result = create_srv_descriptor_heap(srv_max_size_); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(create_srv_descriptor_heap_result); + + const auto source = old_heap->GetCPUDescriptorHandleForHeapStart(); + const auto dest = srv_descriptor_heap_->GetCPUDescriptorHandleForHeapStart(); + + device_->CopyDescriptorsSimple( + old_size, + dest, + source, + D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV + ); + + const auto increment_size = device_->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); + const auto new_cpu_begin = srv_descriptor_heap_->GetCPUDescriptorHandleForHeapStart().ptr; + const auto new_gpu_begin = srv_descriptor_heap_->GetGPUDescriptorHandleForHeapStart().ptr; + + for (std::size_t i = 0; i < old_size; ++i) + { + if (auto& [resource, cpu, gpu] = textures_[i]; resource != nullptr) + { + cpu.ptr = new_cpu_begin + i * increment_size; + gpu.ptr = new_gpu_begin + i * increment_size; + } + } + } + + const auto index = std::ranges::distance(textures_.begin(), it); + + return upload_texture(index, data, size); + } + + auto Dx12Renderer::do_update_texture(const Texture& texture) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(texture.uploaded(), "Create texture first!"); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(texture.dirty(), "No need to update texture!"); + + const auto index = id_to_gpu_handle(texture.id()); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(index < srv_max_size_); + auto& texture_gpu = textures_[index]; + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(texture_gpu.resource != nullptr); + + // todo + do_destroy_texture(texture.id()); + std::ignore = upload_texture(index, texture.data(), texture.size()); + } + + auto Dx12Renderer::do_destroy_texture(const texture_id_type texture_id) noexcept -> void + { + const auto index = id_to_gpu_handle(texture_id); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(index < srv_max_size_); + + auto& texture = textures_[index]; + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(texture.resource != nullptr); + // ComPtr + texture.resource = nullptr; + } + + auto Dx12Renderer::do_present(const RenderContext& renderer_context, const rect_type& display_area) noexcept -> void + { + const auto all_render_data = renderer_context.render_data(); + // const auto [display_x, display_y] = display_area.point; + const auto [display_width, display_height] = display_area.extent; + + const auto [total_vertex_count, total_index_count] = [&]() noexcept + { + struct sum + { + UINT vertex; + UINT index; + }; + + return std::ranges::fold_left( + all_render_data, + sum{.vertex = 0, .index = 0}, + [](const sum s, const RenderData& render_data) noexcept -> sum + { + const auto vertex_list = render_data.vertex_list.get(); + const auto index_list = render_data.index_list.get(); + + return {.vertex = s.vertex + static_cast(vertex_list.size()), .index = s.index + static_cast(index_list.size())}; + } + ); + }(); + + frame_resource_index_ += 1; + const auto this_frame_index = frame_resource_index_ % num_frames_in_flight; + auto& this_frame = frame_resource_[this_frame_index]; + auto& [this_frame_index_buffer, this_frame_index_count, this_frame_vertex_buffer, this_frame_vertex_count] = this_frame; + + constexpr D3D12_HEAP_PROPERTIES heap_properties{ + .Type = D3D12_HEAP_TYPE_UPLOAD, + .CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN, + .MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN, + .CreationNodeMask = 0, + .VisibleNodeMask = 0 + }; + // Create and grow vertex/index buffers if needed + if (this_frame_vertex_buffer == nullptr or this_frame_vertex_count < total_vertex_count) + { + // todo: grow factor + this_frame_vertex_count = total_vertex_count + 5000; + + const D3D12_RESOURCE_DESC resource_desc{ + .Dimension = D3D12_RESOURCE_DIMENSION_BUFFER, + .Alignment = 0, + .Width = this_frame_vertex_count * sizeof(vertex_type), + .Height = 1, + .DepthOrArraySize = 1, + .MipLevels = 1, + .Format = DXGI_FORMAT_UNKNOWN, + .SampleDesc = {.Count = 1, .Quality = 0}, + .Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR, + .Flags = D3D12_RESOURCE_FLAG_NONE + }; + check_hr_error( + device_->CreateCommittedResource( + &heap_properties, + D3D12_HEAP_FLAG_NONE, + &resource_desc, + D3D12_RESOURCE_STATE_GENERIC_READ, + nullptr, + IID_PPV_ARGS(this_frame_vertex_buffer.ReleaseAndGetAddressOf()) + ) + ); + } + if (this_frame_index_buffer == nullptr or this_frame_index_count < total_index_count) + { + // todo: grow factor + this_frame_index_count = total_index_count + 10000; + + const D3D12_RESOURCE_DESC resource_desc{ + .Dimension = D3D12_RESOURCE_DIMENSION_BUFFER, + .Alignment = 0, + .Width = this_frame_index_count * sizeof(index_type), + .Height = 1, + .DepthOrArraySize = 1, + .MipLevels = 1, + .Format = DXGI_FORMAT_UNKNOWN, + .SampleDesc = {.Count = 1, .Quality = 0}, + .Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR, + .Flags = D3D12_RESOURCE_FLAG_NONE + }; + check_hr_error( + device_->CreateCommittedResource( + &heap_properties, + D3D12_HEAP_FLAG_NONE, + &resource_desc, + D3D12_RESOURCE_STATE_GENERIC_READ, + nullptr, + IID_PPV_ARGS(this_frame_index_buffer.ReleaseAndGetAddressOf()) + ) + ); + } + + // Upload vertex/index data into a single contiguous GPU buffer + { + void* mapped_vertex_resource; + void* mapped_index_resource; + constexpr D3D12_RANGE vertex_range{.Begin = 0, .End = 0}; + constexpr D3D12_RANGE index_range{.Begin = 0, .End = 0}; + check_hr_error(this_frame_vertex_buffer->Map(0, &vertex_range, &mapped_vertex_resource)); + check_hr_error(this_frame_index_buffer->Map(0, &index_range, &mapped_index_resource)); + + auto* mapped_vertex = static_cast(mapped_vertex_resource); + auto* mapped_index = static_cast(mapped_index_resource); + + UINT vertex_offset = 0; + UINT index_offset = 0; + + std::ranges::for_each( + all_render_data, + [&](const RenderData& render_data) noexcept -> void + { + const auto vertex_list = render_data.vertex_list.get(); + const auto index_list = render_data.index_list.get(); + + // std::ranges::transform( + // vertex_list, + // mapped_vertex + vertex_offset, + // [](const vertex_type& vertex) noexcept -> vertex_type + // { + // // return { + // // .position = {vertex.position.x, vertex.position.y}, + // // .uv = {vertex.uv.x, vertex.uv.y}, + // // .color = vertex.color.to(primitive::color_format) + // // }; + // return std::bit_cast(vertex); + // } + // ); + std::ranges::copy(vertex_list, mapped_vertex + vertex_offset); + // std::ranges::transform( + // index_list, + // mapped_index + index_offset, + // [vertex_offset](const index_type index) noexcept -> index_type + // { + // return static_cast(index + vertex_offset); + // } + // ); + std::ranges::copy(index_list, mapped_index + index_offset); + + vertex_offset += static_cast(vertex_list.size()); + index_offset += static_cast(index_list.size()); + } + ); + + this_frame_vertex_buffer->Unmap(0, &vertex_range); + this_frame_index_buffer->Unmap(0, &index_range); + } + + // Setup orthographic projection matrix into our constant buffer + projection_matrix_type projection_matrix; + { + constexpr auto left = 0.f; + const auto right = display_width; + constexpr auto top = 0.f; + const auto bottom = display_height; + + const float mvp[4][4]{ + {2.0f / (right - left), 0.0f, 0.0f, 0.0f}, + {0.0f, 2.0f / (top - bottom), 0.0f, 0.0f}, + {0.0f, 0.0f, 0.5f, 0.0f}, + {(right + left) / (left - right), (top + bottom) / (bottom - top), 0.5f, 1.0f}, + }; + static_assert(sizeof(mvp) == sizeof(projection_matrix_type)); + std::memcpy(projection_matrix, mvp, sizeof(projection_matrix_type)); + } + + // Setup viewport + { + const D3D12_VIEWPORT viewport{ + .TopLeftX = .0f, + .TopLeftY = .0f, + .Width = static_cast(display_width), + .Height = static_cast(display_height), + .MinDepth = .0f, + .MaxDepth = 1.f + }; + command_list_->RSSetViewports(1, &viewport); + } + + // Bind shader/vertex buffers, root signature and pipeline state + { + ID3D12DescriptorHeap* descriptor_heaps[]{srv_descriptor_heap_.Get()}; + command_list_->SetDescriptorHeaps(1, descriptor_heaps); + + command_list_->SetGraphicsRootSignature(root_signature_.Get()); + command_list_->SetGraphicsRoot32BitConstants(0, sizeof(projection_matrix_type) / sizeof(float), &projection_matrix, 0); + + command_list_->SetPipelineState(pipeline_state_.Get()); + + const D3D12_VERTEX_BUFFER_VIEW vertex_buffer_view{ + .BufferLocation = this_frame_vertex_buffer->GetGPUVirtualAddress(), + .SizeInBytes = this_frame_vertex_count * static_cast(sizeof(vertex_type)), + .StrideInBytes = sizeof(vertex_type) + }; + command_list_->IASetVertexBuffers(0, 1, &vertex_buffer_view); + + const D3D12_INDEX_BUFFER_VIEW index_buffer_view{ + .BufferLocation = this_frame_index_buffer->GetGPUVirtualAddress(), + .SizeInBytes = this_frame_index_count * static_cast(sizeof(index_type)), + // ReSharper disable once CppUnreachableCode + .Format = sizeof(index_type) == 2 ? DXGI_FORMAT_R16_UINT : DXGI_FORMAT_R32_UINT + }; + command_list_->IASetIndexBuffer(&index_buffer_view); + command_list_->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + } + + // Setup blend factor + constexpr float blend_factor[4]{.0f, .0f, .0f, .0f}; + command_list_->OMSetBlendFactor(blend_factor); + + UINT total_index_offset = 0; + std::ranges::for_each( + all_render_data, + [this, &total_index_offset](const RenderData& render_data) noexcept -> void + { + const auto vertex_list = render_data.vertex_list.get(); + const auto index_list = render_data.index_list.get(); + + for (const auto& command_list = render_data.command_list.get(); + const auto& [clip_rect, texture_id, index_offset, element_count]: command_list) + { + const auto [point, extent] = clip_rect; + const D3D12_RECT rect + { + static_cast(point.x), + static_cast(point.y), + static_cast(point.x + extent.width), + static_cast(point.y + extent.height) + }; + command_list_->RSSetScissorRects(1, &rect); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(texture_id != invalid_texture_id); + const auto index = id_to_gpu_handle(texture_id); + const auto& texture = textures_[index]; + command_list_->SetGraphicsRootDescriptorTable(1, texture.gpu); + + const auto this_index_offset = static_cast(total_index_offset + index_offset); + command_list_->DrawIndexedInstanced(static_cast(element_count), 1, this_index_offset, 0, 0); + } + + total_index_offset += static_cast(index_list.size()); + } + ); + } +} + +#endif diff --git a/src/gfx/old/renderer_dx12.hpp b/src/gfx/old/renderer_dx12.hpp new file mode 100644 index 00000000..2b654764 --- /dev/null +++ b/src/gfx/old/renderer_dx12.hpp @@ -0,0 +1,100 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +// todo +#define GAL_PROMETHEUS_GFX_RENDER_DX12 + +#if defined(GAL_PROMETHEUS_GFX_RENDER_DX12) + +#include + +#include + +#include +#include + +namespace gal::prometheus::gfx +{ + using Microsoft::WRL::ComPtr; + + class Dx12Renderer final : public Renderer + { + public: + struct texture_type + { + ComPtr resource; + D3D12_CPU_DESCRIPTOR_HANDLE cpu; + D3D12_GPU_DESCRIPTOR_HANDLE gpu; + }; + + using textures_type = std::vector; + + private: + struct render_buffer_type + { + ComPtr index; + UINT index_count; + ComPtr vertex; + UINT vertex_count; + }; + + ComPtr device_; + ComPtr command_list_; + + ComPtr root_signature_; + ComPtr pipeline_state_; + ComPtr srv_descriptor_heap_; + // 8 descriptors are enough, and if not, then 1.5 times more + UINT srv_max_size_; + + textures_type textures_; + + constexpr static UINT num_frames_in_flight = 3; + // note: overflow(max + 1 => 0) + UINT frame_resource_index_ = (std::numeric_limits::max)(); + // render_buffer_type frame_resource_[num_frames_in_flight] = {}; + // num_frames_in_flight < 16 + render_buffer_type frame_resource_[16] = {}; + + render_buffer_type render_buffer_; + + [[nodiscard]] auto create_root_signature() noexcept -> bool; + [[nodiscard]] auto create_pipeline_state() noexcept -> bool; + [[nodiscard]] auto create_srv_descriptor_heap(UINT num) noexcept -> bool; + + [[nodiscard]] auto upload_texture( + std::size_t index, + Texture::data_view_type data, + Texture::size_type size, + bool record_resource = true + ) noexcept -> texture_id_type; + + public: + Dx12Renderer() noexcept; + Dx12Renderer(ID3D12Device* device, ID3D12GraphicsCommandList* command_list) noexcept; + Dx12Renderer(ComPtr device, ComPtr command_list) noexcept; + + auto bind_device(ID3D12Device* device) noexcept -> void; + auto bind_device(ComPtr device) noexcept -> void; + auto bind_command_list(ID3D12GraphicsCommandList* command_list) noexcept -> void; + auto bind_command_list(ComPtr command_list) noexcept -> void; + + private: + auto do_create() noexcept -> bool override; + auto do_destroy() noexcept -> void override; + + [[nodiscard]] auto do_ready() const noexcept -> bool override; + + auto do_create_texture(Texture::data_view_type data, Texture::size_type size) noexcept -> texture_id_type override; + auto do_update_texture(const Texture& texture) noexcept -> void override; + auto do_destroy_texture(texture_id_type texture_id) noexcept -> void override; + + auto do_present(const RenderContext& renderer_context, const rect_type& display_area) noexcept -> void override; + }; +} + +#endif diff --git a/src/gfx/old/texture.cpp b/src/gfx/old/texture.cpp new file mode 100644 index 00000000..bed7c86d --- /dev/null +++ b/src/gfx/old/texture.cpp @@ -0,0 +1,254 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +#include + +#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE + +namespace +{ + // #define STB_RECT_PACK_IMPLEMENTATION +#include +} // namespace + +namespace gal::prometheus::gfx +{ + struct Texture::pack_context_type + { + stbrp_context context{}; + std::vector nodes{}; + }; + + Texture::Texture(Texture&&) noexcept = default; + auto Texture::operator=(Texture&&) noexcept -> Texture& = default; + + Texture::~Texture() noexcept = default; + + Texture::Texture(const size_type size) noexcept + : pack_context_{memory::make_unique()}, + data_{std::make_unique_for_overwrite(static_cast(size.width) * size.height)}, + size_{size}, + dirty_{false}, + id_{invalid_texture_id} + { + pack_context_->nodes.resize(size.width); + + stbrp_init_target( + &pack_context_->context, + static_cast(size.width), + static_cast(size.height), + pack_context_->nodes.data(), + static_cast(pack_context_->nodes.size()) + ); + } + + auto Texture::create(Renderer& renderer) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not uploaded()); + + const auto id = renderer.create_texture(data(), size_); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(id != invalid_texture_id); + + dirty_ = false; + id_ = id; + } + + auto Texture::upload(Renderer& renderer) noexcept -> void + { + renderer.update_texture(*this); + dirty_ = false; + } + + auto Texture::upload_if_required(Renderer& renderer) noexcept -> void + { + if (dirty_) + { + renderer.update_texture(*this); + dirty_ = false; + } + } + + auto Texture::data() const noexcept -> data_view_type + { + return {data_.get(), area_size()}; + } + + auto Texture::area_size() const noexcept -> std::size_t + { + return static_cast(size_.width) * size_.height; + } + + auto Texture::size() const noexcept -> size_type + { + return size_; + } + + auto Texture::uv() const noexcept -> uv_type + { + const auto s = size(); + + return {1.f / static_cast(s.width), 1.f / static_cast(s.height)}; + } + + auto Texture::dirty() const noexcept -> bool + { + return dirty_; + } + + auto Texture::uploaded() const noexcept -> bool + { + return id_ != invalid_texture_id; + } + + auto Texture::id() const noexcept -> texture_id_type + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(uploaded()); + + return id_; + } + + auto Texture::select(const size_type size) noexcept -> BorrowTexture + { + stbrp_rect rect{.id = -1, .w = static_cast(size.width), .h = static_cast(size.height), .x = 0, .y = 0, .was_packed = 0}; + + if (stbrp_pack_rects(&pack_context_->context, &rect, 1)) + { + const point_type point{static_cast(rect.x), static_cast(rect.y)}; + + auto* address = data_.get() + (point.y * size.width + point.x); + const auto mapping = BorrowTexture::data_type::mapping_type{ + std::dextents{size.height, size.width}, + std::array{size_.width, 1}, + }; + const auto data = BorrowTexture::data_type{address, mapping}; + + dirty_ = true; + return {point, data}; + } + + return {BorrowTexture::invalid_point, {}}; + } + + BorrowTexture::BorrowTexture(const point_type point, const data_type data) noexcept + : point_{point}, + data_{data} {} + + auto BorrowTexture::valid() const noexcept -> bool + { + return point_ != invalid_point; + } + + auto BorrowTexture::point() const noexcept -> point_type + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(valid()); + + return point_; + } + + auto BorrowTexture::fill(const size_type::value_type y, const size_type::value_type offset, const size_type::value_type n, const element_type element) const noexcept -> void + { + const auto width = data_.extent(1); + const auto height = data_.extent(0); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(valid()); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(y < height); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(offset + n <= width); + + for (size_type::value_type x = offset; x < offset + n; ++x) + { + data_[y, x] = element; + } + } + + auto BorrowTexture::fill(const size_type::value_type y, const size_type::value_type offset, const data_view_type data) const noexcept -> void + { + const auto width = data_.extent(1); + const auto height = data_.extent(0); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(valid()); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(y < height); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(offset + data.size() <= width); + + for (size_type::value_type x = 0; x < data.size(); ++x) + { + data_[y, x + offset] = data[x]; + } + } + + auto BorrowTexture::fill(const size_type::value_type y, const size_type::value_type n, const element_type element) const noexcept -> void + { + const auto width = data_.extent(1); + const auto height = data_.extent(0); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(valid()); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(y < height); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(n <= width); + + fill(y, 0, n, element); + } + + auto BorrowTexture::fill(const size_type::value_type y, const data_view_type data) const noexcept -> void + { + const auto width = data_.extent(1); + const auto height = data_.extent(0); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(valid()); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(y < height); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(data.size() <= width); + + fill(y, 0, data); + } + + auto BorrowTexture::fill(const size_type::value_type y, const element_type element) const noexcept -> void + { + const auto width = data_.extent(1); + const auto height = data_.extent(0); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(valid()); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(y < height); + + fill(y, width, element); + } + + auto BorrowTexture::fill(const element_type element) const noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(valid()); + + const auto height = data_.extent(0); + for (size_type::value_type y = 0; y < height; ++y) + { + fill(y, element); + } + } + + auto BorrowTexture::fill(const data_view_type data) const noexcept -> void + { + const auto width = data_.extent(1); + const auto height = data_.extent(0); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(valid()); + + for (size_type::value_type y = 0; y < height; ++y) + { + const data_view_type sub{data.begin() + static_cast(y) * width, width}; + + fill(y, sub); + } + } + + auto BorrowTexture::operator[](const size_type::value_type x, const size_type::value_type y) const noexcept -> data_type::reference + { + const auto width = data_.extent(1); + const auto height = data_.extent(0); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(valid()); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(y < height); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(x < width); + + return data_[y, x]; + } +} // namespace gal::prometheus::gfx diff --git a/src/gfx/old/texture.hpp b/src/gfx/old/texture.hpp new file mode 100644 index 00000000..da6e762e --- /dev/null +++ b/src/gfx/old/texture.hpp @@ -0,0 +1,161 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include +#include + +#include + +#include + +namespace gal::prometheus::gfx +{ + class Texture final + { + public: + using point_type = primitive::basic_point_2d; + using size_type = primitive::basic_extent_2d; + + using element_type = std::uint32_t; + // size.width * size.height (RGBA) + using data_type = std::unique_ptr; + using data_view_type = std::span; + + using uv_type = primitive::basic_extent_2d; + + private: + struct pack_context_type; + memory::UniquePointer pack_context_; + + // ============================== + // CPU side + // ============================== + + data_type data_; + size_type size_; + + static_assert(sizeof(texture_id_type) == sizeof(std::uint64_t)); + std::uint64_t dirty_ : 1; + + // ============================== + // GPU side + // ============================== + + std::uint64_t id_ : 63; + + public: + Texture(const Texture&) noexcept = delete; + Texture(Texture&&) noexcept; // = default; + auto operator=(const Texture&) noexcept -> Texture& = delete; + auto operator=(Texture&&) noexcept -> Texture&; // = default; + + ~Texture() noexcept; + + explicit Texture(size_type size) noexcept; + + /** + * @brief Upload texture atlas data to GPU and get GPU resource handle + */ + auto create(Renderer& renderer) noexcept -> void; + + /** + * @brief Upload new texture atlas data to GPU (overwrite previous texture atlas data) + * @note Not checking if it needs to be re-uploaded + */ + auto upload(Renderer& renderer) noexcept -> void; + + /** + * @brief Upload new texture atlas data to GPU (overwrite previous texture atlas data) + * @note If no re-upload is required, the upload operation is not performed + */ + auto upload_if_required(Renderer& renderer) noexcept -> void; + + /** + * @brief Texture atlas data (for upload) + */ + [[nodiscard]] auto data() const noexcept -> data_view_type; + + /** + * @brief Texture atlas area size + */ + [[nodiscard]] auto area_size() const noexcept -> std::size_t; + + /** + * @brief Texture atlas size + */ + [[nodiscard]] auto size() const noexcept -> size_type; + + /** + * @brief Texture atlas uv scale (1.0f / size.width, 1.0f / size.height) + */ + [[nodiscard]] auto uv() const noexcept -> uv_type; + + /** + * @brief Does this texture atlas need to be re-uploaded to the GPU + */ + [[nodiscard]] auto dirty() const noexcept -> bool; + + /** + * @brief Texture atlas uploaded (to GPU) + */ + [[nodiscard]] auto uploaded() const noexcept -> bool; + + /** + * @brief Texture atlas id (usually a GPU resource handle) + */ + [[nodiscard]] auto id() const noexcept -> texture_id_type; + + /** + * @brief Find a region that can hold a (piece of) texture of @c size + * @param size Texture size + */ + [[nodiscard]] auto select(size_type size) noexcept -> BorrowTexture; + }; + + class BorrowTexture + { + public: + using point_type = Texture::point_type; + using size_type = Texture::size_type; + + using element_type = Texture::element_type; + using data_type = std::mdspan, std::layout_stride>; + using data_view_type = Texture::data_view_type; + + constexpr static point_type invalid_point{(std::numeric_limits::max)(), (std::numeric_limits::max)()}; + + private: + point_type point_; + data_type data_; + + public: + BorrowTexture(const BorrowTexture&) noexcept = delete; + BorrowTexture(BorrowTexture&&) noexcept = default; + auto operator=(const BorrowTexture&) noexcept -> BorrowTexture& = delete; + auto operator=(BorrowTexture&&) noexcept -> BorrowTexture& = default; + ~BorrowTexture() noexcept = default; + + BorrowTexture(point_type point, data_type data) noexcept; + + [[nodiscard]] auto valid() const noexcept -> bool; + + [[nodiscard]] auto point() const noexcept -> point_type; + + auto fill(size_type::value_type y, size_type::value_type offset, size_type::value_type n, element_type element) const noexcept -> void; + auto fill(size_type::value_type y, size_type::value_type offset, data_view_type data) const noexcept -> void; + + auto fill(size_type::value_type y, size_type::value_type n, element_type element) const noexcept -> void; + auto fill(size_type::value_type y, data_view_type data) const noexcept -> void; + + auto fill(size_type::value_type y, element_type element) const noexcept -> void; + + auto fill(element_type element) const noexcept -> void; + auto fill(data_view_type data) const noexcept -> void; + + auto operator[](size_type::value_type x, size_type::value_type y) const noexcept -> data_type::reference; + }; +} // namespace gal::prometheus::gfx diff --git a/src/gfx/old/type.hpp b/src/gfx/old/type.hpp new file mode 100644 index 00000000..ac57db4c --- /dev/null +++ b/src/gfx/old/type.hpp @@ -0,0 +1,193 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace gal::prometheus::gfx +{ + using point_type = primitive::basic_point_2d; + using uv_type = primitive::basic_point_2d; + using color_type = primitive::basic_color; + using vertex_type = primitive::basic_vertex; + using index_type = std::uint16_t; + + using extent_type = primitive::basic_extent_2d; + using rect_type = primitive::basic_rect_2d; + using circle_type = primitive::basic_circle_2d; + using ellipse_type = primitive::basic_ellipse_2d; + + // ========================================================= + // TEXTURE + // ========================================================= + + // DX11: ID3D11ShaderResourceView + // DX12: D3D12_GPU_DESCRIPTOR_HANDLE::ptr / HEAP index + using texture_id_type = std::uintptr_t; + constexpr texture_id_type invalid_texture_id{0}; + + class Texture; + class BorrowTexture; + + // ========================================================= + // FONT + // ========================================================= + + using texture_atlas_id_type = std::uint32_t; + constexpr texture_atlas_id_type invalid_texture_atlas_id{std::numeric_limits::max()}; + + using font_id_type = std::uint32_t; + constexpr font_id_type invalid_font_id{std::numeric_limits::max()}; + + enum class GlyphFlag : std::uint8_t + { + NONE = 0, + BOLD = 1 << 0, + ITALIC = 1 << 1, + }; + + class GlyphKey; + class GlyphInfo; + class GlyphParser; + + class FontGlyphQueue; + class Font; + class FontLoadQueue; + class Fonts; + + // ========================================================= + // RENDERER LIST + // ========================================================= + + enum class RenderListFlag : std::uint8_t + { + NONE = 0, + ANTI_ALIASED_LINE = 1 << 0, + ANTI_ALIASED_LINE_USE_TEXTURE = 1 << 1, + ANTI_ALIASED_FILL = 1 << 2, + }; + + enum class RenderFlag : std::uint8_t + { + NONE = 0, + // specify that shape should be closed + // @see RenderList::draw_polygon_line + // @see RenderList::draw_polygon_line_aa + // @see RenderList::path_stroke + CLOSED = 1 << 0, + // enable rounding left-top corner only (when rounding > 0.0f, we default to all corners) + // @see RenderList::path_rect + // @see RenderList::rect + // @see RenderList::rect_filled + ROUND_CORNER_LEFT_TOP = 1 << 1, + // enable rounding right_top corner only (when rounding > 0.0f, we default to all corners) + // @see RenderList::path_rect + // @see RenderList::rect + // @see RenderList::rect_filled + ROUND_CORNER_RIGHT_TOP = 1 << 2, + // enable rounding left-bottom corner only (when rounding > 0.0f, we default to all corners) + // @see RenderList::path_rect + // @see RenderList::rect + // @see RenderList::rect_filled + ROUND_CORNER_LEFT_BOTTOM = 1 << 3, + // enable rounding right-bottom corner only (when rounding > 0.0f, we default to all corners) + // @see RenderList::path_rect + // @see RenderList::rect + // @see RenderList::rect_filled + ROUND_CORNER_RIGHT_BOTTOM = 1 << 4, + // disable rounding on all corners (when rounding > 0.0f) + ROUND_CORNER_NONE = 1 << 5, + + ROUND_CORNER_LEFT = ROUND_CORNER_LEFT_TOP | ROUND_CORNER_LEFT_BOTTOM, + ROUND_CORNER_TOP = ROUND_CORNER_LEFT_TOP | ROUND_CORNER_RIGHT_TOP, + ROUND_CORNER_RIGHT = ROUND_CORNER_RIGHT_TOP | ROUND_CORNER_RIGHT_BOTTOM, + ROUND_CORNER_BOTTOM = ROUND_CORNER_LEFT_BOTTOM | ROUND_CORNER_RIGHT_BOTTOM, + + ROUND_CORNER_ALL = ROUND_CORNER_LEFT_TOP | ROUND_CORNER_RIGHT_TOP | ROUND_CORNER_LEFT_BOTTOM | ROUND_CORNER_RIGHT_BOTTOM, + ROUND_CORNER_DEFAULT = ROUND_CORNER_ALL, + ROUND_CORNER_MASK = ROUND_CORNER_ALL | ROUND_CORNER_NONE, + }; + + enum class RenderArcFlag : std::uint8_t + { + // [0~3) + Q1 = 1 << 0, + // [3~6) + Q2 = 1 << 1, + // [6~9) + Q3 = 1 << 2, + // [9~12) + Q4 = 1 << 3, + + RIGHT_TOP = Q1, + LEFT_TOP = Q2, + LEFT_BOTTOM = Q3, + RIGHT_BOTTOM = Q4, + TOP = Q1 | Q2, + BOTTOM = Q3 | Q4, + LEFT = Q2 | Q3, + RIGHT = Q1 | Q4, + ALL = Q1 | Q2 | Q3 | Q4, + + // [3, 0) + Q1_CLOCK_WISH = 1 << 4, + // [6, 3) + Q2_CLOCK_WISH = 1 << 5, + // [9, 6) + Q3_CLOCK_WISH = 1 << 6, + // [12, 9) + Q4_CLOCK_WISH = 1 << 7, + + RIGHT_TOP_CLOCK_WISH = Q1_CLOCK_WISH, + LEFT_TOP_CLOCK_WISH = Q2_CLOCK_WISH, + LEFT_BOTTOM_CLOCK_WISH = Q3_CLOCK_WISH, + RIGHT_BOTTOM_CLOCK_WISH = Q4_CLOCK_WISH, + TOP_CLOCK_WISH = Q1_CLOCK_WISH | Q2_CLOCK_WISH, + BOTTOM_CLOCK_WISH = Q3_CLOCK_WISH | Q4_CLOCK_WISH, + LEFT_CLOCK_WISH = Q2_CLOCK_WISH | Q3_CLOCK_WISH, + RIGHT_CLOCK_WISH = Q1_CLOCK_WISH | Q4_CLOCK_WISH, + ALL_CLOCK_WISH = Q1_CLOCK_WISH | Q2_CLOCK_WISH | Q3_CLOCK_WISH | Q4_CLOCK_WISH, + }; + + class RenderListSharedData; + class RenderList; + + // ========================================================= + // RENDERER + // ========================================================= + + class Renderer; + + // ========================================================= + // CONTEXT + // ========================================================= + + class TextureContext; + class RenderContext; +} // namespace gal::prometheus::gfx + +namespace gal::prometheus::meta::user_defined +{ + template<> + struct enum_is_flag : std::true_type {}; + + template<> + struct enum_is_flag : std::true_type {}; + + template<> + struct enum_is_flag : std::true_type {}; + + template<> + struct enum_is_flag : std::true_type {}; +} // namespace gal::prometheus::meta::user_defined diff --git a/src/gfx/render_list.cpp b/src/gfx/render_list.cpp new file mode 100644 index 00000000..1d2e5dc9 --- /dev/null +++ b/src/gfx/render_list.cpp @@ -0,0 +1,2533 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +#include + +#include +#include +#include +#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE + +namespace +{ + using namespace gal::prometheus; + using namespace gfx; + + // @see https://stackoverflow.com/a/2244088/15194693 + // Number of segments (N) is calculated using equation: + // N = ceil ( pi / acos(1 - error / r) ) where r > 0 and error <= r + [[nodiscard]] constexpr auto circle_segments_calc(const float radius, const float max_error) noexcept -> auto + { + constexpr auto circle_segments_roundup_to_even = [](const auto v) noexcept -> auto + { + return (v + 1) / 2 * 2; + }; + + return std::ranges::clamp( + circle_segments_roundup_to_even(static_cast(std::ceil(std::numbers::pi_v / std::acos(1 - std::ranges::min(radius, max_error) / radius)))), + RenderListSharedData::circle_segments_min, + RenderListSharedData::circle_segments_max + ); + } + + [[nodiscard]] constexpr auto circle_segments_calc_radius(const std::size_t n, const float max_error) noexcept -> auto + { + return max_error / (1 - math::cos(std::numbers::pi_v / std::ranges::max(static_cast(n), std::numbers::pi_v))); + } + + [[nodiscard]] constexpr auto circle_segments_calc_error(const std::size_t n, const float radius) noexcept -> auto + { + return (1 - math::cos(std::numbers::pi_v / std::ranges::max(static_cast(n), std::numbers::pi_v))) / radius; + } + + template + [[nodiscard]] constexpr auto arc_sample_points_calc() noexcept -> RenderListSharedData::arc_sample_points_type + { + return [](std::index_sequence) noexcept -> RenderListSharedData::arc_sample_points_type + { + const auto make_point = []() noexcept -> point_type + { + const auto a = static_cast(I) / static_cast(N) * 2 * std::numbers::pi_v; + return {math::cos(a), -math::sin(a)}; + }; + + return {{make_point.template operator()()...}}; + }(std::make_index_sequence{}); + } + + [[nodiscard]] constexpr auto range_of_arc(const RenderArcFlag flag) noexcept -> std::pair + { + static_assert(RenderListSharedData::arc_sample_points_count % 12 == 0); + constexpr auto factor = static_cast(RenderListSharedData::arc_sample_points_count / 12); + + switch (flag) + { + case RenderArcFlag::Q1: + { + return std::make_pair(0 * factor, 3 * factor); + } + case RenderArcFlag::Q2: + { + return std::make_pair(3 * factor, 6 * factor); + } + case RenderArcFlag::Q3: + { + return std::make_pair(6 * factor, 9 * factor); + } + case RenderArcFlag::Q4: + { + return std::make_pair(9 * factor, 12 * factor); + } + case RenderArcFlag::TOP: + { + return std::make_pair(0 * factor, 6 * factor); + } + case RenderArcFlag::BOTTOM: + { + return std::make_pair(6 * factor, 12 * factor); + } + case RenderArcFlag::LEFT: + { + return std::make_pair(3 * factor, 9 * factor); + } + case RenderArcFlag::RIGHT: + { + return std::make_pair(9 * factor, 15 * factor); + } + case RenderArcFlag::ALL: + { + return std::make_pair(0 * factor, 12 * factor); + } + case RenderArcFlag::Q1_CLOCK_WISH: + { + return std::make_pair(3 * factor, 0 * factor); + } + case RenderArcFlag::Q2_CLOCK_WISH: + { + return std::make_pair(6 * factor, 3 * factor); + } + case RenderArcFlag::Q3_CLOCK_WISH: + { + return std::make_pair(9 * factor, 6 * factor); + } + case RenderArcFlag::Q4_CLOCK_WISH: + { + return std::make_pair(12 * factor, 9 * factor); + } + case RenderArcFlag::TOP_CLOCK_WISH: + { + return std::make_pair(6 * factor, 0 * factor); + } + case RenderArcFlag::BOTTOM_CLOCK_WISH: + { + return std::make_pair(12 * factor, 6 * factor); + } + case RenderArcFlag::LEFT_CLOCK_WISH: + { + return std::make_pair(9 * factor, 3 * factor); + } + case RenderArcFlag::RIGHT_CLOCK_WISH: + { + return std::make_pair(15 * factor, 9 * factor); + } + case RenderArcFlag::ALL_CLOCK_WISH: + { + return std::make_pair(12 * factor, 0 * factor); + } + default: // NOLINT(clang-diagnostic-covered-switch-default) + { + GAL_PROMETHEUS_ERROR_UNREACHABLE(); + } + } + } + + [[nodiscard]] constexpr auto to_fixed_rect_corner_flag(const RenderRectFlag flag) noexcept -> RenderRectFlag + { + using enum RenderRectFlag; + + if ((flag & ROUND_CORNER_MASK) == NONE) + { + return ROUND_CORNER_ALL | flag; + } + + return flag; + } + + [[nodiscard]] constexpr auto to_fixed_normal(const float x, const float y) noexcept -> std::pair + { + if (const auto d = math::pow(x, 2) + math::pow(y, 2); d > 1e-6f) + { + // fixme + const auto inv_len = [d] + { + // #if defined(__AVX512F__) + // __m512 d_v = _mm512_set1_ps(d); + // __m512 inv_len_v = _mm512_rcp14_ps(d_v); + // return _mm512_cvtss_f32(inv_len_v); + // #elif defined(__AVX__) + // __m256 d_v = _mm256_set1_ps(d); + // __m256 inv_len_v = _mm256_rcp_ps(d_v); + // return _mm256_cvtss_f32(inv_len_v); + // #elif defined(__SSE4_1__) or defined(__SSE3__) or defined(__SSE__) + // __m128 d_v = _mm_set_ss(d); + // __m128 inv_len_v = _mm_rcp_ss(d_v); + // return _mm_cvtss_f32(inv_len_v); + // #else + return 1.0f / d; + // #endif + }(); + + return {x * inv_len, y * inv_len}; + } + + return {x, y}; + } + + // fixme + constexpr std::size_t bezier_curve_casteljau_max_level = 10; + + constexpr auto bezier_cubic_calc = [](const point_type& p1, const point_type& p2, const point_type& p3, const point_type& p4, const float tolerance) noexcept -> point_type + { + const auto u = 1.f - tolerance; + + const auto w1 = math::pow(u, 3); + const auto w2 = 3 * math::pow(u, 2) * tolerance; + const auto w3 = 3 * u * math::pow(tolerance, 2); + const auto w4 = math::pow(tolerance, 3); + + return {p1.x * w1 + p2.x * w2 + p3.x * w3 + p4.x * w4, p1.y * w1 + p2.y * w2 + p3.y * w3 + p4.y * w4}; + }; + + constexpr auto bezier_quadratic_calc = [](const point_type& p1, const point_type& p2, const point_type& p3, const float tolerance) noexcept -> point_type + { + const auto u = 1.f - tolerance; + + const auto w1 = math::pow(u, 2); + const auto w2 = 2 * u * tolerance; + const auto w3 = math::pow(tolerance, 2); + + return {p1.x * w1 + p2.x * w2 + p3.x * w3, p1.y * w1 + p2.y * w2 + p3.y * w3}; + }; + + class RenderDataAppender final + { + public: + using size_type = RenderData::size_type; + + using command_type = RenderData::command_type; + + using vertex_list_type = RenderData::vertex_list_type; + using index_list_type = RenderData::index_list_type; + + private: + // command_type::element_count + memory::RefWrapper element_count_; + // RenderList::vertex_list + memory::RefWrapper vertex_list_; + // RenderList::index_list_; + memory::RefWrapper index_list_; + + public: + RenderDataAppender(command_type& command, vertex_list_type& vertex_list, index_list_type& index_list) noexcept + : element_count_{command.element_count}, + vertex_list_{vertex_list}, + index_list_{index_list} {} + + explicit RenderDataAppender(RenderList::RenderListContext& context) noexcept; + // : RenderDataAppender{context.command_list.back(), context.vertex_list, context.index_list} {} + + [[nodiscard]] auto vertex_count() const noexcept -> size_type + { + const auto& list = vertex_list_.get(); + const auto size = list.size(); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(size < std::numeric_limits::max(), "Too many vertex!"); + + return static_cast(size); + } + + auto reserve(const size_type vertex_count, const size_type index_count) noexcept -> void + { + auto& element_count = element_count_.get(); + auto& vertex = vertex_list_.get(); + auto& index = index_list_.get(); + + element_count += index_count; + vertex.reserve(vertex.size() + vertex_count); + index.reserve(index.size() + index_count); + } + + auto reserve(const std::size_t vertex_count, const std::size_t index_count) noexcept -> void + { + reserve(static_cast(vertex_count), static_cast(index_count)); + } + + auto add_vertex(const point_type& point, const uv_type& uv, color_type color) noexcept -> void + { + auto& list = vertex_list_.get(); + + list.emplace_back(point, uv, color); + } + + auto add_index(const index_type a, const index_type b, const index_type c) noexcept -> void + { + auto& list = index_list_.get(); + + list.push_back(a); + list.push_back(b); + list.push_back(c); + } + }; +} + +namespace gal::prometheus::gfx +{ + RenderListSharedData::RenderListSharedData() noexcept + : circle_segment_counts{}, + circle_segment_max_error{0}, + arc_fast_sample_points{arc_sample_points_calc()}, + arc_fast_radius_cutoff{0}, + curve_tessellation_tolerance{1.25f}, + render_list_initial_flag{RenderListFlag::DEFAULT} + { + set_circle_tessellation_max_error(.3f); + } + + auto RenderListSharedData::circle_auto_segment_count(const float radius) const noexcept -> circle_segment_count_type + { + // ceil to never reduce accuracy + if (const auto radius_index = static_cast(radius + .999999f); radius_index < circle_segment_counts.size()) + { + return circle_segment_counts[radius_index]; + } + return static_cast(circle_segments_calc(radius, circle_segment_max_error)); + } + + auto RenderListSharedData::arc_sample_point(const std::size_t index) const noexcept -> const point_type& + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(index < arc_fast_sample_points.size()); + + return arc_fast_sample_points[index]; + } + + auto RenderListSharedData::set_circle_tessellation_max_error(const float max_error) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(max_error > .0f); + + if (circle_segment_max_error == max_error) // NOLINT(clang-diagnostic-float-equal) + { + return; + } + + circle_segment_counts[0] = arc_sample_points_count; + for (auto [index, count]: circle_segment_counts | std::views::drop(1) | std::views::enumerate) + { + const auto radius = static_cast(index); + count = static_cast(circle_segments_calc(radius, max_error)); + } + + circle_segment_max_error = max_error; + arc_fast_radius_cutoff = circle_segments_calc_radius(arc_sample_points_count, max_error); + } + + auto RenderListSharedData::set_curve_tessellation_tolerance(const float tolerance) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(tolerance > .0f); + + curve_tessellation_tolerance = tolerance; + } + + class RenderList::RenderListContext final + { + public: + using size_type = RenderData::size_type; + + using command_type = RenderData::command_type; + + using vertex_list_type = RenderData::vertex_list_type; + using index_list_type = RenderData::index_list_type; + using command_list_type = RenderData::command_list_type; + + mutable memory::RefWrapper context; + + RenderListFlag render_list_flag; + + // vertex_list: v1-v2-v3-v4 + v5-v6-v7-v8 + v9-v10-v11 => rect0 + rect1(clipped by rect0) + triangle0(clipped by rect1) + // index_list: 0/1/2-0/2/3 + 4/5/6-4/6/7 + 8/9/10 + // command_list: + // 0: .scissor = {0, 0, root_window_width, root_window_height}, .index_offset = 0, .element_count = root_window_element_count + 6 (two triangles => 0/1/2-0/2/3) + // 1: .scissor = {max(rect0.left, rect1.left), max(rect0.top, rect1.top), min(rect0.right, rect1.right), min(rect0.bottom, rect1.bottom)}, .index_offset = root_window_element_count + 6, .element_count = 6 (two triangles => 4/5/6-4/6/7) + // 2: .scissor = {...}, .index_offset = root_window_element_count + 12, .element_count = 3 (one triangle => 8/9/10) + command_list_type command_list; + vertex_list_type vertex_list; + index_list_type index_list; + + struct command_header_type + { + rect_type scissor; + texture_id_type texture; + }; + + command_header_type command_header; + + std::vector scissor_stack; + std::vector texture_stack; + + private: + auto push_command() noexcept -> void + { + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(command_header.scissor.valid()); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(command_header.texture != invalid_texture_id); + + command_type new_command + { + .scissor = command_header.scissor, + .texture = command_header.texture, + .index_offset = static_cast(index_list.size()), + // set by draw_xxx + .element_count = 0 + }; + + command_list.emplace_back(new_command); + } + + auto pop_command() noexcept -> void + { + command_list.pop_back(); + } + + auto on_scissor_changed() noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not command_list.empty()); + + // if current command is used with different settings we need to add a new command + // ReSharper disable once CppTooWideScopeInitStatement + auto& current_command = command_list.back(); + if (current_command.element_count != 0 and current_command.scissor != command_header.scissor) + { + push_command(); + return; + } + + // try to merge with previous command if it matches, else use current command + if (current_command.element_count == 0 and command_list.size() > 1) + { + // ReSharper disable once CppUseStructuredBinding + if (const auto& previous_command = command_list[command_list.size() - 2]; + command_header.scissor == previous_command.scissor and // + command_header.texture == previous_command.texture and // + // sequential + current_command.index_offset == previous_command.index_offset + previous_command.element_count + ) + { + pop_command(); + return; + } + } + + current_command.scissor = command_header.scissor; + } + + auto on_texture_changed() noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not command_list.empty()); + + // if current command is used with different settings we need to add a new command + // ReSharper disable once CppTooWideScopeInitStatement + auto& current_command = command_list.back(); + if (current_command.element_count != 0 and current_command.texture != command_header.texture) + { + push_command(); + return; + } + + // try to merge with previous command if it matches, else use current command + if (current_command.element_count == 0 and command_list.size() > 1) + { + // ReSharper disable once CppUseStructuredBinding + if (const auto& previous_command = command_list[command_list.size() - 2]; + command_header.scissor == previous_command.scissor and // + command_header.texture == previous_command.texture and // + // sequential + current_command.index_offset == previous_command.index_offset + previous_command.element_count + ) + { + pop_command(); + return; + } + } + + current_command.texture = command_header.texture; + } + + public: + explicit RenderListContext(Context& context) noexcept + : context{context}, + render_list_flag{shared_data().render_list_initial_flag}, + command_header{.scissor = {}, .texture = invalid_texture_id} {} + + [[nodiscard]] auto shared_data() const noexcept -> const RenderListSharedData& + { + const auto& c = context.get(); + + return c.render_list_shared_data; + } + + [[nodiscard]] auto default_texture() const noexcept -> texture_id_type + { + const auto& c = context.get(); + const auto& pc = c.private_; + const auto& texture_context = pc->texture_context; + + return texture_context.root_texture().id; + } + + auto reset() noexcept -> void + { + command_list.clear(); + vertex_list.clear(); + index_list.clear(); + + command_header.scissor = {}; + // the first texture is always the (default) font texture + command_header.texture = invalid_texture_id; + + // we always have a command ready in the buffer + push_command(); + + // todo + push_texture(default_texture()); + push_scissor(shared_data().fullscreen_scissor, false); + } + + // ---------------------------------------------------------------------------- + // SCISSOR & TEXTURE + + auto push_scissor(const rect_type& rect, const bool intersect_with_current_scissor) noexcept -> const rect_type& + { + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not rect.empty() and rect.valid()); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not command_list.empty()); + + if (intersect_with_current_scissor) + { + command_header.scissor = rect.combine_min(command_header.scissor); + } + else + { + command_header.scissor = rect; + } + command_header.scissor = intersect_with_current_scissor ? rect.combine_min(command_header.scissor) : rect; + + // If the rectangle is not valid (the right-bottom corner is to the left/top of the left-top), + // or if the rectangle is not valid after the intersection calculation (the two rectangles do not intersect), + // make it valid + const auto size = command_header.scissor.size(); + if (size.width < 0) + { + command_header.scissor.extent.width = 0; + } + if (size.height < 0) + { + command_header.scissor.extent.height = 0; + } + + scissor_stack.push_back(command_header.scissor); + on_scissor_changed(); + + return scissor_stack.back(); + } + + auto push_scissor_fullscreen() noexcept -> void + { + push_scissor(shared_data().fullscreen_scissor, false); + } + + auto pop_scissor() noexcept -> void + { + scissor_stack.pop_back(); + if (scissor_stack.empty()) + { + // if we have no scissor, use the fullscreen scissor + command_header.scissor = shared_data().fullscreen_scissor; + } + else + { + command_header.scissor = scissor_stack.back(); + } + + on_scissor_changed(); + } + + auto push_texture(const texture_id_type texture) noexcept -> void + { + texture_stack.push_back(texture); + command_header.texture = texture; + + on_texture_changed(); + } + + auto pop_texture() noexcept -> void + { + texture_stack.pop_back(); + if (texture_stack.empty()) + { + // if we have no texture, use the default texture + command_header.texture = default_texture(); + } + else + { + command_header.texture = texture_stack.back(); + } + + on_texture_changed(); + } + }; + + auto RenderList::Painter::draw_polygon_line(const color_type color, const float thickness, const bool close) noexcept -> void + { + const auto path_point_count = path_list_.size(); + const auto& path_point = path_list_; + + if (path_point_count < 2 or color.alpha == 0) + { + return; + } + + auto& render_list_context = *render_list_.get().context_; + const auto& shared_data = render_list_context.shared_data(); + RenderDataAppender appender{render_list_context}; + + const auto segments_count = close ? path_point_count : path_point_count - 1; + + const auto vertex_count = segments_count * 4; + const auto index_count = segments_count * 6; + appender.reserve(vertex_count, index_count); + + for (std::decay_t i = 0; i < segments_count; ++i) + { + const auto n = (i + 1) % path_point_count; + + const auto& p1 = path_point[i]; + const auto& p2 = path_point[n]; + + auto [normalized_x, normalized_y] = math::normalize(p2.x - p1.x, p2.y - p1.y); + normalized_x *= (thickness * .5f); + normalized_y *= (thickness * .5f); + + const auto current_vertex_index = static_cast(appender.vertex_count()); + const auto& opaque_uv = shared_data.white_pixel_uv; + + appender.add_vertex(p1 + point_type{normalized_y, -normalized_x}, opaque_uv, color); + appender.add_vertex(p2 + point_type{normalized_y, -normalized_x}, opaque_uv, color); + appender.add_vertex(p2 + point_type{-normalized_y, normalized_x}, opaque_uv, color); + appender.add_vertex(p1 + point_type{-normalized_y, normalized_x}, opaque_uv, color); + + appender.add_index(current_vertex_index + 0, current_vertex_index + 1, current_vertex_index + 2); + appender.add_index(current_vertex_index + 0, current_vertex_index + 2, current_vertex_index + 3); + } + } + + auto RenderList::Painter::draw_polygon_line_aa(const color_type color, float thickness, const bool close) noexcept -> void + { + const auto path_point_count = path_list_.size(); + const auto& path_point = path_list_; + + if (path_point_count < 2 or color.alpha == 0) + { + return; + } + + auto& render_list_context = *render_list_.get().context_; + const auto render_list_flag = render_list_context.render_list_flag; + const auto& shared_data = render_list_context.shared_data(); + RenderDataAppender appender{render_list_context}; + + const auto& opaque_uv = shared_data.white_pixel_uv; + const auto transparent_color = color.transparent(); + + const auto segments_count = close ? path_point_count : path_point_count - 1; + const auto is_thick_line = thickness > 1.f; + + thickness = std::ranges::max(thickness, 1.f); + const auto thickness_integer = static_cast(thickness); + const auto thickness_fractional = thickness - static_cast(thickness_integer); + + const auto is_use_texture = + ((render_list_flag & RenderListFlag::ANTI_ALIASED_LINE_USE_TEXTURE) == RenderListFlag::ANTI_ALIASED_LINE_USE_TEXTURE and + (thickness_integer < RenderListSharedData::baked_line_uv_count) and (thickness_fractional <= .00001f)); + + const auto vertex_cont = is_use_texture ? (path_point_count * 2) : (is_thick_line ? path_point_count * 4 : path_point_count * 3); + const auto index_count = is_use_texture ? (segments_count * 6) : (is_thick_line ? segments_count * 18 : segments_count * 12); + appender.reserve(vertex_cont, index_count); + + // The first items are normals at each line point, then after that there are either 2 or 4 temp points for each line point + path_list_type temp_buffer{}; + temp_buffer.resize(path_point_count * ((is_use_texture or not is_thick_line) ? 3 : 5)); + auto temp_buffer_normals = std::span{temp_buffer.begin(), path_point_count}; + auto temp_buffer_points = std::span{temp_buffer.begin() + static_cast(path_point_count), temp_buffer.end()}; + + // Calculate normals (tangents) for each line segment + for (std::decay_t i = 0; i < segments_count; ++i) + { + const auto n = (i + 1) % path_point_count; + const auto d = path_point[n] - path_point[i]; + + const auto [normalized_x, normalized_y] = math::normalize(d.x, d.y); + temp_buffer_normals[i].x = normalized_y; + temp_buffer_normals[i].y = -normalized_x; + } + + if (not close) + { + temp_buffer_normals[temp_buffer_normals.size() - 1] = temp_buffer_normals[temp_buffer_normals.size() - 2]; + } + + // If we are drawing a one-pixel-wide line without a texture, or a textured line of any width, we only need 2 or 3 vertices per point + if (is_use_texture or not is_thick_line) + { + // [PATH 1] Texture-based lines (thick or non-thick) + + // The width of the geometry we need to draw - this is essentially pixels for the line itself, plus "one pixel" for AA + const auto half_draw_size = is_use_texture ? ((thickness * .5f) + 1.f) : 1.f; + + // If line is not closed, the first and last points need to be generated differently as there are no normals to blend + if (not close) + { + temp_buffer_points[0] = path_point[0] + temp_buffer_normals[0] * half_draw_size; + temp_buffer_points[1] = path_point[0] - temp_buffer_normals[0] * half_draw_size; + temp_buffer_points[(path_point_count - 1) * 2 + 0] = path_point[path_point_count - 1] + temp_buffer_normals[path_point_count - 1] * half_draw_size; + temp_buffer_points[(path_point_count - 1) * 2 + 1] = path_point[path_point_count - 1] - temp_buffer_normals[path_point_count - 1] * half_draw_size; + } + + const auto current_vertex_index = static_cast(appender.vertex_count()); + + // Generate the indices to form a number of triangles for each line segment, and the vertices for the line edges + // This takes points n and n+1 and writes into n+1, with the first point in a closed line being generated from the final one (as n+1 wraps) + auto vertex_index_for_start = current_vertex_index; + for (std::decay_t first_point_of_segment = 0; first_point_of_segment < segments_count; ++first_point_of_segment) + { + const auto second_point_of_segment = (first_point_of_segment + 1) % path_point_count; + const auto vertex_index_for_end = static_cast( + // closed + (first_point_of_segment + 1) == path_point_count ? current_vertex_index : (vertex_index_for_start + (is_use_texture ? 2 : 3)) + ); + + // Average normals + const auto d = (temp_buffer_normals[first_point_of_segment] + temp_buffer_normals[second_point_of_segment]) * .5f; + // dm_x, dm_y are offset to the outer edge of the AA area + auto [dm_x, dm_y] = to_fixed_normal(d.x, d.y); + dm_x *= half_draw_size; + dm_y *= half_draw_size; + + // Add temporary vertexes for the outer edges + temp_buffer_points[second_point_of_segment * 2 + 0] = path_point[second_point_of_segment] + point_type{dm_x, dm_y}; + temp_buffer_points[second_point_of_segment * 2 + 1] = path_point[second_point_of_segment] - point_type{dm_x, dm_y}; + + if (is_use_texture) + { + // Add indices for two triangles + + // right + appender.add_index(vertex_index_for_end + 0, vertex_index_for_start + 0, vertex_index_for_start + 1); + // left + appender.add_index(vertex_index_for_end + 1, vertex_index_for_start + 1, vertex_index_for_end + 0); + } + else + { + // Add indexes for four triangles + + // right 1 + appender.add_index(vertex_index_for_end + 0, vertex_index_for_start + 0, vertex_index_for_start + 2); + // right 2 + appender.add_index(vertex_index_for_start + 2, vertex_index_for_end + 2, vertex_index_for_end + 0); + // left 1 + appender.add_index(vertex_index_for_end + 1, vertex_index_for_start + 1, vertex_index_for_start + 0); + // left 2 + appender.add_index(vertex_index_for_start + 0, vertex_index_for_end + 0, vertex_index_for_end + 1); + } + + vertex_index_for_start = vertex_index_for_end; + } + + // Add vertexes for each point on the line + if (is_use_texture) + { + const auto& uv = shared_data.baked_line_uvs[thickness_integer]; + + const auto uv0 = uv.left_top(); + const auto uv1 = uv.right_bottom(); + for (std::decay_t i = 0; i < path_point_count; ++i) + { + // left-side outer edge + appender.add_vertex(temp_buffer_points[i * 2 + 0], uv0, color); + // right-side outer edge + appender.add_vertex(temp_buffer_points[i * 2 + 1], uv1, color); + } + } + else + { + // If we're not using a texture, we need the center vertex as well + for (std::decay_t i = 0; i < path_point_count; ++i) + { + // center of line + appender.add_vertex(path_point[i], opaque_uv, color); + // left-side outer edge + appender.add_vertex(temp_buffer_points[i * 2 + 0], opaque_uv, transparent_color); + // right-side outer edge + appender.add_vertex(temp_buffer_points[i * 2 + 1], opaque_uv, transparent_color); + } + } + } + else + { + // [PATH 2] Non-texture-based lines (non-thick) + + // we need to draw the solid line core and thus require four vertices per point + const auto half_inner_thickness = (thickness - 1.f) * .5f; + + // If line is not closed, the first and last points need to be generated differently as there are no normals to blend + if (not close) + { + const auto point_last = path_point_count - 1; + temp_buffer_points[0] = path_point[0] + temp_buffer_normals[0] * (half_inner_thickness + 1.f); + temp_buffer_points[1] = path_point[0] + temp_buffer_normals[0] * (half_inner_thickness + 0.f); + temp_buffer_points[2] = path_point[0] - temp_buffer_normals[0] * (half_inner_thickness + 0.f); + temp_buffer_points[3] = path_point[0] - temp_buffer_normals[0] * (half_inner_thickness + 1.f); + temp_buffer_points[point_last * 4 + 0] = path_point[point_last] + temp_buffer_normals[point_last] * (half_inner_thickness + 1.f); + temp_buffer_points[point_last * 4 + 1] = path_point[point_last] + temp_buffer_normals[point_last] * (half_inner_thickness + 0.f); + temp_buffer_points[point_last * 4 + 2] = path_point[point_last] - temp_buffer_normals[point_last] * (half_inner_thickness + 0.f); + temp_buffer_points[point_last * 4 + 3] = path_point[point_last] - temp_buffer_normals[point_last] * (half_inner_thickness + 1.f); + } + + const auto current_vertex_index = static_cast(appender.vertex_count()); + + // Generate the indices to form a number of triangles for each line segment, and the vertices for the line edges + // This takes points n and n+1 and writes into n+1, with the first point in a closed line being generated from the final one (as n+1 wraps) + auto vertex_index_for_start = current_vertex_index; + for (std::decay_t first_point_of_segment = 0; first_point_of_segment < segments_count; ++first_point_of_segment) + { + const auto second_point_of_segment = (first_point_of_segment + 1) % path_point_count; + const auto vertex_index_for_end = static_cast((first_point_of_segment + 1) == path_point_count ? current_vertex_index : (vertex_index_for_start + 4)); + + // Average normals + const auto d = (temp_buffer_normals[first_point_of_segment] + temp_buffer_normals[second_point_of_segment]) * .5f; + const auto [dm_x, dm_y] = to_fixed_normal(d.x, d.y); + const auto dm_out_x = dm_x * (half_inner_thickness + 1.f); + const auto dm_out_y = dm_y * (half_inner_thickness + 1.f); + const auto dm_in_x = dm_x * (half_inner_thickness + 0.f); + const auto dm_in_y = dm_y * (half_inner_thickness + 0.f); + + // Add temporary vertices + temp_buffer_points[second_point_of_segment * 4 + 0] = path_point[second_point_of_segment] + point_type{dm_out_x, dm_out_y}; + temp_buffer_points[second_point_of_segment * 4 + 1] = path_point[second_point_of_segment] + point_type{dm_in_x, dm_in_y}; + temp_buffer_points[second_point_of_segment * 4 + 2] = path_point[second_point_of_segment] - point_type{dm_in_x, dm_in_y}; + temp_buffer_points[second_point_of_segment * 4 + 3] = path_point[second_point_of_segment] - point_type{dm_out_x, dm_out_y}; + + // Add indexes + appender.add_index(vertex_index_for_end + 1, vertex_index_for_end + 1, vertex_index_for_start + 2); + appender.add_index(vertex_index_for_start + 2, vertex_index_for_end + 2, vertex_index_for_end + 1); + appender.add_index(vertex_index_for_end + 1, vertex_index_for_start + 1, vertex_index_for_start + 0); + appender.add_index(vertex_index_for_start + 0, vertex_index_for_end + 0, vertex_index_for_end + 1); + appender.add_index(vertex_index_for_end + 2, vertex_index_for_start + 2, vertex_index_for_start + 3); + appender.add_index(vertex_index_for_start + 3, vertex_index_for_end + 3, vertex_index_for_end + 2); + + vertex_index_for_start = vertex_index_for_end; + } + + // Add vertices + for (std::decay_t i = 0; i < path_point_count; ++i) + { + appender.add_vertex(temp_buffer_points[i * 4 + 0], opaque_uv, transparent_color); + appender.add_vertex(temp_buffer_points[i * 4 + 1], opaque_uv, color); + appender.add_vertex(temp_buffer_points[i * 4 + 2], opaque_uv, color); + appender.add_vertex(temp_buffer_points[i * 4 + 2], opaque_uv, transparent_color); + } + } + } + + auto RenderList::Painter::draw_convex_polygon_line_filled(const color_type color) noexcept -> void + { + const auto path_point_count = path_list_.size(); + const auto& path_point = path_list_; + + if (path_point_count < 3 or color.alpha == 0) + { + return; + } + + auto& render_list_context = *render_list_.get().context_; + const auto& shared_data = render_list_context.shared_data(); + RenderDataAppender appender{render_list_context}; + + const auto vertex_count = path_point_count; + const auto index_count = (path_point_count - 2) * 3; + appender.reserve(vertex_count, index_count); + + const auto current_vertex_index = static_cast(appender.vertex_count()); + const auto& opaque_uv = shared_data.white_pixel_uv; + + std::ranges::for_each( + path_point, + [&](const point_type& point) noexcept -> void + { + appender.add_vertex(point, opaque_uv, color); + } + ); + for (index_type i = 2; std::cmp_less(i, path_point_count); ++i) + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_index + i - 1 >= current_vertex_index); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_index + i >= current_vertex_index); + + appender.add_index(current_vertex_index + 0, current_vertex_index + i - 1, current_vertex_index + i); + } + } + + auto RenderList::Painter::draw_convex_polygon_line_filled_aa(const color_type color) noexcept -> void + { + const auto path_point_count = path_list_.size(); + const auto& path_point = path_list_; + + if (path_point_count < 3 or color.alpha == 0) + { + return; + } + + auto& render_list_context = *render_list_.get().context_; + const auto& shared_data = render_list_context.shared_data(); + RenderDataAppender appender{render_list_context}; + + const auto& opaque_uv = shared_data.white_pixel_uv; + const auto transparent_color = color.transparent(); + + const auto vertex_count = path_point_count * 2; + const auto index_count = (path_point_count - 2) * 3 + path_point_count * 6; + appender.reserve(vertex_count, index_count); + + const auto current_vertex_inner_index = static_cast(appender.vertex_count()); + const auto current_vertex_outer_index = static_cast(appender.vertex_count() + 1); + + // Add indexes for fill + for (index_type i = 2; std::cmp_less(i, path_point_count); ++i) + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_inner_index + static_cast((i - 1) << 1) >= current_vertex_inner_index); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_inner_index + static_cast(i << 1) >= current_vertex_inner_index); + + appender.add_index(current_vertex_inner_index + 0, current_vertex_inner_index + static_cast((i - 1) << 1), current_vertex_inner_index + static_cast(i << 1)); + } + + path_list_type temp_buffer{}; + temp_buffer.resize(path_point_count); + auto temp_buffer_normals = std::span{temp_buffer.begin(), path_point_count}; + + for (auto i = path_point_count - 1, n = static_cast(0); n < path_point_count; i = n++) + { + const auto d = path_point[n] - path_point[i]; + + const auto [normalized_x, normalized_y] = math::normalize(d.x, d.y); + temp_buffer_normals[i].x = normalized_y; + temp_buffer_normals[i].y = -normalized_x; + } + + for (auto i = path_point_count - 1, n = static_cast(0); n < path_point_count; i = n++) + { + // Average normals + const auto d = (temp_buffer_normals[n] + temp_buffer_normals[i]) * .5f; + auto [dm_x, dm_y] = to_fixed_normal(d.x, d.y); + dm_x *= .5f; + dm_y *= .5f; + + // inner + appender.add_vertex(path_point[n] - point_type{dm_x, dm_y}, opaque_uv, color); + // outer + appender.add_vertex(path_point[n] + point_type{dm_x, dm_y}, opaque_uv, transparent_color); + + // Add indexes for fringes + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_inner_index + static_cast(n << 1) >= current_vertex_inner_index); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_inner_index + static_cast(i << 1) >= current_vertex_inner_index); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_outer_index + static_cast(i << 1) >= current_vertex_outer_index); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_outer_index + static_cast(i << 1) >= current_vertex_outer_index); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_outer_index + static_cast(n << 1) >= current_vertex_outer_index); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_inner_index + static_cast(n << 1) >= current_vertex_inner_index); + + appender.add_index( + current_vertex_inner_index + static_cast(n << 1), + current_vertex_inner_index + static_cast(i << 1), + current_vertex_outer_index + static_cast(i << 1) + ); + appender.add_index( + current_vertex_outer_index + static_cast(i << 1), + current_vertex_outer_index + static_cast(n << 1), + current_vertex_inner_index + static_cast(n << 1) + ); + } + } + + RenderList::Painter::~Painter() noexcept +#if GAL_PROMETHEUS_COMPILER_DEBUG + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(path_list_.empty(), "Call `stroke` to draw or call `clear` to discard!"); + } +#else + = default; +#endif + + RenderList::Painter::Painter(RenderList& render_list, const std::size_t reserve_point) noexcept + : render_list_{render_list} + { + reserve(reserve_point); + } + + auto RenderList::Painter::clear() noexcept -> Painter& + { + path_list_.clear(); + + return *this; + } + + auto RenderList::Painter::reserve_extra(const std::size_t size) noexcept -> Painter& + { + return reserve(path_list_.size() + size); + } + + auto RenderList::Painter::reserve(const std::size_t size) noexcept -> Painter& + { + path_list_.reserve(size); + + return *this; + } + + auto RenderList::Painter::pin(const point_type& point) noexcept -> Painter& + { + path_list_.emplace_back(point); + + return *this; + } + + auto RenderList::Painter::line(const point_type& from, const point_type& to) noexcept -> Painter& + { + pin(from); + pin(to); + + return *this; + } + + auto RenderList::Painter::triangle(const point_type& a, const point_type& b, const point_type& c) noexcept -> Painter& + { + pin(a); + pin(b); + pin(c); + + return *this; + } + + auto RenderList::Painter::quadrilateral(const point_type& p1, const point_type& p2, const point_type& p3, const point_type& p4) noexcept -> Painter& + { + pin(p1); + pin(p2); + pin(p3); + pin(p4); + + return *this; + } + + auto RenderList::Painter::rect(const rect_type& rect, float rounding, RenderRectFlag flag) noexcept -> Painter& + { + if (rounding >= .5f) + { + flag = to_fixed_rect_corner_flag(flag); + + const auto v = (flag & RenderRectFlag::ROUND_CORNER_TOP) == RenderRectFlag::ROUND_CORNER_TOP or (flag & RenderRectFlag::ROUND_CORNER_BOTTOM) == RenderRectFlag::ROUND_CORNER_BOTTOM; + const auto h = (flag & RenderRectFlag::ROUND_CORNER_LEFT) == RenderRectFlag::ROUND_CORNER_LEFT or (flag & RenderRectFlag::ROUND_CORNER_RIGHT) == RenderRectFlag::ROUND_CORNER_RIGHT; + + rounding = std::ranges::min(rounding, rect.width() * (v ? .5f : 1.f) - 1.f); + rounding = std::ranges::min(rounding, rect.height() * (h ? .5f : 1.f) - 1.f); + } + + if (rounding < .5f or (RenderRectFlag::ROUND_CORNER_MASK & flag) == RenderRectFlag::ROUND_CORNER_NONE) + { + reserve_extra(4); + quadrilateral(rect.left_top(), rect.right_top(), rect.right_bottom(), rect.left_bottom()); + } + else + { + const auto rounding_left_top = (flag & RenderRectFlag::ROUND_CORNER_LEFT_TOP) != RenderRectFlag::NONE ? rounding : 0; + const auto rounding_right_top = (flag & RenderRectFlag::ROUND_CORNER_RIGHT_TOP) != RenderRectFlag::NONE ? rounding : 0; + const auto rounding_left_bottom = (flag & RenderRectFlag::ROUND_CORNER_LEFT_BOTTOM) != RenderRectFlag::NONE ? rounding : 0; + const auto rounding_right_bottom = (flag & RenderRectFlag::ROUND_CORNER_RIGHT_BOTTOM) != RenderRectFlag::NONE ? rounding : 0; + + arc_fast({rect.left_top() + point_type{rounding_left_top, rounding_left_top}, rounding_left_top}, RenderArcFlag::Q2_CLOCK_WISH); + arc_fast({rect.right_top() + point_type{-rounding_right_top, rounding_right_top}, rounding_right_top}, RenderArcFlag::Q1_CLOCK_WISH); + arc_fast({rect.right_bottom() + point_type{-rounding_right_bottom, -rounding_right_bottom}, rounding_right_bottom}, RenderArcFlag::Q4_CLOCK_WISH); + arc_fast({rect.left_bottom() + point_type{rounding_left_bottom, -rounding_left_bottom}, rounding_left_bottom}, RenderArcFlag::Q3_CLOCK_WISH); + } + + return *this; + } + + auto RenderList::Painter::rect(const rect_type::point_type& left_top, const rect_type::extent_type& extent, const float rounding, const RenderRectFlag flag) noexcept -> Painter& + { + return rect({left_top, extent}, rounding, flag); + } + + auto RenderList::Painter::rect(const rect_type::point_type& left_top, const rect_type::point_type& right_bottom, const float rounding, const RenderRectFlag flag) noexcept -> Painter& + { + return rect({left_top, right_bottom}, rounding, flag); + } + + auto RenderList::Painter::circle_n(const circle_type& circle, const std::uint32_t segments) noexcept -> Painter& + { + return arc_n(circle, 0, std::numbers::pi_v * 2, segments); + } + + auto RenderList::Painter::circle_n(const circle_type::point_type& center, const circle_type::radius_value_type radius, const std::uint32_t segments) noexcept -> Painter& + { + return circle_n({center, radius}, segments); + } + + auto RenderList::Painter::circle(const circle_type& circle) noexcept -> Painter& + { + return arc_fast(circle, 0, RenderListSharedData::arc_sample_points_count - 1); + } + + auto RenderList::Painter::circle(const circle_type::point_type& center, const circle_type::radius_value_type radius) noexcept -> Painter& + { + return circle({center, radius}); + } + + auto RenderList::Painter::ellipse_n(const ellipse_type& ellipse, const std::uint32_t segments) noexcept -> Painter& + { + return arc_n(ellipse, 0, std::numbers::pi_v * 2, segments); + } + + auto RenderList::Painter::ellipse_n( + const ellipse_type::point_type& center, + const ellipse_type::radius_type& radius, + const ellipse_type::rotation_value_type rotation, + const std::uint32_t segments + ) noexcept -> Painter& + { + return ellipse_n({center, radius, rotation}, segments); + } + + auto RenderList::Painter::ellipse(const ellipse_type& ellipse) noexcept -> Painter& + { + // FIXME-OPT + const auto& render_list_context = *render_list_.get().context_; + const auto& shared_data = render_list_context.shared_data(); + + const auto segments = shared_data.circle_auto_segment_count(std::ranges::max(ellipse.radius.width, ellipse.radius.height)); + + return ellipse_n(ellipse, segments); + } + + auto RenderList::Painter::ellipse(const ellipse_type::point_type& center, const ellipse_type::radius_type& radius, const ellipse_type::rotation_value_type rotation) noexcept -> Painter& + { + return ellipse({center, radius, rotation}); + } + + auto RenderList::Painter::arc_fast(const circle_type& circle, const int sample_point_from, const int sample_point_to) noexcept -> Painter& + { + const auto& [center, radius] = circle; + + if (radius < .5f) + { + return pin(center); + } + + const auto& render_list_context = *render_list_.get().context_; + const auto& shared_data = render_list_context.shared_data(); + + // Calculate arc auto segment step size + auto step = RenderListSharedData::arc_sample_points_count / shared_data.circle_auto_segment_count(radius); + // Make sure we never do steps larger than one quarter of the circle + step = std::clamp(step, static_cast(1), RenderListSharedData::arc_sample_points_count / 4); + + const auto sample_range = math::abs(sample_point_to - sample_point_from); + const auto next_step = step; + + auto extra_max_sample = false; + if (step > 1) + { + const auto overstep = sample_range % step; + if (overstep > 0) + { + extra_max_sample = true; + + // When we have overstepped to avoid awkwardly looking one long line and one tiny one at the end, + // distribute first step range evenly between them by reducing first step size. + step -= (step - overstep) / 2; + } + + reserve_extra(sample_range / step + 1 + (overstep > 0)); + } + else + { + reserve_extra(sample_range + 1); + } + + auto sample_index = sample_point_from; + if (sample_index < 0 or std::cmp_greater_equal(sample_index, RenderListSharedData::arc_sample_points_count)) + { + sample_index = sample_index % static_cast(RenderListSharedData::arc_sample_points_count); + if (sample_index < 0) + { + sample_index += static_cast(RenderListSharedData::arc_sample_points_count); + } + } + + if (sample_point_to >= sample_point_from) + { + for (int i = sample_point_from; i <= sample_point_to; i += static_cast(step), sample_index += static_cast(step), step = next_step) + { + // a_step is clamped to arc_sample_points_count, so we have guaranteed that it will not wrap over range twice or more + if (std::cmp_greater_equal(sample_index, RenderListSharedData::arc_sample_points_count)) + { + sample_index -= static_cast(RenderListSharedData::arc_sample_points_count); + } + + const auto& sample_point = shared_data.arc_sample_point(sample_index); + + pin({center + sample_point * radius}); + } + } + else + { + for (int i = sample_point_from; i >= sample_point_to; i -= static_cast(step), sample_index -= static_cast(step), step = next_step) + { + // a_step is clamped to arc_sample_points_count, so we have guaranteed that it will not wrap over range twice or more + if (sample_index < 0) + { + sample_index += static_cast(RenderListSharedData::arc_sample_points_count); + } + + const auto& sample_point = shared_data.arc_sample_point(sample_index); + + pin({center + sample_point * radius}); + } + } + + if (extra_max_sample) + { + auto normalized_max_sample_index = sample_point_to % static_cast(RenderListSharedData::arc_sample_points_count); + if (normalized_max_sample_index < 0) + { + normalized_max_sample_index += RenderListSharedData::arc_sample_points_count; + } + + const auto& sample_point = shared_data.arc_sample_point(normalized_max_sample_index); + + pin({center + sample_point * radius}); + } + + return *this; + } + + auto RenderList::Painter::arc_fast(const circle_type& circle, const RenderArcFlag flag) noexcept -> Painter& + { + const auto [from, to] = range_of_arc(flag); + + return arc_fast(circle, from, to); + } + + auto RenderList::Painter::arc_n(const circle_type& circle, const float degree_from, const float degree_to, const std::uint32_t segments) noexcept -> Painter& + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(degree_to > degree_from); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(degree_from >= 0); + + const auto& [center, radius] = circle; + + if (radius < .5f) + { + return pin(center); + } + + reserve_extra(segments); + for (std::uint32_t i = 0; i < segments; ++i) + { + const auto a = degree_from + static_cast(i) / static_cast(segments) * (degree_to - degree_from); + pin({center + point_type{math::cos(a), math::sin(a)} * radius}); + } + + return *this; + } + + auto RenderList::Painter::arc(const circle_type& circle, const float degree_from, const float degree_to) noexcept -> Painter& + { + const auto& [center, radius] = circle; + + if (radius < .5f) + { + return pin(center); + } + + const auto& render_list_context = *render_list_.get().context_; + const auto& shared_data = render_list_context.shared_data(); + + // Automatic segment count + if (radius <= shared_data.arc_fast_radius_cutoff) + { + const auto is_reversed = degree_to < degree_from; + + // We are going to use precomputed values for mid-samples. + // Determine first and last sample in lookup table that belong to the arc + const auto sample_from_f = RenderListSharedData::arc_sample_points_count * degree_from / (std::numbers::pi_v * 2); + const auto sample_to_f = RenderListSharedData::arc_sample_points_count * degree_to / (std::numbers::pi_v * 2); + + const auto sample_from = is_reversed ? static_cast(math::floor(sample_from_f)) : static_cast(math::ceil(sample_from_f)); + const auto sample_to = is_reversed ? static_cast(math::ceil(sample_to_f)) : static_cast(math::floor(sample_to_f)); + const auto sample_mid = is_reversed ? static_cast(std::ranges::max(sample_from - sample_to, 0)) : static_cast(std::ranges::max(sample_to - sample_from, 0)); + + const auto segment_from_angle = static_cast(sample_from) * std::numbers::pi_v * 2 / RenderListSharedData::arc_sample_points_count; + const auto segment_to_angle = static_cast(sample_to) * std::numbers::pi_v * 2 / RenderListSharedData::arc_sample_points_count; + + const auto emit_start = math::abs(segment_from_angle - degree_from) >= 1e-5f; + const auto emit_end = math::abs(degree_to - segment_to_angle) >= 1e-5f; + + if (emit_start) + { + // The quadrant must be the same, otherwise it is not continuous with the path drawn by `arc_fast`. + pin({center + point_type{math::cos(degree_from), -math::sin(degree_from)} * radius}); + } + if (sample_mid > 0) + { + arc_fast(circle, sample_from, sample_to); + } + if (emit_end) + { + // The quadrant must be the same, otherwise it is not continuous with the path drawn by `arc_fast`. + pin({center + point_type{math::cos(degree_to), -math::sin(degree_to)} * radius}); + } + } + else + { + const auto arc_length = degree_to - degree_from; + const auto circle_segment_count = shared_data.circle_auto_segment_count(radius); + const auto arc_segment_count = std::ranges::max( + static_cast(math::ceil(static_cast(circle_segment_count) * arc_length / (std::numbers::pi_v * 2))), + static_cast(std::numbers::pi_v * 2 / arc_length) + ); + arc_n(circle, degree_from, degree_to, arc_segment_count); + } + + return *this; + } + + auto RenderList::Painter::arc_n(const ellipse_type& ellipse, const float degree_from, const float degree_to, const std::uint32_t segments) noexcept -> Painter& + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(degree_to > degree_from); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(degree_from >= 0); + + const auto& [center, radius, rotation] = ellipse; + + if (radius.width < .5f or radius.height < .5f) + { + return pin(center); + } + + const auto cos_theta = math::cos(rotation); + const auto sin_theta = math::sin(rotation); + + reserve_extra(segments); + for (std::uint32_t i = 0; i < segments; ++i) + { + const auto a = degree_from + static_cast(i) / static_cast(segments) * (degree_to - degree_from); + const auto offset = point_type{math::cos(a), math::sin(a)} * radius; + const auto prime_x = offset.x * cos_theta - offset.y * sin_theta; + const auto prime_y = offset.x * sin_theta + offset.y * cos_theta; + pin({center + point_type{prime_x, prime_y}}); + } + + return *this; + } + + auto RenderList::Painter::bezier_cubic_n(const point_type& p1, const point_type& p2, const point_type& p3, const point_type& p4, const std::uint32_t segments) noexcept -> Painter& + { + reserve_extra(segments); + const auto step = 1.f / static_cast(segments); + for (std::uint32_t i = 1; i <= segments; ++i) + { + pin(bezier_cubic_calc(p1, p2, p3, p4, step * static_cast(i))); + } + + return *this; + } + + auto RenderList::Painter::bezier_cubic(const point_type& p1, const point_type& p2, const point_type& p3, const point_type& p4) noexcept -> Painter& + { + // auto-tessellated + const auto bezier_cubic_curve_casteljau = functional::y_combinator + { + [this]( + auto& self, + const point_type& ip1, + const point_type& ip2, + const point_type& ip3, + const point_type& ip4, + const float tessellation_tolerance, + const std::size_t level + ) noexcept -> void + { + const auto dx = ip4.x - ip1.x; + const auto dy = ip4.y - ip1.y; + const auto d2 = math::abs((ip2.x - ip4.x) * dy - (ip2.y - ip4.y) * dx); + const auto d3 = math::abs((ip3.x - ip4.x) * dy - (ip3.y - ip4.y) * dx); + + if (math::pow(d2 + d3, 2) < tessellation_tolerance * (math::pow(dx, 2) + math::pow(dy, 2))) + { + pin(ip4); + } + else if (level < bezier_curve_casteljau_max_level) + { + const auto p_12 = (ip1 + ip2) * .5f; + const auto p_23 = (ip2 + ip3) * .5f; + const auto p_34 = (ip3 + ip4) * .5f; + const auto p_123 = (p_12 + p_23) * .5f; + const auto p_234 = (p_23 + p_34) * .5f; + const auto p_1234 = (p_123 + p_234) * .5f; + + self(ip1, p_12, p_123, p_1234, tessellation_tolerance, level + 1); + self(p_1234, p_234, p_34, ip4, tessellation_tolerance, level + 1); + } + } + }; + + const auto& render_list_context = *render_list_.get().context_; + const auto& shared_data = render_list_context.shared_data(); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(shared_data.curve_tessellation_tolerance > 0); + + reserve_extra(bezier_curve_casteljau_max_level * 2); + bezier_cubic_curve_casteljau(p1, p2, p3, p4, shared_data.curve_tessellation_tolerance, 0); + + return *this; + } + + auto RenderList::Painter::bezier_quadratic_n(const point_type& p1, const point_type& p2, const point_type& p3, const std::uint32_t segments) noexcept -> Painter& + { + reserve_extra(segments); + const auto step = 1.f / static_cast(segments); + for (std::uint32_t i = 1; i <= segments; ++i) + { + pin(bezier_quadratic_calc(p1, p2, p3, step * static_cast(i))); + } + + return *this; + } + + auto RenderList::Painter::bezier_quadratic(const point_type& p1, const point_type& p2, const point_type& p3) noexcept -> Painter& + { + // auto-tessellated + const auto bezier_quadratic_curve_casteljau = functional::y_combinator{ + [this]( + auto& self, + const point_type& ip1, + const point_type& ip2, + const point_type& ip3, + const float tessellation_tolerance, + const std::size_t level + ) noexcept -> void + { + const auto dx = ip3.x - ip1.x; + const auto dy = ip3.y - ip1.y; + const auto det = (ip2.x - ip3.x) * dy - (ip2.y - ip3.y) * dx; + + if (math::pow(det, 2) * 4.f < tessellation_tolerance * (math::pow(dx, 2) + math::pow(dy, 2))) + { + pin(ip3); + } + else if (level < bezier_curve_casteljau_max_level) + { + const auto p_12 = (ip1 + ip2) * .5f; + const auto p_23 = (ip2 + ip3) * .5f; + const auto p_123 = (p_12 + p_23) * .5f; + + self(ip1, p_12, p_123, tessellation_tolerance, level + 1); + self(p_123, p_23, ip3, tessellation_tolerance, level + 1); + } + } + }; + + const auto& render_list_context = *render_list_.get().context_; + const auto& shared_data = render_list_context.shared_data(); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(shared_data.curve_tessellation_tolerance > 0); + + reserve_extra(bezier_curve_casteljau_max_level * 2); + bezier_quadratic_curve_casteljau(p1, p2, p3, shared_data.curve_tessellation_tolerance, 0); + + return *this; + } + + auto RenderList::Painter::stroke(const color_type color) noexcept -> void + { + const auto& render_list_context = *render_list_.get().context_; + + if (const auto render_list_flag = render_list_context.render_list_flag; (render_list_flag & RenderListFlag::ANTI_ALIASED_FILL) != RenderListFlag::NONE) + { + draw_convex_polygon_line_filled_aa(color); + } + else + { + draw_convex_polygon_line_filled(color); + } + + clear(); + } + + auto RenderList::Painter::stroke(const color_type color, const float thickness, const bool close) noexcept -> void + { + const auto& render_list_context = *render_list_.get().context_; + + if (const auto render_list_flag = render_list_context.render_list_flag; (render_list_flag & RenderListFlag::ANTI_ALIASED_LINE) != RenderListFlag::NONE) + { + draw_polygon_line_aa(color, thickness, close); + } + else + { + draw_polygon_line(color, thickness, close); + } + + clear(); + } + + RenderList::RenderList(Context& context) noexcept + : context_{memory::make_unique(context)} {} + + RenderList::RenderList(RenderList&&) noexcept = default; + + auto RenderList::operator=(RenderList&&) noexcept -> RenderList& = default; + + RenderList::~RenderList() noexcept = default; + + auto RenderList::reset() noexcept -> void + { + context_->reset(); + } + + auto RenderList::set_flag(const RenderListFlag flag) noexcept -> void + { + context_->render_list_flag = flag; + } + + auto RenderList::data() const noexcept -> RenderData + { + return {.vertex_list = context_->vertex_list, .index_list = context_->index_list, .command_list = context_->command_list}; + } + + auto RenderList::push_scissor(const rect_type& rect, const bool intersect_with_current_scissor) noexcept -> const rect_type& + { + return context_->push_scissor(rect, intersect_with_current_scissor); + } + + auto RenderList::pop_scissor() noexcept -> void + { + context_->pop_scissor(); + } + + auto RenderList::push_texture(const texture_id_type texture) noexcept -> void + { + context_->push_texture(texture); + } + + auto RenderList::pop_texture() noexcept -> void + { + context_->pop_texture(); + } + + auto RenderList::line( + const point_type& from, + const point_type& to, + const color_type color, + const float thickness + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + painter(2).line(from, to).stroke(color, thickness, false); + } + + auto RenderList::triangle( + const point_type& a, + const point_type& b, + const point_type& c, + const color_type color, + const float thickness + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + painter(3).triangle(a, b, c).stroke(color, thickness, true); + } + + auto RenderList::triangle_filled( + const point_type& a, + const point_type& b, + const point_type& c, + const color_type color + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + painter(3).triangle(a, b, c).stroke(color); + } + + auto RenderList::quadrilateral( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const point_type& p4, + const color_type color, + const float thickness + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + painter(4).quadrilateral(p1, p2, p3, p4).stroke(color, thickness, true); + } + + auto RenderList::quadrilateral_filled( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const point_type& p4, + const color_type color + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + painter(4).quadrilateral(p1, p2, p3, p4).stroke(color); + } + + auto RenderList::rect( + const rect_type& rect, + const color_type color, + const float rounding, + const RenderRectFlag flag, + const float thickness + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + painter().rect(rect, rounding, flag).stroke(color, thickness, true); + } + + auto RenderList::rect( + const point_type& left_top, + const rect_type::extent_type& extent, + const color_type color, + const float rounding, + const RenderRectFlag flag, + const float thickness + ) noexcept -> void + { + return rect({left_top, extent}, color, rounding, flag, thickness); + } + + auto RenderList::rect( + const rect_type::point_type& left_top, + const rect_type::point_type& right_bottom, + const color_type color, + const float rounding, + const RenderRectFlag flag, + const float thickness + ) noexcept -> void + { + return rect({left_top, right_bottom}, color, rounding, flag, thickness); + } + + auto RenderList::rect_filled( + const rect_type& rect, + const color_type color, + const float rounding, + const RenderRectFlag flag + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + if (rounding < .5f or (RenderRectFlag::ROUND_CORNER_MASK & flag) == RenderRectFlag::ROUND_CORNER_NONE) + { + rect_filled(rect, color, color, color, color); + } + else + { + painter().rect(rect, rounding, flag).stroke(color); + } + } + + auto RenderList::rect_filled( + const point_type& left_top, + const rect_type::extent_type& extent, + const color_type color, + const float rounding, + const RenderRectFlag flag + ) noexcept -> void + { + return rect_filled({left_top, extent}, color, rounding, flag); + } + + auto RenderList::rect_filled( + const rect_type::point_type& left_top, + const rect_type::point_type& right_bottom, + const color_type color, + const float rounding, + const RenderRectFlag flag + ) noexcept -> void + { + return rect_filled({left_top, right_bottom}, color, rounding, flag); + } + + auto RenderList::rect_filled( + const rect_type& rect, + const color_type color_left_top, + const color_type color_right_top, + const color_type color_left_bottom, + const color_type color_right_bottom + ) noexcept -> void + { + if (color_left_top.alpha == 0 or color_right_top.alpha == 0 or color_left_bottom.alpha == 0 or color_right_bottom.alpha == 0) + { + return; + } + + const auto& shared_data = context_->shared_data(); + const auto& opaque_uv = shared_data.white_pixel_uv; + + RenderDataAppender appender{*context_}; + + // two triangle without path + constexpr RenderDataAppender::size_type vertex_count = 4; + constexpr RenderDataAppender::size_type index_count = 6; + appender.reserve(vertex_count, index_count); + + const auto current_vertex_index = static_cast(appender.vertex_count()); + + appender.add_vertex(rect.left_top(), opaque_uv, color_left_top); + appender.add_vertex(rect.right_top(), opaque_uv, color_right_top); + appender.add_vertex(rect.right_bottom(), opaque_uv, color_left_bottom); + appender.add_vertex(rect.left_bottom(), opaque_uv, color_right_bottom); + + appender.add_index(current_vertex_index + 0, current_vertex_index + 1, current_vertex_index + 2); + appender.add_index(current_vertex_index + 0, current_vertex_index + 2, current_vertex_index + 3); + } + + auto RenderList::rect_filled( + const point_type& left_top, + const rect_type::extent_type& extent, + const color_type color_left_top, + const color_type color_right_top, + const color_type color_left_bottom, + const color_type color_right_bottom + ) noexcept -> void + { + return rect_filled({left_top, extent}, color_left_top, color_right_top, color_left_bottom, color_right_bottom); + } + + auto RenderList::rect_filled( + const rect_type::point_type& left_top, + const rect_type::point_type& right_bottom, + const color_type color_left_top, + const color_type color_right_top, + const color_type color_left_bottom, + const color_type color_right_bottom + ) noexcept -> void + { + return rect_filled({left_top, right_bottom}, color_left_top, color_right_top, color_left_bottom, color_right_bottom); + } + + auto RenderList::circle_n( + const circle_type& circle, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept -> void + { + if (color.alpha == 0 or circle.radius < .5f or segments < 3) + { + return; + } + + painter().circle_n(circle, segments).stroke(color, thickness, true); + } + + auto RenderList::circle_n( + const circle_type::point_type& center, + const circle_type::radius_value_type radius, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept -> void + { + return circle_n({center, radius}, color, segments, thickness); + } + + auto RenderList::circle_n_filled( + const circle_type& circle, + const color_type color, + const std::uint32_t segments + ) noexcept -> void + { + if (color.alpha == 0 or circle.radius < .5f or segments < 3) + { + return; + } + + painter().circle_n(circle, segments).stroke(color); + } + + auto RenderList::circle_n_filled( + const circle_type::point_type& center, + const circle_type::radius_value_type radius, + const color_type color, + const std::uint32_t segments + ) noexcept -> void + { + return circle_n_filled({center, radius}, color, segments); + } + + void RenderList::circle( + const circle_type& circle, + const color_type color, + const float thickness + ) noexcept + { + if (color.alpha == 0 or circle.radius < .5f) + { + return; + } + + painter().circle(circle).stroke(color, thickness, true); + } + + auto RenderList::circle( + const circle_type::point_type& center, + const circle_type::radius_value_type radius, + const color_type color, + const float thickness + ) noexcept -> void + { + return circle({center, radius}, color, thickness); + } + + auto RenderList::circle_filled( + const circle_type& circle, + const color_type color + ) noexcept -> void + { + if (color.alpha == 0 or circle.radius < .5f) + { + return; + } + + painter().circle(circle).stroke(color); + } + + auto RenderList::circle_filled( + const circle_type::point_type& center, + const circle_type::radius_value_type radius, + const color_type color + ) noexcept -> void + { + circle_filled({center, radius}, color); + } + + auto RenderList::ellipse_n( + const ellipse_type& ellipse, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept -> void + { + if (color.alpha == 0 or ellipse.radius.width < .5f or ellipse.radius.height < .5f or segments < 3) + { + return; + } + + painter().ellipse_n(ellipse, segments).stroke(color, thickness, true); + } + + void RenderList::ellipse_n( + const ellipse_type::point_type& center, + const ellipse_type::radius_type& radius, + const ellipse_type::rotation_value_type rotation, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept + { + return ellipse_n({center, radius, rotation}, color, segments, thickness); + } + + auto RenderList::ellipse_n_filled( + const ellipse_type& ellipse, + const color_type color, + const std::uint32_t segments + ) noexcept -> void + { + if (color.alpha == 0 or ellipse.radius.width < .5f or ellipse.radius.height < .5f or segments < 3) + { + return; + } + + painter().ellipse_n(ellipse, segments).stroke(color); + } + + auto RenderList::ellipse_n_filled( + const ellipse_type::point_type& center, + const ellipse_type::radius_type& radius, + const ellipse_type::rotation_value_type rotation, + const color_type color, + const std::uint32_t segments + ) noexcept -> void + { + return ellipse_n_filled({center, radius, rotation}, color, segments); + } + + auto RenderList::ellipse( + const ellipse_type& ellipse, + const color_type color, + const float thickness + ) noexcept -> void + { + if (color.alpha == 0 or ellipse.radius.width < .5f or ellipse.radius.height < .5f) + { + return; + } + + painter().ellipse(ellipse).stroke(color, thickness, true); + } + + auto RenderList::ellipse( + const ellipse_type::point_type& center, + const ellipse_type::radius_type& radius, + const ellipse_type::rotation_value_type rotation, + const color_type color, + const float thickness + ) noexcept -> void + { + return ellipse({center, radius, rotation}, color, thickness); + } + + auto RenderList::ellipse_filled( + const ellipse_type& ellipse, + const color_type color + ) noexcept -> void + { + if (color.alpha == 0 or ellipse.radius.width < .5f or ellipse.radius.height < .5f) + { + return; + } + + painter().ellipse(ellipse).stroke(color); + } + + auto RenderList::ellipse_filled( + const ellipse_type::point_type& center, + const ellipse_type::radius_type& radius, + const ellipse_type::rotation_value_type rotation, + const color_type color + ) noexcept -> void + { + return ellipse_filled({center, radius, rotation}, color); + } + + auto RenderList::bezier_cubic_n( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const point_type& p4, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + painter().bezier_cubic_n(p1, p2, p3, p4, segments).stroke(color, thickness, false); + } + + auto RenderList::bezier_cubic( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const point_type& p4, + const color_type color, + const float thickness + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + painter().bezier_cubic(p1, p2, p3, p4).stroke(color, thickness, false); + } + + auto RenderList::bezier_quadratic_n( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + painter().bezier_quadratic_n(p1, p2, p3, segments).stroke(color, thickness, false); + } + + auto RenderList::bezier_quadratic( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const color_type color, + const float thickness + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + painter().bezier_quadratic(p1, p2, p3).stroke(color, thickness, false); + } + + auto RenderList::text( + const std::string_view utf8_text, + const std::uint32_t font_size, + const point_type& point, + const color_type color, + const float wrap_width + ) noexcept -> void + { + return this->text(utf8_text, font_size, point, color, GlyphFlag::NONE, wrap_width); + } + + auto RenderList::text( + const std::string_view utf8_text, + const std::uint32_t font_size, + const point_type& point, + const color_type color, + const GlyphFlag flag, + const float wrap_width + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(wrap_width > 0); + + // todo: + // The texture used for glyphs may not be the default texture, then we need to switch the texture, + // but at this time, the glyph information may not be written to the texture atlas (e.g. the first frame), then we can't know the ID of the texture atlas + + auto& render_list_context = *context_; + auto& context = render_list_context.context.get(); + // ReSharper disable once CppUseStructuredBinding + auto& context_private = *context.private_; + + const auto& texture_context = context_private.texture_context; + auto& glyph_context = context_private.glyph_context; + + const auto utf32_text = chars::convert(utf8_text); + const auto glyphs = glyph_context.glyph_of(utf32_text, font_size, flag); + + if (const auto has_glyph_not_uploaded = std::ranges::any_of( + glyphs, + [](const auto& glyph) noexcept -> bool + { + if (const auto& g = glyph.get(); g.visible) + { + return g.texture_atlas_id == invalid_texture_atlas_id; + } + + return false; + } + ); + has_glyph_not_uploaded) + { + // skip this frame? + return; + } + + // const auto visible_glyph_count = std::ranges::count_if( + // glyphs, + // [](const auto& glyph) noexcept -> bool + // { + // const auto& g = glyph.get(); + // + // return g.visible; + // } + // ); + // + // // two triangle without path + // const auto vertex_count = std::size_t{4} * visible_glyph_count; + // const auto index_count = std::size_t{6} * visible_glyph_count; + // RenderDataAppender appender{render_list_context}; + // appender.reserve(vertex_count, index_count); + + const auto wrap_pos_x = point.x + wrap_width; + const auto line_height = static_cast(font_size); + auto cursor = point + point_type{0, line_height}; + + auto adjust_cursor_position = [=, &cursor](const float advance_x) noexcept -> void + { + if (cursor.x + advance_x > wrap_pos_x) + { + cursor.x = point.x; + cursor.y += line_height; + } + }; + + for (const auto [codepoint, glyph]: std::views::zip(utf32_text, glyphs)) + { + // ReSharper disable once CppUseStructuredBinding + if (auto& g = glyph.get(); g.visible) + { + const auto& glyph_rect = g.rect; + const auto& glyph_uv = g.uv; + const auto glyph_advance_x = g.advance_x; + const auto glyph_colored = g.colored; + + const auto& texture = texture_context.select_texture(g.texture_atlas_id); + const auto new_texture = render_list_context.command_header.texture != texture.id; + + if (new_texture) + { + render_list_context.push_texture(texture.id); + } + + // We could have used RenderDataAppender::reserve on the outer scope to allocate space once, but we can't guarantee that we won't switch textures when rendering characters + + // two triangle without path + constexpr auto vertex_count = std::size_t{4}; + constexpr auto index_count = std::size_t{6}; + RenderDataAppender appender{render_list_context}; + appender.reserve(vertex_count, index_count); + + adjust_cursor_position(glyph_advance_x); + + const auto left_top = glyph_rect.left_top().to(); + const rect_type char_rect + { + cursor + point_type{left_top.x, -left_top.y}, + glyph_rect.size().to() + }; + const auto color_may_colored = glyph_colored ? color.transparent() : color; + const auto current_vertex_index = static_cast(appender.vertex_count()); + + appender.add_vertex(char_rect.left_top(), glyph_uv.left_top(), color_may_colored); + appender.add_vertex(char_rect.right_top(), glyph_uv.right_top(), color_may_colored); + appender.add_vertex(char_rect.right_bottom(), glyph_uv.right_bottom(), color_may_colored); + appender.add_vertex(char_rect.left_bottom(), glyph_uv.left_bottom(), color_may_colored); + + appender.add_index(current_vertex_index + 0, current_vertex_index + 1, current_vertex_index + 2); + appender.add_index(current_vertex_index + 0, current_vertex_index + 2, current_vertex_index + 3); + + cursor.x += glyph_advance_x; + + if (new_texture) + { + render_list_context.pop_texture(); + } + } + else + { + if (codepoint == U'\n' or codepoint == U'\r') + { + cursor.x = point.x; + cursor.y += line_height; + } + else if (codepoint == U'\t') + { + constexpr std::size_t step = 4; + const auto& space_glyph = glyph_context.glyph_of(' ', font_size, flag); + const auto tab_width = step * space_glyph.advance_x; + + if (const auto remaining_width = wrap_width - (cursor.x - point.x); + remaining_width >= tab_width) + { + cursor.x += tab_width; + } + else + { + const auto n = remaining_width / space_glyph.advance_x; + const auto n_integer = static_cast(n); + + cursor.x = point.x + static_cast(step - n_integer) * space_glyph.advance_x; + cursor.y += line_height; + } + } + else + { + adjust_cursor_position(g.advance_x); + + cursor.x += g.advance_x; + } + } + } + } + + auto RenderList::text_size( + const std::string_view utf8_text, + const std::uint32_t font_size, + const float wrap_width + ) noexcept -> extent_type + { + return this->text_size(utf8_text, font_size, GlyphFlag::NONE, wrap_width); + } + + auto RenderList::text_size( + const std::string_view utf8_text, + const std::uint32_t font_size, + const GlyphFlag flag, + const float wrap_width + ) noexcept -> extent_type + { + const auto& render_list_context = *context_; + auto& context = render_list_context.context.get(); + // ReSharper disable once CppUseStructuredBinding + auto& context_private = *context.private_; + + auto& glyph_context = context_private.glyph_context; + + const auto utf32_text = chars::convert(utf8_text); + const auto glyphs = glyph_context.glyph_of(utf32_text, font_size, flag); + + if (const auto has_glyph_not_uploaded = std::ranges::any_of( + glyphs, + [](const auto& glyph) noexcept -> bool + { + if (const auto& g = glyph.get(); g.visible) + { + return g.texture_atlas_id == invalid_texture_atlas_id; + } + + return false; + } + ); + has_glyph_not_uploaded) + { + // skip this frame? + return {0, 0}; + } + + const auto line_height = static_cast(font_size); + + float max_width = 0; + float current_width = 0; + float total_height = line_height; + + for (const auto [codepoint, glyph]: std::views::zip(utf32_text, glyphs)) + { + if (auto& g = glyph.get(); g.visible) + { + if (const auto glyph_advance_x = g.advance_x; + current_width + glyph_advance_x > wrap_width) + { + max_width = std::ranges::max(max_width, current_width); + current_width = glyph_advance_x; + total_height += line_height; + } + else + { + current_width += glyph_advance_x; + } + } + else + { + if (codepoint == U'\n' or codepoint == U'\r') + { + max_width = std::ranges::max(max_width, current_width); + current_width = 0; + total_height += line_height; + } + else if (codepoint == U'\t') + { + constexpr std::size_t step = 4; + const auto& space_glyph = glyph_context.glyph_of(' ', font_size, flag); + const auto tab_width = step * space_glyph.advance_x; + + if (const auto remaining_width = wrap_width - current_width; remaining_width >= tab_width) + { + current_width += tab_width; + } + else + { + const auto n = remaining_width / space_glyph.advance_x; + const auto n_integer = static_cast(n); + + max_width += static_cast(n_integer) * space_glyph.advance_x; + current_width = static_cast(step - n_integer) * space_glyph.advance_x; + total_height += line_height; + } + } + else + { + current_width += g.advance_x; + } + } + } + + max_width = std::ranges::max(max_width, current_width); + + return {max_width, total_height}; + } + + auto RenderList::image( + const texture_id_type texture_id, + const rect_type::point_type& display_p1, + const rect_type::point_type& display_p2, + const rect_type::point_type& display_p3, + const rect_type::point_type& display_p4, + const uv_type& uv_p1, + const uv_type& uv_p2, + const uv_type& uv_p3, + const uv_type& uv_p4, + const color_type color + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + auto& render_list_context = *context_; + + const auto new_texture = render_list_context.command_header.texture != texture_id; + + if (new_texture) + { + render_list_context.push_texture(texture_id); + } + + RenderDataAppender appender{render_list_context}; + + // two triangle without path + constexpr RenderDataAppender::size_type vertex_count = 4; + constexpr RenderDataAppender::size_type index_count = 6; + appender.reserve(vertex_count, index_count); + + const auto current_vertex_index = static_cast(appender.vertex_count()); + + appender.add_vertex(display_p1, uv_p1, color); + appender.add_vertex(display_p2, uv_p2, color); + appender.add_vertex(display_p3, uv_p3, color); + appender.add_vertex(display_p4, uv_p4, color); + + appender.add_index(current_vertex_index + 0, current_vertex_index + 1, current_vertex_index + 2); + appender.add_index(current_vertex_index + 0, current_vertex_index + 2, current_vertex_index + 3); + + if (new_texture) + { + render_list_context.pop_texture(); + } + } + + auto RenderList::image( + const texture_id_type texture_id, + const rect_type& display_rect, + const rect_type& uv_rect, + const color_type color + ) noexcept -> void + { + this->image( + texture_id, + display_rect.left_top(), + display_rect.right_top(), + display_rect.right_bottom(), + display_rect.left_bottom(), + uv_rect.left_top(), + uv_rect.right_top(), + uv_rect.right_bottom(), + uv_rect.left_bottom(), + color + ); + } + + auto RenderList::image( + const texture_id_type texture_id, + const rect_type::point_type& display_left_top, + const rect_type::extent_type& display_size, + const uv_type& uv_left_top, + const uv_type& uv_right_bottom, + const color_type color + ) noexcept -> void + { + this->image(texture_id, {display_left_top, display_size}, {uv_left_top, uv_right_bottom}, color); + } + + auto RenderList::image( + const texture_id_type texture_id, + const rect_type::point_type& display_left_top, + const rect_type::point_type& display_right_bottom, + const uv_type& uv_left_top, + const uv_type& uv_right_bottom, + const color_type color + ) noexcept -> void + { + this->image(texture_id, {display_left_top, display_right_bottom}, {uv_left_top, uv_right_bottom}, color); + } + + auto RenderList::image_rounded( + const texture_id_type texture_id, + const rect_type& display_rect, + float rounding, + RenderRectFlag flag, + const rect_type& uv_rect, + const color_type color + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + // @see `path_rect` + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(display_rect.valid() and not display_rect.empty()); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(uv_rect.valid() and not uv_rect.empty()); + + if (rounding >= .5f) + { + flag = to_fixed_rect_corner_flag(flag); + + const auto v = (flag & RenderRectFlag::ROUND_CORNER_TOP) == RenderRectFlag::ROUND_CORNER_TOP or (flag & RenderRectFlag::ROUND_CORNER_BOTTOM) == RenderRectFlag::ROUND_CORNER_BOTTOM; + const auto h = (flag & RenderRectFlag::ROUND_CORNER_LEFT) == RenderRectFlag::ROUND_CORNER_LEFT or (flag & RenderRectFlag::ROUND_CORNER_RIGHT) == RenderRectFlag::ROUND_CORNER_RIGHT; + + rounding = std::ranges::min(rounding, display_rect.width() * (v ? .5f : 1.f) - 1.f); + rounding = std::ranges::min(rounding, display_rect.height() * (h ? .5f : 1.f) - 1.f); + } + + if (rounding < .5f or (RenderRectFlag::ROUND_CORNER_MASK & flag) == RenderRectFlag::ROUND_CORNER_NONE) + { + this->image(texture_id, display_rect, uv_rect, color); + } + else + { + auto& render_list_context = *context_; + + const auto new_texture = render_list_context.command_header.texture != texture_id; + + if (new_texture) + { + render_list_context.push_texture(texture_id); + } + + const auto rounding_left_top = (flag & RenderRectFlag::ROUND_CORNER_LEFT_TOP) != RenderRectFlag::NONE ? rounding : 0; + const auto rounding_right_top = (flag & RenderRectFlag::ROUND_CORNER_RIGHT_TOP) != RenderRectFlag::NONE ? rounding : 0; + const auto rounding_left_bottom = (flag & RenderRectFlag::ROUND_CORNER_LEFT_BOTTOM) != RenderRectFlag::NONE ? rounding : 0; + const auto rounding_right_bottom = (flag & RenderRectFlag::ROUND_CORNER_RIGHT_BOTTOM) != RenderRectFlag::NONE ? rounding : 0; + + const auto before_vertex_count = render_list_context.vertex_list.size(); + + // draw + painter() + .arc_fast({display_rect.left_top() + point_type{rounding_left_top, rounding_left_top}, rounding_left_top}, RenderArcFlag::Q2_CLOCK_WISH) + .arc_fast({display_rect.right_top() + point_type{-rounding_right_top, rounding_right_top}, rounding_right_top}, RenderArcFlag::Q1_CLOCK_WISH) + .arc_fast({display_rect.right_bottom() + point_type{-rounding_right_bottom, -rounding_right_bottom}, rounding_right_bottom}, RenderArcFlag::Q4_CLOCK_WISH) + .arc_fast({display_rect.left_bottom() + point_type{rounding_left_bottom, -rounding_left_bottom}, rounding_left_bottom}, RenderArcFlag::Q3_CLOCK_WISH) + .stroke(color); + + const auto after_vertex_count = render_list_context.vertex_list.size(); + + // set uv manually + + const auto display_size = display_rect.size(); + const auto uv_size = uv_rect.size(); + const auto scale = uv_size / display_size; + + auto it = render_list_context.vertex_list.begin() + static_cast(before_vertex_count); + const auto end = render_list_context.vertex_list.begin() + static_cast(after_vertex_count); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(it < end); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(end == render_list_context.vertex_list.end()); + + // FIXME-OPT: linear uv + const auto uv_min = uv_rect.left_top(); + // const auto uv_max = uv_rect.right_bottom(); + while (it != end) + { + const auto v = uv_min + (it->position - display_rect.left_top()) * scale; + + it->uv = { + // std::ranges::clamp(v.x, uv_min.x, uv_max.x), + v.x, + // std::ranges::clamp(v.y, uv_min.y, uv_max.y) + v.y + }; + it += 1; + } + + if (new_texture) + { + render_list_context.pop_texture(); + } + } + } + + auto RenderList::image_rounded( + const texture_id_type texture_id, + const rect_type::point_type& display_left_top, + const rect_type::extent_type& display_size, + const float rounding, + const RenderRectFlag flag, + const uv_type& uv_left_top, + const uv_type& uv_right_bottom, + const color_type color + ) noexcept -> void + { + this->image_rounded(texture_id, {display_left_top, display_size}, rounding, flag, {uv_left_top, uv_right_bottom}, color); + } + + auto RenderList::image_rounded( + const texture_id_type texture_id, + const rect_type::point_type& display_left_top, + const rect_type::point_type& display_right_bottom, + const float rounding, + const RenderRectFlag flag, + const uv_type& uv_left_top, + const uv_type& uv_right_bottom, + const color_type color + ) noexcept -> void + { + this->image_rounded(texture_id, {display_left_top, display_right_bottom}, rounding, flag, {uv_left_top, uv_right_bottom}, color); + } +} + +namespace +{ + RenderDataAppender::RenderDataAppender(RenderList::RenderListContext& context) noexcept + : RenderDataAppender{context.command_list.back(), context.vertex_list, context.index_list} {} +}; diff --git a/src/gfx/render_list.hpp b/src/gfx/render_list.hpp new file mode 100644 index 00000000..f4c40c9a --- /dev/null +++ b/src/gfx/render_list.hpp @@ -0,0 +1,693 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace gal::prometheus::gfx +{ + enum class RenderListFlag : std::uint8_t + { + NONE = 0, + ANTI_ALIASED_LINE = 1 << 0, + ANTI_ALIASED_LINE_USE_TEXTURE = 1 << 1, + ANTI_ALIASED_FILL = 1 << 2, + + DEFAULT = ANTI_ALIASED_LINE | ANTI_ALIASED_LINE_USE_TEXTURE | ANTI_ALIASED_FILL, + }; + + enum class RenderRectFlag : std::uint8_t + { + NONE = 0, + // enable rounding left-top corner only (when rounding > 0.0f, we default to all corners) + // @see RenderList::path_rect + // @see RenderList::rect + // @see RenderList::rect_filled + ROUND_CORNER_LEFT_TOP = 1 << 1, + // enable rounding right_top corner only (when rounding > 0.0f, we default to all corners) + // @see RenderList::path_rect + // @see RenderList::rect + // @see RenderList::rect_filled + ROUND_CORNER_RIGHT_TOP = 1 << 2, + // enable rounding left-bottom corner only (when rounding > 0.0f, we default to all corners) + // @see RenderList::path_rect + // @see RenderList::rect + // @see RenderList::rect_filled + ROUND_CORNER_LEFT_BOTTOM = 1 << 3, + // enable rounding right-bottom corner only (when rounding > 0.0f, we default to all corners) + // @see RenderList::path_rect + // @see RenderList::rect + // @see RenderList::rect_filled + ROUND_CORNER_RIGHT_BOTTOM = 1 << 4, + // disable rounding on all corners (when rounding > 0.0f) + ROUND_CORNER_NONE = 1 << 5, + + ROUND_CORNER_LEFT = ROUND_CORNER_LEFT_TOP | ROUND_CORNER_LEFT_BOTTOM, + ROUND_CORNER_TOP = ROUND_CORNER_LEFT_TOP | ROUND_CORNER_RIGHT_TOP, + ROUND_CORNER_RIGHT = ROUND_CORNER_RIGHT_TOP | ROUND_CORNER_RIGHT_BOTTOM, + ROUND_CORNER_BOTTOM = ROUND_CORNER_LEFT_BOTTOM | ROUND_CORNER_RIGHT_BOTTOM, + + ROUND_CORNER_ALL = ROUND_CORNER_LEFT_TOP | ROUND_CORNER_RIGHT_TOP | ROUND_CORNER_LEFT_BOTTOM | ROUND_CORNER_RIGHT_BOTTOM, + ROUND_CORNER_DEFAULT = ROUND_CORNER_ALL, + ROUND_CORNER_MASK = ROUND_CORNER_ALL | ROUND_CORNER_NONE, + }; + + enum class RenderArcFlag : std::uint8_t + { + // [0~3) + Q1 = 1 << 0, + // [3~6) + Q2 = 1 << 1, + // [6~9) + Q3 = 1 << 2, + // [9~12) + Q4 = 1 << 3, + + RIGHT_TOP = Q1, + LEFT_TOP = Q2, + LEFT_BOTTOM = Q3, + RIGHT_BOTTOM = Q4, + TOP = Q1 | Q2, + BOTTOM = Q3 | Q4, + LEFT = Q2 | Q3, + RIGHT = Q1 | Q4, + ALL = Q1 | Q2 | Q3 | Q4, + + // [3, 0) + Q1_CLOCK_WISH = 1 << 4, + // [6, 3) + Q2_CLOCK_WISH = 1 << 5, + // [9, 6) + Q3_CLOCK_WISH = 1 << 6, + // [12, 9) + Q4_CLOCK_WISH = 1 << 7, + + RIGHT_TOP_CLOCK_WISH = Q1_CLOCK_WISH, + LEFT_TOP_CLOCK_WISH = Q2_CLOCK_WISH, + LEFT_BOTTOM_CLOCK_WISH = Q3_CLOCK_WISH, + RIGHT_BOTTOM_CLOCK_WISH = Q4_CLOCK_WISH, + TOP_CLOCK_WISH = Q1_CLOCK_WISH | Q2_CLOCK_WISH, + BOTTOM_CLOCK_WISH = Q3_CLOCK_WISH | Q4_CLOCK_WISH, + LEFT_CLOCK_WISH = Q2_CLOCK_WISH | Q3_CLOCK_WISH, + RIGHT_CLOCK_WISH = Q1_CLOCK_WISH | Q4_CLOCK_WISH, + ALL_CLOCK_WISH = Q1_CLOCK_WISH | Q2_CLOCK_WISH | Q3_CLOCK_WISH | Q4_CLOCK_WISH, + }; + + class RenderListSharedData + { + public: + using circle_segment_count_type = std::uint8_t; + constexpr static std::size_t circle_segment_counts_count = 64; + using circle_segment_counts_type = std::array; + + constexpr static std::uint32_t circle_segments_min = 4; + constexpr static std::uint32_t circle_segments_max = 512; + + constexpr static std::size_t arc_sample_points_count = 48; + using arc_sample_points_type = std::array; + + constexpr static std::size_t baked_line_uv_count = 64; + using baked_line_uvs_type = std::array; + + // Precomputed segment count for given radius before we calculate it dynamically (to avoid calculation overhead) + circle_segment_counts_type circle_segment_counts; + // Maximum error (in pixels) allowed when using @c RenderList::circle and @c RenderList::circle_filled or drawing rounded corner rectangles with no explicit segment count specified. + // Decrease for higher quality but more geometry. + float circle_segment_max_error; + + // Sample points on the quarter of the circle + arc_sample_points_type arc_fast_sample_points; + // Cutoff radius after which arc drawing will fall back to slower @c RenderList::Painter::arc + float arc_fast_radius_cutoff; + + // Tessellation tolerance when using @c RenderList::Painter::bezier_curve without a specific number of segments. + // Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. + float curve_tessellation_tolerance; + + baked_line_uvs_type baked_line_uvs; + point_type white_pixel_uv; + + // Initial flags at the beginning of the frame (it is possible to alter flag on a per-RenderList basis afterward) + RenderListFlag render_list_initial_flag; + rect_type fullscreen_scissor; + + // -------------------------------------------------- + + RenderListSharedData() noexcept; + + // -------------------------------------------------- + + [[nodiscard]] auto circle_auto_segment_count(float radius) const noexcept -> circle_segment_count_type; + + [[nodiscard]] auto arc_sample_point(std::size_t index) const noexcept -> const point_type&; + + // -------------------------------------------------- + + auto set_circle_tessellation_max_error(float max_error) noexcept -> void; + + auto set_curve_tessellation_tolerance(float tolerance) noexcept -> void; + }; + + class RenderData final + { + public: + using size_type = std::uint32_t; + + struct [[nodiscard]] command_type + { + rect_type scissor; + texture_id_type texture; + + // ======================= + + // set by RenderList::index_list.size() + // start offset in @c RenderList::index_list + size_type index_offset; + // set by RenderList::draw_xxx + // number of indices (multiple of 3) to be rendered as triangles + size_type element_count; + }; + + using vertex_list_type = std::vector; + using index_list_type = std::vector; + using command_list_type = std::vector; + + std::reference_wrapper vertex_list; + std::reference_wrapper index_list; + std::reference_wrapper command_list; + }; + + using render_data_list_type = std::vector; + + class RenderList final + { + friend class Context; + friend class Renderer; + + public: + class RenderListContext; + + class Painter final + { + public: + using path_list_type = std::vector; + + private: + memory::RefWrapper render_list_; + path_list_type path_list_; + + auto draw_polygon_line(color_type color, float thickness, bool close) noexcept -> void; + auto draw_polygon_line_aa(color_type color, float thickness, bool close) noexcept -> void; + auto draw_convex_polygon_line_filled(color_type color) noexcept -> void; + auto draw_convex_polygon_line_filled_aa(color_type color) noexcept -> void; + + public: + Painter(const Painter&) noexcept = delete; + Painter(Painter&&) noexcept = default; + auto operator=(const Painter&) noexcept -> Painter& = delete; + auto operator=(Painter&&) noexcept -> Painter& = default; + + ~Painter() noexcept; + + explicit Painter(RenderList& render_list, std::size_t reserve_point) noexcept; + + auto clear() noexcept -> Painter&; + auto reserve_extra(std::size_t size) noexcept -> Painter&; + auto reserve(std::size_t size) noexcept -> Painter&; + + auto pin(const point_type& point) noexcept -> Painter&; + + auto line(const point_type& from, const point_type& to) noexcept -> Painter&; + auto triangle(const point_type& a, const point_type& b, const point_type& c) noexcept -> Painter&; + auto quadrilateral(const point_type& p1, const point_type& p2, const point_type& p3, const point_type& p4) noexcept -> Painter&; + auto rect(const rect_type& rect, float rounding, RenderRectFlag flag) noexcept -> Painter&; + auto rect(const rect_type::point_type& left_top, const rect_type::extent_type& extent, float rounding, RenderRectFlag flag) noexcept -> Painter&; + auto rect(const rect_type::point_type& left_top, const rect_type::point_type& right_bottom, float rounding, RenderRectFlag flag) noexcept -> Painter&; + auto circle_n(const circle_type& circle, std::uint32_t segments) noexcept -> Painter&; + auto circle_n(const circle_type::point_type& center, circle_type::radius_value_type radius, std::uint32_t segments) noexcept -> Painter&; + auto circle(const circle_type& circle) noexcept -> Painter&; + auto circle(const circle_type::point_type& center, circle_type::radius_value_type radius) noexcept -> Painter&; + auto ellipse_n(const ellipse_type& ellipse, std::uint32_t segments) noexcept -> Painter&; + auto ellipse_n(const ellipse_type::point_type& center, const ellipse_type::radius_type& radius, ellipse_type::rotation_value_type rotation, std::uint32_t segments) noexcept -> Painter&; + auto ellipse(const ellipse_type& ellipse) noexcept -> Painter&; + auto ellipse(const ellipse_type::point_type& center, const ellipse_type::radius_type& radius, ellipse_type::rotation_value_type rotation) noexcept -> Painter&; + auto arc_fast(const circle_type& circle, int sample_point_from, int sample_point_to) noexcept -> Painter&; + auto arc_fast(const circle_type& circle, RenderArcFlag flag) noexcept -> Painter&; + auto arc_n(const circle_type& circle, float degree_from, float degree_to, std::uint32_t segments) noexcept -> Painter&; + auto arc(const circle_type& circle, float degree_from, float degree_to) noexcept -> Painter&; + auto arc_n(const ellipse_type& ellipse, float degree_from, float degree_to, std::uint32_t segments) noexcept -> Painter&; + auto bezier_cubic_n(const point_type& p1, const point_type& p2, const point_type& p3, const point_type& p4, std::uint32_t segments) noexcept -> Painter&; + auto bezier_cubic(const point_type& p1, const point_type& p2, const point_type& p3, const point_type& p4) noexcept -> Painter&; + auto bezier_quadratic_n(const point_type& p1, const point_type& p2, const point_type& p3, std::uint32_t segments) noexcept -> Painter&; + auto bezier_quadratic(const point_type& p1, const point_type& p2, const point_type& p3) noexcept -> Painter&; + + /** + * @brief Draws the fill shape according to the specified path points + * @param color Shape color + */ + auto stroke(color_type color) noexcept -> void; + + /** + * @brief Plot the corresponding lines/shapes according to the specified path points + * @param color Line/shape color + * @param thickness Line thickness + * @param close Whether the graph is closed, in other words, whether the first point should be connected to the last point + */ + auto stroke(color_type color, float thickness, bool close) noexcept -> void; + }; + + private: + memory::UniquePointer context_; + + explicit RenderList(Context& context) noexcept; + + public: + RenderList(const RenderList&) noexcept = delete; + RenderList(RenderList&&) noexcept; //= default; + auto operator=(const RenderList&) noexcept -> RenderList& = delete; + auto operator=(RenderList&&) noexcept -> RenderList&; // = default; + + ~RenderList() noexcept; + + auto reset() noexcept -> void; + + auto set_flag(RenderListFlag flag) noexcept -> void; + + [[nodiscard]] auto data() const noexcept -> RenderData; + + // ---------------------------------------------------------------------------- + // SCISSOR & TEXTURE + + auto push_scissor(const rect_type& rect, bool intersect_with_current_scissor) noexcept -> const rect_type&; + + auto pop_scissor() noexcept -> void; + + auto push_texture(texture_id_type texture) noexcept -> void; + + auto pop_texture() noexcept -> void; + + // ---------------------------------------------------------------------------- + // PAINTER + + [[nodiscard]] auto painter(const Painter::path_list_type::size_type reserve_point = 0) noexcept -> Painter + { + return Painter{*this, reserve_point}; + } + + // ---------------------------------------------------------------------------- + // PRIMITIVE + + auto line( + const point_type& from, + const point_type& to, + color_type color, + float thickness = 1.f + ) noexcept -> void; + + auto triangle( + const point_type& a, + const point_type& b, + const point_type& c, + color_type color, + float thickness = 1.f + ) noexcept -> void; + + auto triangle_filled( + const point_type& a, + const point_type& b, + const point_type& c, + color_type color + ) noexcept -> void; + + auto quadrilateral( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const point_type& p4, + color_type color, + float thickness = 1.f + ) noexcept -> void; + + auto quadrilateral_filled( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const point_type& p4, + color_type color + ) noexcept -> void; + + auto rect( + const rect_type& rect, + color_type color, + float rounding = .0f, + RenderRectFlag flag = RenderRectFlag::ROUND_CORNER_ALL, + float thickness = 1.f + ) noexcept -> void; + + auto rect( + const point_type& left_top, + const rect_type::extent_type& extent, + color_type color, + float rounding = .0f, + RenderRectFlag flag = RenderRectFlag::ROUND_CORNER_ALL, + float thickness = 1.f + ) noexcept -> void; + + auto rect( + const rect_type::point_type& left_top, + const rect_type::point_type& right_bottom, + color_type color, + float rounding = .0f, + RenderRectFlag flag = RenderRectFlag::ROUND_CORNER_ALL, + float thickness = 1.f + ) noexcept -> void; + + auto rect_filled( + const rect_type& rect, + color_type color, + float rounding = .0f, + RenderRectFlag flag = RenderRectFlag::ROUND_CORNER_ALL + ) noexcept -> void; + + auto rect_filled( + const point_type& left_top, + const rect_type::extent_type& extent, + color_type color, + float rounding = .0f, + RenderRectFlag flag = RenderRectFlag::ROUND_CORNER_ALL + ) noexcept -> void; + + auto rect_filled( + const rect_type::point_type& left_top, + const rect_type::point_type& right_bottom, + color_type color, + float rounding = .0f, + RenderRectFlag flag = RenderRectFlag::ROUND_CORNER_ALL + ) noexcept -> void; + + auto rect_filled( + const rect_type& rect, + color_type color_left_top, + color_type color_right_top, + color_type color_left_bottom, + color_type color_right_bottom + ) noexcept -> void; + + auto rect_filled( + const point_type& left_top, + const rect_type::extent_type& extent, + color_type color_left_top, + color_type color_right_top, + color_type color_left_bottom, + color_type color_right_bottom + ) noexcept -> void; + + auto rect_filled( + const rect_type::point_type& left_top, + const rect_type::point_type& right_bottom, + color_type color_left_top, + color_type color_right_top, + color_type color_left_bottom, + color_type color_right_bottom + ) noexcept -> void; + + auto circle_n( + const circle_type& circle, + color_type color, + std::uint32_t segments, + float thickness = 1.f + ) noexcept -> void; + + auto circle_n( + const circle_type::point_type& center, + circle_type::radius_value_type radius, + color_type color, + std::uint32_t segments, + float thickness = 1.f + ) noexcept -> void; + + auto circle_n_filled( + const circle_type& circle, + color_type color, + std::uint32_t segments + ) noexcept -> void; + + auto circle_n_filled( + const circle_type::point_type& center, + circle_type::radius_value_type radius, + color_type color, + std::uint32_t segments + ) noexcept -> void; + + auto circle( + const circle_type& circle, + color_type color, + float thickness = 1.f + ) noexcept -> void; + + auto circle( + const circle_type::point_type& center, + circle_type::radius_value_type radius, + color_type color, + float thickness = 1.f + ) noexcept -> void; + + auto circle_filled( + const circle_type& circle, + color_type color + ) noexcept -> void; + + auto circle_filled( + const circle_type::point_type& center, + circle_type::radius_value_type radius, + color_type color + ) noexcept -> void; + + auto ellipse_n( + const ellipse_type& ellipse, + color_type color, + std::uint32_t segments, + float thickness = 1.f + ) noexcept -> void; + + auto ellipse_n( + const ellipse_type::point_type& center, + const ellipse_type::radius_type& radius, + ellipse_type::rotation_value_type rotation, + color_type color, + std::uint32_t segments, + float thickness = 1.f + ) noexcept -> void; + + auto ellipse_n_filled( + const ellipse_type& ellipse, + color_type color, + std::uint32_t segments + ) noexcept -> void; + + auto ellipse_n_filled( + const ellipse_type::point_type& center, + const ellipse_type::radius_type& radius, + ellipse_type::rotation_value_type rotation, + color_type color, + std::uint32_t segments + ) noexcept -> void; + + auto ellipse( + const ellipse_type& ellipse, + color_type color, + float thickness = 1.f + ) noexcept -> void; + + auto ellipse( + const ellipse_type::point_type& center, + const ellipse_type::radius_type& radius, + ellipse_type::rotation_value_type rotation, + color_type color, + float thickness = 1.f + ) noexcept -> void; + + auto ellipse_filled( + const ellipse_type& ellipse, + color_type color + ) noexcept -> void; + + auto ellipse_filled( + const ellipse_type::point_type& center, + const ellipse_type::radius_type& radius, + ellipse_type::rotation_value_type rotation, + color_type color + ) noexcept -> void; + + auto bezier_cubic_n( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const point_type& p4, + color_type color, + std::uint32_t segments, + float thickness = 1.f + ) noexcept -> void; + + auto bezier_cubic( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const point_type& p4, + color_type color, + float thickness = 1.f + ) noexcept -> void; + + auto bezier_quadratic_n( + const point_type& p1, + const point_type& p2, + const point_type& p3, + color_type color, + std::uint32_t segments, + float thickness = 1.f + ) noexcept -> void; + + auto bezier_quadratic( + const point_type& p1, + const point_type& p2, + const point_type& p3, + color_type color, + float thickness = 1.f + ) noexcept -> void; + + // ---------------------------------------------------------------------------- + // TEXT + + auto text( + std::string_view utf8_text, + std::uint32_t font_size, + const point_type& point, + color_type color, + float wrap_width = 99999999.f + ) noexcept -> void; + + auto text( + std::string_view utf8_text, + std::uint32_t font_size, + const point_type& point, + color_type color, + GlyphFlag flag, + float wrap_width = 99999999.f + ) noexcept -> void; + + // FIXME: There is no suitable place to implement this function, this function must be highly consistent with the implementation when drawing text + auto text_size( + std::string_view utf8_text, + std::uint32_t font_size, + float wrap_width = 99999999.f + ) noexcept -> extent_type; + + // FIXME: There is no suitable place to implement this function, this function must be highly consistent with the implementation when drawing text + auto text_size( + std::string_view utf8_text, + std::uint32_t font_size, + GlyphFlag flag, + float wrap_width = 99999999.f + ) noexcept -> extent_type; + + // ---------------------------------------------------------------------------- + // IMAGE + + // p1________ p2 + // | | + // | | + // p4|_______| p3 + auto image( + texture_id_type texture_id, + const rect_type::point_type& display_p1, + const rect_type::point_type& display_p2, + const rect_type::point_type& display_p3, + const rect_type::point_type& display_p4, + const uv_type& uv_p1 = {0, 0}, + const uv_type& uv_p2 = {1, 0}, + const uv_type& uv_p3 = {1, 1}, + const uv_type& uv_p4 = {0, 1}, + color_type color = primitive::colors::white + ) noexcept -> void; + + auto image( + texture_id_type texture_id, + const rect_type& display_rect, + const rect_type& uv_rect = {0, 0, 1, 1}, + color_type color = primitive::colors::white + ) noexcept -> void; + + auto image( + texture_id_type texture_id, + const rect_type::point_type& display_left_top, + const rect_type::extent_type& display_size, + const uv_type& uv_left_top = {0, 0}, + const uv_type& uv_right_bottom = {1, 1}, + color_type color = primitive::colors::white + ) noexcept -> void; + + auto image( + texture_id_type texture_id, + const rect_type::point_type& display_left_top, + const rect_type::point_type& display_right_bottom, + const uv_type& uv_left_top = {0, 0}, + const uv_type& uv_right_bottom = {1, 1}, + color_type color = primitive::colors::white + ) noexcept -> void; + + auto image_rounded( + texture_id_type texture_id, + const rect_type& display_rect, + float rounding = .0f, + RenderRectFlag flag = RenderRectFlag::NONE, + const rect_type& uv_rect = {0, 0, 1, 1}, + color_type color = primitive::colors::white + ) noexcept -> void; + + auto image_rounded( + texture_id_type texture_id, + const rect_type::point_type& display_left_top, + const rect_type::extent_type& display_size, + float rounding = .0f, + RenderRectFlag flag = RenderRectFlag::NONE, + const uv_type& uv_left_top = {0, 0}, + const uv_type& uv_right_bottom = {1, 1}, + color_type color = primitive::colors::white + ) noexcept -> void; + + auto image_rounded( + texture_id_type texture_id, + const rect_type::point_type& display_left_top, + const rect_type::point_type& display_right_bottom, + float rounding = .0f, + RenderRectFlag flag = RenderRectFlag::NONE, + const uv_type& uv_left_top = {0, 0}, + const uv_type& uv_right_bottom = {1, 1}, + color_type color = primitive::colors::white + ) noexcept -> void; + }; +} + +namespace gal::prometheus::meta::user_defined +{ + template<> + struct enum_is_flag : std::true_type {}; + + template<> + struct enum_is_flag : std::true_type {}; + + template<> + struct enum_is_flag : std::true_type {}; +} // namespace gal::prometheus::meta::user_defined diff --git a/src/gfx/renderer.cpp b/src/gfx/renderer.cpp new file mode 100644 index 00000000..4a6da285 --- /dev/null +++ b/src/gfx/renderer.cpp @@ -0,0 +1,13 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +namespace gal::prometheus::gfx +{ + Renderer::~Renderer() noexcept = default; + + Renderer::Renderer() noexcept = default; +} diff --git a/src/gfx/renderer.hpp b/src/gfx/renderer.hpp new file mode 100644 index 00000000..8afd3398 --- /dev/null +++ b/src/gfx/renderer.hpp @@ -0,0 +1,41 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include + +#include + +#include +#include + +namespace gal::prometheus::gfx +{ + class Renderer + { + public: + Renderer(const Renderer&) noexcept = delete; + Renderer(Renderer&&) noexcept = default; + auto operator=(const Renderer&) noexcept -> Renderer& = delete; + auto operator=(Renderer&&) noexcept -> Renderer& = default; + + virtual ~Renderer() noexcept; + + protected: + Renderer() noexcept; + + public: + [[nodiscard]] virtual auto create_texture(const Texture::data_type& data, Texture::size_type size) noexcept -> texture_id_type = 0; + virtual auto update_texture(texture_id_type id, std::span update_viewer) noexcept -> void = 0; + virtual auto destroy_texture(texture_id_type id) noexcept -> void = 0; + + /** + * @brief + * @note @c new_frame -> @c present -> @c end_frame + */ + virtual auto present(const render_data_list_type& render_data_list, const extent_type& display_size) noexcept -> void = 0; + }; +} diff --git a/src/gfx/texture.cpp b/src/gfx/texture.cpp new file mode 100644 index 00000000..78ec189f --- /dev/null +++ b/src/gfx/texture.cpp @@ -0,0 +1,186 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE + +namespace gal::prometheus::gfx +{ + TextureWriter::TextureWriter(const point_type point, const borrow_data_type& data) noexcept + : point_{point}, + data_{data} {} + + auto TextureWriter::valid() const noexcept -> bool + { + return point_ != invalid_point; + } + + auto TextureWriter::position() const noexcept -> point_type + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(valid()); + + return point_; + } + + auto TextureWriter::fill(const size_type::value_type y, const size_type::value_type offset, const size_type::value_type n, const element_type element) const noexcept -> void + { + const auto width = data_.extent(1); + const auto height = data_.extent(0); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(valid()); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(y < height); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(offset + n <= width); + + for (size_type::value_type x = offset; x < offset + n; ++x) + { + data_[y, x] = element; + } + } + + auto TextureWriter::fill(const size_type::value_type y, const size_type::value_type offset, const data_view_type data) const noexcept -> void + { + const auto width = data_.extent(1); + const auto height = data_.extent(0); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(valid()); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(y < height); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(offset + data.size() <= width); + + for (size_type::value_type x = 0; x < data.size(); ++x) + { + data_[y, x + offset] = data[x]; + } + } + + auto TextureWriter::fill(const size_type::value_type y, const size_type::value_type n, const element_type element) const noexcept -> void + { + const auto width = data_.extent(1); + const auto height = data_.extent(0); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(valid()); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(y < height); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(n <= width); + + fill(y, 0, n, element); + } + + auto TextureWriter::fill(const size_type::value_type y, const data_view_type data) const noexcept -> void + { + const auto width = data_.extent(1); + const auto height = data_.extent(0); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(valid()); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(y < height); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(data.size() <= width); + + fill(y, 0, data); + } + + auto TextureWriter::fill(const size_type::value_type y, const element_type element) const noexcept -> void + { + const auto width = data_.extent(1); + const auto height = data_.extent(0); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(valid()); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(y < height); + + fill(y, width, element); + } + + auto TextureWriter::fill(const element_type element) const noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(valid()); + + const auto height = data_.extent(0); + for (size_type::value_type y = 0; y < height; ++y) + { + fill(y, element); + } + } + + auto TextureWriter::fill(const data_view_type data) const noexcept -> void + { + const auto width = data_.extent(1); + const auto height = data_.extent(0); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(valid()); + + for (size_type::value_type y = 0; y < height; ++y) + { + const data_view_type sub{data.begin() + static_cast(y) * width, width}; + + fill(y, sub); + } + } + + auto TextureWriter::operator[](const size_type::value_type x, const size_type::value_type y) const noexcept -> borrow_data_type::reference + { + const auto width = data_.extent(1); + const auto height = data_.extent(0); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(valid()); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(y < height); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(x < width); + + return data_[y, x]; + } + + TextureViewer::TextureViewer(const point_type point, const borrow_data_type& data) noexcept + : point_{point}, + data_{data} {} + + auto TextureViewer::position() const noexcept -> point_type + { + return point_; + } + + auto TextureViewer::size() const noexcept -> size_type + { + const auto width = data_.extent(1); + const auto height = data_.extent(0); + + return {width, height}; + } + + auto TextureViewer::line(const size_type::value_type y, const size_type::value_type offset, const size_type::value_type n) const noexcept -> data_view_type + { + const auto width = data_.extent(1); + const auto height = data_.extent(0); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(y < height); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(offset + n <= width); + + // TODO: submdspan(C++ 26) + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(data_.stride(1) == 1); + const auto& this_line = data_[y, 0]; + + return {&this_line, n}; + } + + auto TextureViewer::line(const size_type::value_type y, const size_type::value_type n) const noexcept -> data_view_type + { + return line(y, 0, n); + } + + auto TextureViewer::line(const size_type::value_type y) const noexcept -> data_view_type + { + const auto width = data_.extent(1); + // const auto height = data_.extent(0); + + return line(y, width); + } + + auto TextureViewer::operator[](const size_type::value_type x, const size_type::value_type y) const noexcept -> borrow_data_type::reference + { + const auto width = data_.extent(1); + const auto height = data_.extent(0); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(y < height); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(x < width); + + return data_[y, x]; + } +} diff --git a/src/gfx/texture.hpp b/src/gfx/texture.hpp new file mode 100644 index 00000000..182b9428 --- /dev/null +++ b/src/gfx/texture.hpp @@ -0,0 +1,132 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include +#include +#include + +#include + +namespace gal::prometheus::gfx +{ + class Texture final + { + public: + // 32-bits (RGBA) + using element_type = std::uint32_t; + // size.width * size.height + using data_type = std::unique_ptr; + using data_view_type = std::span; + + // RectPackContext::point_type + using point_type = primitive::basic_point_2d; + // RectPackContext::size_type + using size_type = primitive::basic_extent_2d; + + using uv_scale_type = primitive::basic_extent_2d; + + // ============================== + // CPU side + // ============================== + + data_type data; + size_type size; + + // It's much more cost-effective to keep a member variable than to compute it every time + uv_scale_type uv_scale; + + // ============================== + // GPU side + // ============================== + + texture_id_type id; + }; + + class TextureContext; + class TextureWriter; + class TextureViewer; + + class TextureWriter final + { + friend TextureContext; + friend TextureViewer; + + public: + using element_type = Texture::element_type; + using data_type = Texture::data_type; + using data_view_type = Texture::data_view_type; + + using point_type = Texture::point_type; + using size_type = Texture::size_type; + + using uv_scale_type = Texture::uv_scale_type; + + constexpr static point_type invalid_point{(std::numeric_limits::max)(), (std::numeric_limits::max)()}; + + private: + using borrow_data_type = std::mdspan, std::layout_stride>; + + point_type point_; + borrow_data_type data_; + + TextureWriter(point_type point, const borrow_data_type& data) noexcept; + + public: + [[nodiscard]] auto valid() const noexcept -> bool; + + [[nodiscard]] auto position() const noexcept -> point_type; + + auto fill(size_type::value_type y, size_type::value_type offset, size_type::value_type n, element_type element) const noexcept -> void; + auto fill(size_type::value_type y, size_type::value_type offset, data_view_type data) const noexcept -> void; + + auto fill(size_type::value_type y, size_type::value_type n, element_type element) const noexcept -> void; + auto fill(size_type::value_type y, data_view_type data) const noexcept -> void; + + auto fill(size_type::value_type y, element_type element) const noexcept -> void; + + auto fill(element_type element) const noexcept -> void; + auto fill(data_view_type data) const noexcept -> void; + + auto operator[](size_type::value_type x, size_type::value_type y) const noexcept -> borrow_data_type::reference; + }; + + class TextureViewer final + { + friend TextureContext; + + public: + using element_type = Texture::element_type; + using data_type = Texture::data_type; + using data_view_type = Texture::data_view_type; + + using point_type = Texture::point_type; + using size_type = Texture::size_type; + + using uv_scale_type = Texture::uv_scale_type; + + private: + using borrow_data_type = std::mdspan, std::layout_stride>; + + point_type point_; + borrow_data_type data_; + + TextureViewer(point_type point, const borrow_data_type& data) noexcept; + + public: + [[nodiscard]] auto position() const noexcept -> point_type; + + [[nodiscard]] auto size() const noexcept -> size_type; + + [[nodiscard]] auto line(size_type::value_type y, size_type::value_type offset, size_type::value_type n) const noexcept -> data_view_type; + + [[nodiscard]] auto line(size_type::value_type y, size_type::value_type n) const noexcept -> data_view_type; + + [[nodiscard]] auto line(size_type::value_type y) const noexcept -> data_view_type; + + auto operator[](size_type::value_type x, size_type::value_type y) const noexcept -> borrow_data_type::reference; + }; +} diff --git a/src/gfx/type.hpp b/src/gfx/type.hpp new file mode 100644 index 00000000..a8fbc68e --- /dev/null +++ b/src/gfx/type.hpp @@ -0,0 +1,41 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace gal::prometheus::gfx +{ + // ========================================================= + // PRIMITIVE + // ========================================================= + + using point_type = primitive::basic_point_2d; + using uv_type = primitive::basic_point_2d; + using color_type = primitive::basic_color; + using vertex_type = primitive::basic_vertex; + using index_type = std::uint16_t; + + using extent_type = primitive::basic_extent_2d; + using rect_type = primitive::basic_rect_2d; + using circle_type = primitive::basic_circle_2d; + using ellipse_type = primitive::basic_ellipse_2d; + + // ========================================================= + // TEXTURE + // ========================================================= + + // D3D11: ID3D11ShaderResourceView + // D3D12: D3D12_GPU_DESCRIPTOR_HANDLE::ptr / HEAP index + using texture_id_type = std::uintptr_t; + constexpr texture_id_type invalid_texture_id{std::numeric_limits::max()}; +} diff --git a/src/gui/gui.hpp b/src/gui/gui.hpp new file mode 100644 index 00000000..5b459805 --- /dev/null +++ b/src/gui/gui.hpp @@ -0,0 +1,869 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include +#include +#include + +#include +#include +#include + +#include +#include + +namespace gal::prometheus +{ + // ================================================== + // TYPE & FLAG + // ================================================== + + namespace gui + { + using rect_type = primitive::basic_rect_2d; + using point_type = rect_type::point_type; + using extent_type = rect_type::extent_type; + + using uv_type = primitive::basic_point_2d; + using color_type = primitive::basic_color; + + using vertex_type = primitive::basic_vertex; + using index_type = std::uint16_t; + + using texture_id_type = std::uintptr_t; + using time_type = float; + + //------------------------------------------------------------------ + // FONT + //------------------------------------------------------------------ + + class FontOption + { + public: + using value_type = extent_type::value_type; + // todo: char32_t ? + using char_type = char16_t; + + using glyph_value_type = i18n::RangeBuilder::value_type; + using glyph_ranges_type = i18n::RangeBuilder::ranges_type; + + // todo + constexpr static std::uint32_t default_baked_line_max_width = 63; + + std::string font_path{}; + glyph_ranges_type glyph_ranges{}; + std::uint32_t pixel_height{}; + + std::uint32_t baked_line_max_width{default_baked_line_max_width}; + value_type scale{1.f}; + char_type fallback_char{u'?'}; + extent_type display_offset{.0f, .0f}; + point_type white_pixel_uv{-1, -1}; + }; + + class [[nodiscard]] Texture final + { + public: + using size_type = std::uint32_t; + // size.width * size.height (RGBA) + using data_type = std::unique_ptr; + + size_type width; + size_type height; + data_type data; + std::reference_wrapper id; + + explicit Texture(texture_id_type& texture_id) noexcept; + + Texture(const Texture& other) = delete; + Texture(Texture&& other) noexcept = default; + auto operator=(const Texture& other) -> Texture& = delete; + auto operator=(Texture&& other) noexcept -> Texture& = default; + + ~Texture() noexcept; + + [[nodiscard]] auto valid() const noexcept -> bool; + + auto bind(texture_id_type texture_id) noexcept -> void; + }; + + //------------------------------------------------------------------ + // THEME + //------------------------------------------------------------------ + + enum class ThemeCategory : std::uint8_t + { + TEXT = 0, + + BORDER, + BORDER_SHADOW, + + WINDOW_BACKGROUND, + + TITLEBAR, + TITLEBAR_COLLAPSED, + + RESIZE_GRIP, + RESIZE_GRIP_HOVERED, + RESIZE_GRIP_ACTIVATED, + + SCROLLBAR_BACKGROUND, + SCROLLBAR_GRAB, + SCROLLBAR_GRAB_HOVERED, + SCROLLBAR_GRAB_ACTIVATED, + + CLOSE_BUTTON, + CLOSE_BUTTON_HOVERED, + CLOSE_BUTTON_ACTIVATED, + + TOOLTIP_BACKGROUND, + TOOLTIP_TEXT, + + BUTTON, + BUTTON_HOVERED, + BUTTON_ACTIVATED, + + // RADIO-BUTTON / CHECKBOX / SLIDER + FRAME_BACKGROUND, + + RADIO_BUTTON_HOVERED, + RADIO_BUTTON_ACTIVATED, + + CHECKBOX_HOVERED, + CHECKBOX_ACTIVATED, + + SLIDER, + SLIDER_ACTIVATED, + + COMBO_ITEM, + COMBO_ITEM_HOVERED, + COMBO_ITEM_ACTIVATED, + + // ------------------------------- + INTERNAL_COUNT + }; + + constexpr auto theme_category_count = static_cast(ThemeCategory::INTERNAL_COUNT); + + class Theme final + { + public: + using value_type = extent_type::value_type; + + using color_type = primitive::basic_color; + using alpha_type = value_type; + + using colors_type = std::array; + + // < 0 + constexpr static alpha_type window_background_fill_alpha_not_set{-.99999f}; + + //----------------- + // WINDOW + //----------------- + + // Default alpha of window background + value_type window_background_alpha; + // Rounding of the window corners + value_type window_corner_rounding; + // Minimum size of the window + extent_type window_min_size; + // Size of the window resize grip + extent_type window_resize_grip_size; + // width: Minimum distance of the first element of each line from the left border of the window + // height: Minimum distance of all elements of the first line from the upper border of the window (excluding the title bar) + extent_type window_padding; + // The extra padding space after adapting the window to the contents of the window (i.e., shrinking the window to just fit all the contents) + extent_type window_auto_fit_padding; + // Width of the vertical scrollbar + value_type window_vertical_scrollbar_width; + + //----------------- + // WINDOW CANVAS LAYOUT + //----------------- + + // The factor of the line (window width) that an element takes up by default + value_type item_default_width_factor; + // Extra padding size for elements with borders (e.g. horizontal and vertical fill for text in button boxes) + extent_type item_frame_padding; + // Filling distance between two different elements when they are on the `same line` + extent_type item_spacing; + // If an element consists of more than one sub-element, the distance between these sub-elements is filled + extent_type item_inner_spacing; + + //----------------- + // WINDOW WIDGET COLOR + //----------------- + + // Default alpha of all widget + alpha_type alpha; + colors_type colors; + + //----------------- + // DRAW + //----------------- + + // Maximum error (in pixels) allowed when using @c DrawList::circle and @c DrawList::circle_filled or drawing rounded corner rectangles with no explicit segment count specified + // Decrease for higher quality but more geometry + value_type circle_segment_max_error; + // Tessellation tolerance when using @c DrawList::path_bezier_curve without a specific number of segments + // Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality + value_type draw_curve_tessellation_tolerance; + }; + + //------------------------------------------------------------------ + // IO + //------------------------------------------------------------------ + + enum class MouseKey : std::uint8_t + { + LEFT = 0, + MIDDLE, + RIGHT, + X1, + X2, + + // ------------------------------- + INTERNAL_COUNT + }; + + constexpr auto mouse_key_count = static_cast(MouseKey::INTERNAL_COUNT); + + // ReSharper disable once CppInconsistentNaming + class IO final + { + public: + using value_type = extent_type::value_type; + + // Update once (it can also be updated every frame basis if desired) + // Current window (viewport) size + extent_type display_size{0, 0}; + // Update every frame + // Time elapsed since last frame, in seconds + // FPS = 60 ==> time_delta = 1/60 + time_type delta_time{1.f / 60.f}; + + //----------------- + // MOUSE + //----------------- + + struct mouse_button_state_type + { + std::array state{}; + + [[nodiscard]] constexpr auto operator[](const MouseKey key) noexcept -> bool& + { + return state[static_cast(key)]; + } + + [[nodiscard]] constexpr auto operator[](const MouseKey key) const noexcept -> bool + { + return state[static_cast(key)]; + } + }; + + // Update every frame + // Current mouse position (this frame) + point_type mouse_position{0, 0}; + // Update every frame + // Current mouse wheel value + value_type mouse_wheel{0}; + // Update every frame + // Current mouse button state (this frame) + mouse_button_state_type mouse_button_state{}; + + // Update once (it can also be updated every frame basis if desired) + // The interval between two clicks is less than this threshold to be considered as a double click, in seconds + time_type mouse_double_click_interval_threshold{.3f}; + // Update once (it can also be updated every frame basis if desired) + // The mouse position delta between two clicks is less than this threshold to be considered as a double click + value_type mouse_double_click_distance_threshold{6}; + // Update once (it can also be updated every frame basis if desired) + // When holding a button, time before it starts repeating, in seconds + time_type mouse_repeat_click_delay{.275f}; + // Update once (it can also be updated every frame basis if desired) + // When holding a button, rate at which it repeats, in seconds + time_type mouse_repeat_click_rate{.05f}; + + //----------------- + // KEYBOARD + //----------------- + }; + + //------------------------------------------------------------------ + // DRAW + //------------------------------------------------------------------ + + enum class DrawListFlag : std::uint8_t + { + NONE = 0, + ANTI_ALIASED_LINE = 1 << 0, + ANTI_ALIASED_LINE_USE_TEXTURE = 1 << 1, + ANTI_ALIASED_FILL = 1 << 2, + }; + + class DrawData final + { + public: + using size_type = std::uint32_t; + + struct [[nodiscard]] command_type + { + rect_type clip_rect; + texture_id_type texture_id; + + // ======================= + + // set by DrawList::index_list.size() + // start offset in @c DrawList::index_list + size_type index_offset; + // set by DrawList::draw_xxx + // number of indices (multiple of 3) to be rendered as triangles + size_type element_count; + }; + + using vertex_list_type = std::vector; + using index_list_type = std::vector; + using command_list_type = std::vector; + + std::reference_wrapper vertex_list; + std::reference_wrapper index_list; + std::reference_wrapper command_list; + }; + + //------------------------------------------------------------------ + // WIDGET + //------------------------------------------------------------------ + + enum class WindowFlag : std::uint16_t + { + NONE = 0, + + BORDERED = 1 << 0, + + NO_TITLEBAR = 1 << 1, + NO_CLOSE = 1 << 2, + NO_RESIZE = 1 << 3, + NO_MOVE = 1 << 4, + NO_SCROLLBAR = 1 << 5, + NO_SCROLLBAR_WITH_MOUSE = 1 << 6, + + AUTO_RESIZE = 1 << 7, + }; + } + + namespace meta::user_defined + { + template<> + struct enum_is_flag : std::true_type {}; + + template<> + struct enum_is_flag : std::true_type {}; + } + + // ================================================== + // CONTEXT + API + // ================================================== + + namespace gui + { + //------------------------------------------------------------------ + // CONTEXT + //------------------------------------------------------------------ + + class Context; + + // Create context + [[nodiscard]] auto create_context() noexcept -> Context*; + // Destroy context + auto destroy_context(Context& context) noexcept -> void; + // Destroy context, for smart pointer + auto destroy_context(Context* context) noexcept -> void; + + //------------------------------------------------------------------ + // FONT + //------------------------------------------------------------------ + + [[nodiscard]] auto set_default_font(Context& context, const FontOption& option) noexcept -> Texture; + + // [[nodiscard]] auto push_font(Context& context, const FontOption& option) noexcept -> Texture; + // auto pop_font(Context& context) noexcept -> void; + + //------------------------------------------------------------------ + // THEME + //------------------------------------------------------------------ + + auto set_default_theme(Context& context, const Theme& theme) noexcept -> void; + + auto push_theme(Context& context, ThemeCategory category, Theme::color_type new_color) noexcept -> void; + auto pop_theme(Context& context) noexcept -> void; + + //------------------------------------------------------------------ + // IO + //------------------------------------------------------------------ + + [[nodiscard]] auto get_io(Context& context) noexcept -> IO&; + + //------------------------------------------------------------------ + // DRAW + //------------------------------------------------------------------ + + auto set_default_draw_list_flag(Context& context, DrawListFlag flag) noexcept -> void; + + // auto push_draw_list_flag(Context& context, DrawListFlag new_flag) noexcept -> void; + // auto pop_draw_list_flag(Context& context) noexcept -> void; + + auto new_frame(Context& context) noexcept -> void; + auto end_frame(Context& context) noexcept -> void; + auto render(Context& context) noexcept -> void; + + [[nodiscard]] auto get_draw_data(Context& context) noexcept -> std::vector; + + //------------------------------------------------------------------ + // WIDGET + //------------------------------------------------------------------ + + auto set_next_window_point(Context& context, const point_type& point) noexcept -> void; + + auto begin_window( + Context& context, + std::string_view name, + const extent_type& size = {0, 0}, + Theme::alpha_type fill_alpha = Theme::window_background_fill_alpha_not_set, + WindowFlag flag = WindowFlag::NONE + ) noexcept -> bool; + auto end_window(Context& context) noexcept -> void; + + auto begin_child_window( + Context& context, + std::string_view name, + const extent_type& size = {0, 0}, + bool border = false, + WindowFlag flag = WindowFlag::NONE + ) noexcept -> void; + auto end_child_window(Context& context) noexcept -> void; + + auto begin_tooltip_window(Context& context) noexcept -> void; + auto end_tooltip_window(Context& context) noexcept -> void; + + /** + * @brief Draw a piece of text + */ + auto draw_text(Context& context, std::string_view utf8_text) noexcept -> void; + + /** + * @brief Draw a piece of text with specified color + */ + auto draw_text_colored(Context& context, std::string_view utf8_text, Theme::color_type color) noexcept -> void; + + /** + * @brief Draw a button, if the specified size is smaller than the size occupied by the text (plus padding), the text will not be completely inside the box + * @return Whether the button is pressed or not + */ + auto draw_button(Context& context, std::string_view utf8_text, const extent_type& size = {0, 0}, bool repeat_when_held = false) noexcept -> bool; + + /** + * @brief Draw a button, it fits within text without additional spacing + * @return Whether the button is pressed or not + */ + auto draw_small_button(Context& context, std::string_view utf8_text, bool repeat_when_held = false) noexcept -> bool; + + /** + * @brief Draw a radio button, its initial state is required + * @return Whether the radio button is pressed (being pressed does not mean that the state is switched, it can also be a repeat selection) + */ + auto draw_radio_button(Context& context, std::string_view utf8_text, bool checked) noexcept -> bool; + + /** + * @brief Draw a radio button, its initial state and referenced state is required + * @return Whether the radio button is pressed (being pressed does not mean that the state is switched, it can also be a repeat selection) + */ + template + auto draw_radio_button(Context& context, std::string_view utf8_text, T& reference, const std::type_identity_t& identifier) noexcept -> bool; + + + /** + * @brief Draw a checkbox, its initial state is required + * @return Current state of the checkbox (checked or unchecked) + */ + auto draw_checkbox(Context& context, std::string_view utf8_text, bool checked) noexcept -> bool; + + /** + * @brief Draw a checkbox, its initial state and referenced state is required + * @return Current state of the checkbox (checked or unchecked) + */ + template + auto draw_checkbox( + Context& context, + std::string_view utf8_text, + T& reference, + const std::type_identity_t& checked_identifier, + const std::type_identity_t& unchecked_identifier + ) noexcept -> bool; + + auto draw_slider( + Context& context, + std::string_view utf8_text, + float& reference, + float min, + float max, + std::uint32_t decimal_precision = 3, + float power = 1 + ) noexcept -> bool; + + template + requires std::is_arithmetic_v + auto draw_slider( + Context& context, + std::string_view utf8_text, + T& reference, + ValueType min, + std::type_identity_t max, + std::uint32_t decimal_precision = 3, + float power = 1 + ) noexcept -> bool; + + template + requires std::is_arithmetic_v + auto draw_slider( + Context& context, + T&& object, + ValueType min, + std::type_identity_t max, + std::uint32_t decimal_precision = 3, + float power = 1 + ) noexcept -> bool; + + auto draw_slider_n( + Context& context, + std::string_view utf8_text, + std::span references, + float min, + float max, + std::uint32_t decimal_precision = 3, + float power = 1 + ) noexcept -> bool; + + auto draw_combo( + Context& context, + std::string_view utf8_text, + std::span selections, + std::size_t& selected, + std::size_t show_selection_count = 7 + ) noexcept -> bool; + + auto draw_combo( + Context& context, + std::string_view utf8_text, + std::span selections, + std::size_t& selected, + std::size_t show_selection_count = 7 + ) noexcept -> bool; + + auto draw_combo( + Context& context, + std::string_view utf8_text, + std::span selections, + std::size_t& selected, + std::size_t show_selection_count = 7 + ) noexcept -> bool; + + //------------------------------------------------------------------ + // WIDGET LAYOUT + //------------------------------------------------------------------ + + // < 0 + constexpr Theme::value_type layout_auto_size = -1; + + /** + * @brief Each element drawn will move the cursor to the next line of the canvas, + * this function forces the cursor to move to the end of the previous line (immediately after the previous element) + * @note @c column_width == @c layout_auto_size and @c spacing_width == @c layout_auto_size, + * the x-axis distance of the next drawn element from the previous element will depend on @c theme.item_spacing.width + * @note @c column_width == @c layout_auto_size and @c spacing_width != @c layout_auto_size, + * the x-axis distance of the next drawn element from the previous element will depend on @c spacing_width + * @note @c column_width != @c layout_auto_size (@c spacing_width can be any value, force to 0 if less than 0), + * the x-axis distance of the next drawn element from the previous element will depend on @c column_width + @c spacing_width + */ + auto layout_same_line(Context& context, Theme::value_type column_width = layout_auto_size, Theme::value_type spacing_width = layout_auto_size) noexcept -> void; + + //------------------------------------------------------------------ + // CANVAS LAYOUT + //------------------------------------------------------------------ + + auto push_item_width(Context& context, Theme::value_type new_item_width) noexcept -> void; + auto pop_item_width(Context& context) noexcept -> void; + + auto push_text_wrap_width(Context& context, Theme::value_type new_wrap_width) noexcept -> void; + auto pop_text_wrap_width(Context& context) noexcept -> void; + + //------------------------------------------------------------------ + // WIDGET STATE + //------------------------------------------------------------------ + + // The available area of the current drawing unit + [[nodiscard]] auto get_content_region_max(const Context& context) noexcept -> extent_type; + // The available area of the current window + [[nodiscard]] auto get_window_content_region_min(const Context& context) noexcept -> extent_type; + // The available area of the current window + [[nodiscard]] auto get_window_content_region_max(const Context& context) noexcept -> extent_type; + + // Whether the last drawn item was hovered by the mouse + [[nodiscard]] auto is_item_hovered(const Context& context) noexcept -> bool; + // Whether the last drawn item was focused by the mouse + [[nodiscard]] auto is_item_focused(const Context& context) noexcept -> bool; + } + + // ================================================== + // API + // ================================================== + + namespace gui + { + //------------------------------------------------------------------ + // CONTEXT + //------------------------------------------------------------------ + + auto set_current_context(Context& context) noexcept -> void; + auto get_current_context() noexcept -> Context&; + + auto create_current_context() noexcept -> void; + auto destroy_current_context() noexcept -> void; + + //------------------------------------------------------------------ + // FONT + //------------------------------------------------------------------ + + [[nodiscard]] auto set_default_font(const FontOption& option) noexcept -> Texture; + + // [[nodiscard]] auto push_font(const FontOption& option) noexcept -> Texture; + // auto pop_font() noexcept -> void; + + //------------------------------------------------------------------ + // THEME + //------------------------------------------------------------------ + + auto set_default_theme(const Theme& theme) noexcept -> void; + + auto push_theme(ThemeCategory category, Theme::color_type new_color) noexcept -> void; + auto pop_theme() noexcept -> void; + + //------------------------------------------------------------------ + // IO + //------------------------------------------------------------------ + + [[nodiscard]] auto get_io() noexcept -> IO&; + + //------------------------------------------------------------------ + // DRAW + //------------------------------------------------------------------ + + auto set_default_draw_list_flag(DrawListFlag flag) noexcept -> void; + + // auto push_draw_list_flag(DrawListFlag new_flag) noexcept -> void; + // auto pop_draw_list_flag() noexcept -> void; + + auto new_frame() noexcept -> void; + auto end_frame() noexcept -> void; + auto render() noexcept -> void; + + [[nodiscard]] auto get_draw_data() noexcept -> std::vector; + + //------------------------------------------------------------------ + // WIDGET + //------------------------------------------------------------------ + + auto set_next_window_point(const point_type& point) noexcept -> void; + + auto begin_window( + std::string_view name, + const extent_type& size = {0, 0}, + Theme::value_type fill_alpha = Theme::window_background_fill_alpha_not_set, + WindowFlag flag = WindowFlag::NONE + ) noexcept -> bool; + auto end_window() noexcept -> void; + + auto begin_child_window( + std::string_view name, + const extent_type& size = {0, 0}, + bool border = false, + WindowFlag flag = WindowFlag::NONE + ) noexcept -> void; + auto end_child_window() noexcept -> void; + + auto begin_tooltip_window() noexcept -> void; + auto end_tooltip_window() noexcept -> void; + + /** + * @brief Draw a piece of text + */ + auto draw_text(std::string_view utf8_text) noexcept -> void; + + /** + * @brief Draw a piece of text with specified color + */ + auto draw_text_colored(std::string_view utf8_text, Theme::color_type color) noexcept -> void; + + /** + * @brief Draw a button, if the specified size is smaller than the size occupied by the text (plus padding), the text will not be completely inside the box + * @return Whether the button is pressed or not + */ + auto draw_button(std::string_view utf8_text, const extent_type& size = {0, 0}, bool repeat_when_held = false) noexcept -> bool; + + /** + * @brief Draw a button, it fits within text without additional spacing + * @return Whether the button is pressed or not + */ + auto draw_small_button(std::string_view utf8_text, bool repeat_when_held = false) noexcept -> bool; + + /** + * @brief Draw a radio button, its initial state is required + * @return Whether the radio button is pressed (being pressed does not mean that the state is switched, it can also be a repeat selection) + */ + auto draw_radio_button(std::string_view utf8_text, bool checked) noexcept -> bool; + + /** + * @brief Draw a radio button, its initial state and referenced state is required + */ + template + auto draw_radio_button(std::string_view utf8_text, T& reference, const std::type_identity_t& identifier) noexcept -> void + { + auto& context = get_current_context(); + + gui::draw_radio_button(context, utf8_text, reference, identifier); + } + + /** + * @brief Draw a checkbox, its initial state is required + * @return Current state of the checkbox (checked or unchecked) + */ + auto draw_checkbox(std::string_view utf8_text, bool checked) noexcept -> bool; + + /** + * @brief Draw a checkbox, its initial state and referenced state is required + */ + template + static auto draw_checkbox( + const std::string_view utf8_text, + T& reference, + const std::type_identity_t& checked_identifier, + const std::type_identity_t& unchecked_identifier + ) noexcept -> void + { + auto& context = get_current_context(); + + gui::draw_checkbox(context, utf8_text, reference, checked_identifier, unchecked_identifier); + } + + auto draw_slider( + std::string_view utf8_text, + float& reference, + float min, + float max, + std::uint32_t decimal_precision = 3, + float power = 1 + ) noexcept -> bool; + + template + requires std::is_arithmetic_v + auto draw_slider( + T& object, + const ValueType min, + const std::type_identity_t max, + const std::uint32_t decimal_precision = 3, + const float power = 1 + ) noexcept -> bool + { + auto& context = get_current_context(); + + return gui::draw_slider(context, object, min, max, decimal_precision, power); + } + + auto draw_slider_n( + std::string_view utf8_text, + std::span references, + float min, + float max, + std::uint32_t decimal_precision = 3, + float power = 1 + ) noexcept -> bool; + + auto draw_combo( + std::string_view utf8_text, + std::span selections, + std::size_t& selected, + std::size_t show_selection_count = 7 + ) noexcept -> bool; + + auto draw_combo( + std::string_view utf8_text, + std::span selections, + std::size_t& selected, + std::size_t show_selection_count = 7 + ) noexcept -> bool; + + auto draw_combo( + std::string_view utf8_text, + std::span selections, + std::size_t& selected, + std::size_t show_selection_count = 7 + ) noexcept -> bool; + + //------------------------------------------------------------------ + // WIDGET LAYOUT + //------------------------------------------------------------------ + + /** + * @brief Each element drawn will move the cursor to the next line of the canvas, + * this function forces the cursor to move to the end of the previous line (immediately after the previous element) + * @note @c column_width == @c auto_size and @c spacing_width == @c auto_size, + * the x-axis distance of the next drawn element from the previous element will depend on @c theme.item_spacing.width + * @note @c column_width == @c auto_size and @c spacing_width != @c auto_size, + * the x-axis distance of the next drawn element from the previous element will depend on @c spacing_width + * @note @c column_width != @c auto_size (@c spacing_width can be any value, force to 0 if less than 0), + * the x-axis distance of the next drawn element from the previous element will depend on @c column_width + @c spacing_width + */ + auto layout_same_line(Theme::value_type column_width = layout_auto_size, Theme::value_type spacing_width = layout_auto_size) noexcept -> void; + + //------------------------------------------------------------------ + // CANVAS LAYOUT + //------------------------------------------------------------------ + + auto push_item_width(Theme::value_type new_item_width) noexcept -> void; + auto pop_item_width() noexcept -> void; + + auto push_text_wrap_width(Theme::value_type new_wrap_width) noexcept -> void; + auto pop_text_wrap_width() noexcept -> void; + + //------------------------------------------------------------------ + // WIDGET STATE + //------------------------------------------------------------------ + + // The available area of the current drawing unit + [[nodiscard]] auto get_content_region_max() noexcept -> extent_type; + // The available area of the current window + [[nodiscard]] auto get_window_content_region_min() noexcept -> extent_type; + // The available area of the current window + [[nodiscard]] auto get_window_content_region_max() noexcept -> extent_type; + + // Whether the last drawn item was hovered by the mouse + [[nodiscard]] auto is_item_hovered() noexcept -> bool; + // Whether the last drawn item was focused by the mouse + [[nodiscard]] auto is_item_focused() noexcept -> bool; + + //------------------------------------------------------------------ + // FOR TEST + //------------------------------------------------------------------ + + // todo + [[nodiscard]] auto test_theme() noexcept -> Theme; + + auto show_theme_editor() noexcept -> bool; + } +} + +#include diff --git a/src/gui/internal/common.cpp b/src/gui/internal/common.cpp new file mode 100644 index 00000000..ba109693 --- /dev/null +++ b/src/gui/internal/common.cpp @@ -0,0 +1,166 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +#include +#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE + +namespace +{ + using namespace gal::prometheus; + using namespace gui; + using namespace internal; + + // @see https://stackoverflow.com/a/2244088/15194693 + // Number of segments (N) is calculated using equation: + // N = ceil ( pi / acos(1 - error / r) ) where r > 0 and error <= r + [[nodiscard]] constexpr auto circle_segments_calc(const float radius, const float max_error) noexcept -> auto + { + constexpr auto circle_segments_roundup_to_even = [](const auto v) noexcept -> auto + { + return (v + 1) / 2 * 2; + }; + + return std::ranges::clamp( + circle_segments_roundup_to_even(static_cast(std::ceil(std::numbers::pi_v / std::acos(1 - std::ranges::min(radius, max_error) / radius)))), + DrawListSharedData::circle_segments_min, + DrawListSharedData::circle_segments_max + ); + } + + [[nodiscard]] constexpr auto circle_segments_calc_radius(const std::size_t n, const float max_error) noexcept -> auto + { + return max_error / (1 - math::cos(std::numbers::pi_v / std::ranges::max(static_cast(n), std::numbers::pi_v))); + } + + [[nodiscard]] constexpr auto circle_segments_calc_error(const std::size_t n, const float radius) noexcept -> auto + { + return (1 - math::cos(std::numbers::pi_v / std::ranges::max(static_cast(n), std::numbers::pi_v))) / radius; + } + + template + [[nodiscard]] constexpr auto vertex_sample_points_calc() noexcept -> DrawListSharedData::vertex_sample_points_type + { + return [](std::index_sequence) noexcept -> DrawListSharedData::vertex_sample_points_type + { + constexpr auto make_point = []() noexcept -> point_type + { + const auto a = static_cast(I) / static_cast(N) * 2 * std::numbers::pi_v; + return {math::cos(a), -math::sin(a)}; + }; + + return {{make_point.template operator()()...}}; + }(std::make_index_sequence{}); + } +} + +namespace gal::prometheus::gui::internal +{ + DrawListSharedData::DrawListSharedData() noexcept + : circle_segment_counts{}, + vertex_sample_points{vertex_sample_points_calc()}, + circle_segment_max_error{0}, + arc_fast_radius_cutoff{0}, + curve_tessellation_tolerance{1.25f} + { + set_circle_tessellation_max_error(.3f); + } + + auto DrawListSharedData::circle_auto_segment_count(const float radius) const noexcept -> circle_segment_count_type + { + // ceil to never reduce accuracy + if (const auto radius_index = static_cast(radius + .999999f); + radius_index < circle_segment_counts.size()) + { + return circle_segment_counts[radius_index]; + } + return static_cast(circle_segments_calc(radius, circle_segment_max_error)); + } + + auto DrawListSharedData::vertex_sample_point(const std::size_t index) const noexcept -> const point_type& + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(index < vertex_sample_points.size()); + + return vertex_sample_points[index]; + } + + auto DrawListSharedData::set_circle_tessellation_max_error(const float max_error) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(max_error > .0f); + + if (circle_segment_max_error == max_error) // NOLINT(clang-diagnostic-float-equal) + { + return; + } + + for (auto [index, count]: circle_segment_counts | std::views::enumerate) + { + const auto radius = static_cast(index); + count = static_cast(circle_segments_calc(radius, max_error)); + } + + circle_segment_max_error = max_error; + arc_fast_radius_cutoff = circle_segments_calc_radius(vertex_sample_points_count, max_error); + } + + auto DrawListSharedData::set_curve_tessellation_tolerance(const float tolerance) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(tolerance > .0f); + + curve_tessellation_tolerance = tolerance; + } + + PrimitiveAppender::PrimitiveAppender( + command_type& command, + vertex_list_type& vertex_list, + index_list_type& index_list + ) noexcept + : element_count_{command.element_count}, + vertex_list_{vertex_list}, + index_list_{index_list} {} + + auto PrimitiveAppender::vertex_count() const noexcept -> size_type + { + const auto& list = vertex_list_.get(); + const auto size = list.size(); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(size < std::numeric_limits::max(), "Too many vertex!"); + + return static_cast(size); + } + + auto PrimitiveAppender::reserve(const size_type vertex_count, const size_type index_count) noexcept -> void + { + auto& element_count = element_count_.get(); + auto& vertex = vertex_list_.get(); + auto& index = index_list_.get(); + + element_count += index_count; + vertex.reserve(vertex.size() + vertex_count); + index.reserve(index.size() + index_count); + } + + auto PrimitiveAppender::reserve(const std::size_t vertex_count, const std::size_t index_count) noexcept -> void + { + reserve(static_cast(vertex_count), static_cast(index_count)); + } + + auto PrimitiveAppender::add_vertex(const point_type& point, const uv_type& uv, const color_type color) noexcept -> void + { + auto& list = vertex_list_.get(); + + list.emplace_back(point, uv, color); + } + + auto PrimitiveAppender::add_index(const index_type a, const index_type b, const index_type c) noexcept -> void + { + auto& list = index_list_.get(); + + list.push_back(a); + list.push_back(b); + list.push_back(c); + } +} diff --git a/src/gui/internal/common.hpp b/src/gui/internal/common.hpp new file mode 100644 index 00000000..625c69bb --- /dev/null +++ b/src/gui/internal/common.hpp @@ -0,0 +1,212 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include + +#include +#include + +#include +#include + +namespace gal::prometheus +{ + namespace gui::internal + { + using circle_type = primitive::basic_circle_2d; + using ellipse_type = primitive::basic_ellipse_2d; + + using frame_count_type = std::uint32_t; + // The number of frames is always counted from 1(call new_frame first), + // so we can set the number of uninitialized frames to the maximum value, + // and its +1 overflows to 0, then we can compare it with current frame count normally + constexpr static auto frame_count_uninitialized{std::numeric_limits::max()}; + + using widget_id_type = functional::hash_result_type; + constexpr auto invalid_widget_id = std::numeric_limits::max(); + + class DrawList; + using draw_lists_type = std::vector>; + + enum class WindowInternalFlag : std::uint16_t + { + NONE = 0, + + CHILD_WINDOW = 1 << 0, + CHILD_WINDOW_AUTO_FIT_X = 1 << 1, + CHILD_WINDOW_AUTO_FIT_Y = 1 << 2, + + CATEGORY_TOOLTIP = 1 << 3, + CATEGORY_COMBO = 1 << 4, + }; + } + + namespace meta::user_defined + { + template<> + struct enum_is_flag : std::true_type {}; + } + + namespace gui::internal + { + class [[nodiscard]] WindowFlag final + { + gui::WindowFlag flag_; + WindowInternalFlag internal_flag_; + + public: + constexpr explicit (false) WindowFlag(const gui::WindowFlag flag) noexcept + : flag_{flag}, + internal_flag_{WindowInternalFlag::NONE} {} + + constexpr explicit WindowFlag(const gui::WindowFlag flag, const WindowInternalFlag internal_flag) noexcept + : flag_{flag}, + internal_flag_{internal_flag} {} + + constexpr auto operator|=(const gui::WindowFlag flag) noexcept -> WindowFlag& + { + flag_ |= flag; + return *this; + } + + constexpr auto operator|=(const WindowInternalFlag flag) noexcept -> WindowFlag& + { + internal_flag_ |= flag; + return *this; + } + + constexpr auto operator&=(const gui::WindowFlag flag) noexcept -> WindowFlag& + { + flag_ &= flag; + return *this; + } + + constexpr auto operator&=(const WindowInternalFlag flag) noexcept -> WindowFlag& + { + internal_flag_ &= flag; + return *this; + } + + #if defined(GAL_PROMETHEUS_COMPILER_MSVC) + + template + struct workaround + { + T flag; + + constexpr explicit (false) workaround(const T flag) noexcept + : flag{flag} {} + }; + + template Flag> + [[nodiscard]] constexpr auto is() const noexcept -> bool + { + return std::to_underlying(flag_) & Flag.flag; + } + + template Flag> + [[nodiscard]] constexpr auto is() const noexcept -> bool + { + return std::to_underlying(internal_flag_) & Flag.flag; + } + + #else + + template + [[nodiscard]] constexpr auto is() const noexcept -> bool + { + return std::to_underlying(flag_) & Flag; + } + + template + [[nodiscard]] constexpr auto is() const noexcept -> bool + { + return std::to_underlying(internal_flag_) & Flag; + } + + #endif + }; + + static_assert(sizeof(WindowFlag) == sizeof(std::uint32_t)); + + class [[nodiscard]] DrawListSharedData final + { + public: + using circle_segment_count_type = std::uint8_t; + constexpr static std::size_t circle_segment_counts_count = 64; + using circle_segment_counts_type = std::array; + + constexpr static std::uint32_t circle_segments_min = 4; + constexpr static std::uint32_t circle_segments_max = 512; + + constexpr static std::size_t vertex_sample_points_count = 48; + using vertex_sample_points_type = std::array; + + circle_segment_counts_type circle_segment_counts; + vertex_sample_points_type vertex_sample_points; + + // Maximum error (in pixels) allowed when using `circle`/`circle_filled` or drawing rounded corner rectangles with no explicit segment count specified. + // Decrease for higher quality but more geometry. + float circle_segment_max_error; + // Cutoff radius after which arc drawing will fall back to slower `path_arc` + float arc_fast_radius_cutoff; + // Tessellation tolerance when using `path_bezier_curve` without a specific number of segments. + // Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. + float curve_tessellation_tolerance; + + DrawListSharedData() noexcept; + + // -------------------------------------------------- + + [[nodiscard]] auto circle_auto_segment_count(float radius) const noexcept -> circle_segment_count_type; + + [[nodiscard]] auto vertex_sample_point(std::size_t index) const noexcept -> const point_type&; + + // -------------------------------------------------- + + auto set_circle_tessellation_max_error(float max_error) noexcept -> void; + + auto set_curve_tessellation_tolerance(float tolerance) noexcept -> void; + }; + + class [[nodiscard]] PrimitiveAppender final + { + public: + using size_type = DrawData::size_type; + + using command_type = DrawData::command_type; + + using vertex_list_type = DrawData::vertex_list_type; + using index_list_type = DrawData::index_list_type; + + private: + // command_type::element_count + memory::RefWrapper element_count_; + // DrawList::vertex_list + memory::RefWrapper vertex_list_; + // DrawList::index_list_; + memory::RefWrapper index_list_; + + public: + PrimitiveAppender( + command_type& command, + vertex_list_type& vertex_list, + index_list_type& index_list + ) noexcept; + + [[nodiscard]] auto vertex_count() const noexcept -> size_type; + + auto reserve(size_type vertex_count, size_type index_count) noexcept -> void; + + auto reserve(std::size_t vertex_count, std::size_t index_count) noexcept -> void; + + auto add_vertex(const point_type& point, const uv_type& uv, color_type color) noexcept -> void; + + auto add_index(index_type a, index_type b, index_type c) noexcept -> void; + }; + } +} diff --git a/src/gui/internal/context.cpp b/src/gui/internal/context.cpp new file mode 100644 index 00000000..86324f5b --- /dev/null +++ b/src/gui/internal/context.cpp @@ -0,0 +1,1474 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +#include +#include +#include + +#include +#include + +#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE + +namespace +{ + using namespace gal::prometheus; + using namespace gui; + + Context* g_context = nullptr; +} + +namespace gal::prometheus::gui +{ + Context::Context() noexcept + : + initialized_{false}, + draw_list_flag_{DrawListFlag::NONE}, + draw_list_shared_data_{}, + font_{}, + theme_{}, + theme_mod_stack_{}, + io_{}, + time_total_{0}, + frame_count_{0}, + frame_count_rendered_{0}, + mouse_{}, + window_default_spawn_position_{50, 50}, + window_hive_{}, + window_root_list_{}, + window_current_stack_{}, + window_hovered_{nullptr}, + window_hovered_root_{nullptr}, + window_focused_{nullptr}, + widget_hovered_{internal::invalid_widget_id}, + widget_activated_{internal::invalid_widget_id}, + widget_activated_previous_frame_{internal::invalid_widget_id}, + widget_activated_still_alive_{false}, + widget_activated_combo_id_{internal::invalid_widget_id}, + draw_lists_{} {} + + auto Context::create() noexcept -> Context* + { + auto* p = new Context{}; + + // todo + p->initialized_ = true; + + return p; + } + + auto Context::destroy(Context& context) noexcept -> void + { + delete std::addressof(context); + } + + auto Context::set_default_draw_list_flag(const DrawListFlag flag) noexcept -> void + { + draw_list_flag_ = flag; + } + + auto Context::current_draw_list_flag() const noexcept -> DrawListFlag + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(initialized_); + + // return draw_list_flag_stack[draw_list_flag_current]; + return draw_list_flag_; + } + + // auto Context::push_draw_list_flag(const DrawListFlag flag) noexcept -> void + // { + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(initialized); + // + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME( + // draw_list_flag_current == Context::stack_pointer_default or + // draw_list_flag_current < Context::draw_list_flag_stack_size, + // "DrawListFlag stack overflow" + // ); + // + // static_assert( + // static_cast<::Context::stack_pointer_type>(::Context::stack_pointer_default + 1) == + // static_cast<::Context::stack_pointer_type>(0) + // ); + // + // draw_list_flag_current += 1; + // draw_list_flag_stack[draw_list_flag_current] = flag; + // } + // + // auto Context::pop_draw_list_flag() noexcept -> void + // { + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(initialized); + // + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME( + // draw_list_flag_current != Context::stack_pointer_default, + // "Unable to popup the default DrawListFlag!" + // ); + // + // static_assert( + // static_cast(static_cast(0) - 1) == + // Context::stack_pointer_default + // ); + // + // draw_list_flag_stack[draw_list_flag_current] = DrawListFlag::NONE; + // draw_list_flag_current -= 1; + // } + + auto Context::current_draw_list_shared_data() const noexcept -> const internal::DrawListSharedData& + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(initialized_); + + // return draw_list_shared_data_stack[draw_list_shared_data_current]; + return draw_list_shared_data_; + } + + // auto Context::push_draw_list_shared_data(const DrawListSharedData& shared_data) noexcept -> void + // { + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(initialized); + // + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME( + // draw_list_shared_data_current == Context::stack_pointer_default or + // draw_list_shared_data_current < Context::draw_list_shared_data_stack_size, + // "DrawListSharedData stack overflow" + // ); + // + // static_assert( + // static_cast<::Context::stack_pointer_type>(::Context::stack_pointer_default + 1) == + // static_cast<::Context::stack_pointer_type>(0) + // ); + // + // draw_list_shared_data_current += 1; + // draw_list_shared_data_stack[draw_list_shared_data_current] = shared_data; + // } + // + // auto Context::pop_draw_list_shared_data() noexcept -> void + // { + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(initialized); + // + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME( + // draw_list_shared_data_current != Context::stack_pointer_default, + // "Unable to popup the default DrawListSharedData!" + // ); + // + // static_assert( + // static_cast(static_cast(0) - 1) == + // Context::stack_pointer_default + // ); + // + // draw_list_shared_data_stack[draw_list_shared_data_current] = {}; + // draw_list_shared_data_current -= 1; + // } + + auto Context::set_default_font(const FontOption& option) noexcept -> Texture + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(initialized_); + + // if (context.font_current == Context::stack_pointer_default) + // { + // return push_font(context, option); + // } + // + // auto font = memory::make_unique(); + // auto texture = do_load_font(option, *font); + // + // context.font_stack[0] = std::move(font); + // + // return texture; + return font_.load(option); + } + + auto Context::current_font() const noexcept -> const internal::Font& + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(initialized_); + + // const auto& font = *font_stack[font_current]; + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(font.loaded(), "Invalid font!"); + // + // return font; + return font_; + } + + auto Context::set_default_theme(const Theme& default_theme) noexcept -> void + { + theme_ = default_theme; + } + + auto Context::current_theme() const noexcept -> const Theme& + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(initialized_); + + return theme_; + } + + auto Context::push_theme(const ThemeCategory category, const Theme::color_type new_color) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(initialized_); + + const auto old_color = color_of(category); + theme_mod_stack_.emplace_back(category, old_color); + + theme_.colors[static_cast(category)] = new_color; + } + + auto Context::pop_theme() noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(initialized_); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not theme_mod_stack_.empty()); + + const auto [category, old_color] = theme_mod_stack_.back(); + theme_mod_stack_.pop_back(); + theme_.colors[static_cast(category)] = old_color; + } + + auto Context::color_of(const ThemeCategory category, const Theme::value_type factor) const noexcept -> Theme::color_type + { + const auto& theme = current_theme(); + + return color_of(theme, category, factor); + } + + auto Context::color_of(const Theme& theme, const ThemeCategory category, const Theme::value_type factor) const noexcept -> Theme::color_type + { + std::ignore = this; + + const auto index = static_cast(category); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(index < theme_category_count); + auto color = theme.colors[static_cast(category)]; + + color.alpha = static_cast(theme.alpha * 255 * factor); + + return color; + } + + auto Context::io() noexcept -> IO& + { + return io_; + } + + auto Context::io() const noexcept -> const IO& + { + return io_; + } + + auto Context::mouse() const noexcept -> const internal::Mouse& + { + return mouse_; + } + + auto Context::test_mouse(const widget_id_type id, const rect_type& area, const bool repeat) noexcept -> std::underlying_type_t + { + const auto& window = current_window(); + const auto hovered = + window_hovered_root_ == std::addressof(window.root()) and + widget_hovered_ == internal::invalid_widget_id and + window.is_hovered(*this, area); + + auto state = std::to_underlying(MouseState::NONE); + if (hovered) + { + state |= MouseState::HOVERED; + + // hovering widget + widget_hovered_ = id; + + if (mouse_.is_clicked(*this, MouseKey::LEFT, false)) + { + // select widget + widget_activated_ = id; + } + else if ( + repeat and + widget_activated_ != internal::invalid_widget_id and + mouse_.is_clicked(*this, MouseKey::LEFT, true) + ) + { + state |= MouseState::PRESSED; + } + } + + if (widget_activated_ == id) + { + if (mouse_.is_down(*this, MouseKey::LEFT)) + { + // select current widget, keep the left mouse button pressed + state |= MouseState::KEEPING; + } + else + { + if (hovered) + { + // select current widget, release the left mouse button on the widget + state |= MouseState::PRESSED; + } + else + { + // select current widget, did not release the left mouse button on the widget + // do nothing + } + + // the widget is no longer selected + widget_activated_ = internal::invalid_widget_id; + } + } + + return state; + } + + auto Context::queue_mouse(const widget_id_type id, const rect_type& area) noexcept -> std::underlying_type_t + { + const auto& window = current_window(); + const auto hovered = + window_hovered_root_ == std::addressof(window.root()) and + widget_hovered_ == internal::invalid_widget_id and + window.is_hovered(*this, area); + + auto state = std::to_underlying(MouseState::NONE); + if (hovered) + { + state |= MouseState::HOVERED; + + // hovering widget + widget_hovered_ = id; + + if (mouse_.is_clicked(*this, MouseKey::LEFT, false)) + { + state |= MouseState::PRESSED; + } + } + + return state; + } + + auto Context::find_window(std::string_view name) noexcept -> window_type* + { + const auto it = std::ranges::find_if( + window_hive_, + [name](const auto& window) noexcept -> bool + { + return window->name() == name; + } + ); + + if (it != window_hive_.end()) + { + return it.operator*().get(); + } + + return nullptr; + } + + auto Context::find_or_create_window(const std::string_view name, const extent_type& size, const internal::WindowFlag flag) noexcept -> window_type& + { + [[maybe_unused]] const auto is_child_window = flag.is(); + if (is_child_window) + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not window_current_stack_.empty()); + } + + if (auto* window = find_window(name); + window == nullptr) + { + auto* root = is_child_window ? current_root_window() : nullptr; + if (is_child_window and root == nullptr) + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(window_current_stack_.empty()); + } + + // fixme: load cached settings? + auto temp = std::make_unique( + name, + flag, + window_default_spawn_position_, + size, + root + ); + + auto& ref = window_hive_.emplace_back(std::move(temp)); + if (root == nullptr) + { + // The current window is the root window + window_root_list_.emplace_back(ref.get()); + } + + window_current_stack_.emplace_back(ref.get()); + } + else + { + window->reset(flag, size); + + window_current_stack_.emplace_back(window); + } + + return *window_current_stack_.back(); + } + + auto Context::current_window() const noexcept -> window_type& + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not window_current_stack_.empty()); + + const auto index = window_current_stack_.size() - 1; + return *window_current_stack_[index]; + } + + auto Context::current_parent_window() const noexcept -> window_type& + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(window_current_stack_.size() > 1); + + const auto index = window_current_stack_.size() - 2; + return *window_current_stack_[index]; + } + + auto Context::current_root_window() noexcept -> window_type* + { + const auto view = window_current_stack_ | std::views::reverse; + + const auto it = std::ranges::find_if( + view, + [](const auto& window) noexcept -> bool + { + const auto flag = window->flag(); + return not flag.template is(); + } + ); + + if (it == std::ranges::end(view)) + { + return nullptr; + } + + return it.operator*(); + } + + auto Context::set_next_window_point(const point_type& point) noexcept -> void + { + // todo + window_default_spawn_position_ = point; + } + + auto Context::begin_window( + const std::string_view name, + const extent_type& size, + Theme::value_type background_fill_alpha, + const internal::WindowFlag flag + ) noexcept -> bool + { + const auto is_child_window = flag.is(); + auto* parent = is_child_window ? std::addressof(current_window()) : nullptr; + + auto& window = find_or_create_window(name, size, flag); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(window_current_stack_.back() == std::addressof(window)); + + // alpha + static_assert(window_fill_alpha_not_set < 0); + if (background_fill_alpha < 0) + { + background_fill_alpha = theme_.window_background_alpha; + } + + return window.begin_window(*this, parent, background_fill_alpha); + } + + auto Context::end_window() noexcept -> void + { + auto& window = current_window(); + + window.end_window(*this); + + // Select window for move/focus when we're done with all our widgets (we only consider non-children windows here) + if (const auto rect = window.rect(); + widget_activated_ == internal::invalid_widget_id and + widget_hovered_ == internal::invalid_widget_id and + window_hovered_root_ == std::addressof(window) and + window.is_hovered(*this, rect) and + mouse_.is_clicked(*this, MouseKey::LEFT) + ) + { + widget_activated_ = window.id_of_move(*this); + } + + window_current_stack_.pop_back(); + } + + auto Context::begin_child_window( + const std::string_view name, + const extent_type& size, + const Theme::alpha_type background_fill_alpha, + const internal::WindowFlag flag, + const bool border + ) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not window_current_stack_.empty()); + auto& parent = current_window(); + + // todo + parent.begin_child_window(*this, name, background_fill_alpha, size, border, flag); + } + + auto Context::end_child_window() noexcept -> void + { + auto& parent = current_parent_window(); + auto& child = current_window(); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(child.flag().is()); + + parent.end_child_window(*this, child); + } + + auto Context::is_window_hovered(const window_type& window) const noexcept -> bool + { + return window_hovered_ == std::addressof(window); + } + + auto Context::find_hovered_window(const point_type& position, const bool excludes_children) noexcept -> window_type* + { + for (const auto view = window_root_list_ | std::views::reverse; + auto* root: view) + { + if (auto* window = root->find_hovered_window(position, excludes_children); + window != nullptr) + { + return window; + } + } + + return nullptr; + } + + auto Context::focus_window(window_type& window) noexcept -> void + { + if (window_focused_ == std::addressof(window)) + { + return; + } + + window_focused_ = std::addressof(window); + + auto& root = window.root(); + + const auto it = std::ranges::find(window_root_list_, std::addressof(root)); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(it != window_root_list_.end()); + + if (it != window_root_list_.end() - 1) + { + // The focused window is drawn last + const auto p = *it; + window_root_list_.erase(it); + window_root_list_.push_back(p); + } + + // todo: reorder child window? + } + + auto Context::is_widget_hovered(const widget_id_type id) const noexcept -> bool + { + return widget_hovered_ == id; + } + + auto Context::is_any_widget_hovered() const noexcept -> bool + { + return widget_hovered_ != internal::invalid_widget_id; + } + + auto Context::is_widget_activated(const widget_id_type id) const noexcept -> bool + { + return widget_activated_ == id; + } + + auto Context::is_any_widget_activated() const noexcept -> bool + { + return widget_activated_ != internal::invalid_widget_id; + } + + auto Context::mark_widget_alive(const widget_id_type id) noexcept -> bool + { + if (is_widget_activated(id)) + { + widget_activated_still_alive_ = true; + + return true; + } + + return false; + } + + auto Context::mark_widget_dead(const widget_id_type id) noexcept -> void + { + widget_activated_ = id; + } + + auto Context::mark_combo_alive(const widget_id_type id) noexcept -> void + { + widget_activated_combo_id_ = id; + } + + auto Context::mark_combo_dead(const widget_id_type id) noexcept -> void + { + widget_activated_combo_id_ = id; + } + + auto Context::is_combo_activated(const widget_id_type id) const noexcept -> bool + { + return widget_activated_combo_id_ == id; + } + + auto Context::current_time() const noexcept -> time_type + { + return time_total_; + } + + auto Context::current_frame() const noexcept -> internal::frame_count_type + { + return frame_count_; + } + + auto Context::new_frame() noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(initialized_); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(io_.display_size.width > 0 and io_.display_size.height > 0); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(io_.delta_time > 0); + + time_total_ += io_.delta_time; + frame_count_ += 1; + + // Update mouse + { + mouse_.tick(*this); + } + + const auto mouse_position = mouse_.position_current; + + // Update widget + { + // Clear reference to active widget if the widget isn't alive anymore + widget_hovered_ = internal::invalid_widget_id; + if ( + not widget_activated_still_alive_ and + widget_activated_previous_frame_ == widget_activated_ and + widget_activated_ != internal::invalid_widget_id + ) + { + widget_activated_ = internal::invalid_widget_id; + } + widget_activated_previous_frame_ = widget_activated_; + widget_activated_still_alive_ = false; + } + + // Update window + { + window_hovered_ = find_hovered_window(mouse_position, false); + window_hovered_root_ = find_hovered_window(mouse_position, true); + + if (window_hovered_ != nullptr) + { + window_hovered_->handle_inputs(*this); + } + + // Mark all windows as not visible + std::ranges::for_each( + window_root_list_, + [](auto* window) noexcept -> void + { + window->hide(); + } + ); + + // No window should be open at the beginning of the frame + // But in order to allow the user to call `new_frame` multiple times without calling `render`, we are doing an explicit clear + window_current_stack_.clear(); + } + } + + auto Context::end_frame() noexcept -> void + { + std::ignore = this; + } + + auto Context::render() noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(initialized_); + + const auto& theme = current_theme(); + + const auto is_first_render_this_frame = frame_count_rendered_ != frame_count_; + frame_count_rendered_ = frame_count_; + + if (is_first_render_this_frame) + { + // clear all data for new frame + io_.delta_time = -1; + // io.mouse_position = {0, 0}; + io_.mouse_wheel = 0; + // io.mouse_button_state.state.fill(false); + } + + draw_lists_.clear(); + + if (theme.alpha > .0f) + { + // gather windows to render + std::ranges::for_each( + window_root_list_, + [this](auto* window) noexcept -> void + { + window->render(*this, draw_lists_); + } + ); + } + } + + auto Context::get_draw_data() noexcept -> std::vector + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(initialized_); + + std::vector data; + data.reserve(draw_lists_.size()); + + std::ranges::transform( + draw_lists_, + std::back_inserter(data), + [](const auto& draw_list_ref) noexcept -> DrawData + { + auto& draw_list = draw_list_ref.get(); + + return + { + .vertex_list = draw_list.vertex_list(), + .index_list = draw_list.index_list(), + .command_list = draw_list.command_list() + }; + } + ); + + return data; + } + + auto Context::show_theme_editor() noexcept -> bool + { + auto& context = get_current_context(); + auto& theme = context.theme_; + + const auto window_closed = gui::begin_window(context, "ThemeEditor"); + + draw_slider<"window_background_alpha">(context, theme, 0, 1); + draw_slider<"window_corner_rounding">(context, theme, 0, 24); + draw_slider<"window_min_size.width">(context, theme, 64, 640); + draw_slider<"window_min_size.height">(context, theme, 48, 480); + draw_slider<"window_resize_grip_size.width">(context, theme, 10, 50); + draw_slider<"window_resize_grip_size.height">(context, theme, 10, 50); + draw_slider<"window_padding.width">(context, theme, 2, 20); + draw_slider<"window_padding.height">(context, theme, 2, 20); + draw_slider<"window_auto_fit_padding.width">(context, theme, 2, 20); + draw_slider<"window_auto_fit_padding.height">(context, theme, 2, 20); + draw_slider<"window_vertical_scrollbar_width">(context, theme, 6, 25); + draw_slider<"item_default_width_factor">(context, theme, .35f, .85f); + draw_slider<"item_frame_padding.width">(context, theme, 1, 10); + draw_slider<"item_frame_padding.height">(context, theme, 1, 10); + draw_slider<"item_spacing.width">(context, theme, 1, 10); + draw_slider<"item_spacing.height">(context, theme, 1, 10); + draw_slider<"alpha">(context, theme, 0, 1); + + // todo: update DrawListSharedData + draw_slider<"circle_segment_max_error">(context, theme, 0, 1); + draw_slider<"draw_curve_tessellation_tolerance">(context, theme, 0, 5); + + gui::end_window(context); + + return window_closed; + } + + auto create_context() noexcept -> Context* + { + return Context::create(); + } + + auto destroy_context(Context& context) noexcept -> void + { + Context::destroy(context); + } + + auto destroy_context(Context* context) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(context != nullptr); + destroy_context(*context); + } + + auto set_default_font(Context& context, const FontOption& option) noexcept -> Texture + { + return context.set_default_font(option); + } + + auto set_default_theme(Context& context, const Theme& theme) noexcept -> void + { + context.set_default_theme(theme); + } + + auto push_theme(Context& context, const ThemeCategory category, const Theme::color_type new_color) noexcept -> void + { + context.push_theme(category, new_color); + } + + auto pop_theme(Context& context) noexcept -> void + { + context.pop_theme(); + } + + auto get_io(Context& context) noexcept -> IO& + { + return context.io(); + } + + auto set_default_draw_list_flag(Context& context, const DrawListFlag flag) noexcept -> void + { + context.set_default_draw_list_flag(flag); + } + + auto new_frame(Context& context) noexcept -> void + { + context.new_frame(); + } + + auto end_frame(Context& context) noexcept -> void + { + context.end_frame(); + } + + auto render(Context& context) noexcept -> void + { + context.render(); + } + + [[nodiscard]] auto get_draw_data(Context& context) noexcept -> std::vector + { + return context.get_draw_data(); + } + + auto set_next_window_point(Context& context, const point_type& point) noexcept -> void + { + context.set_next_window_point(point); + } + + auto begin_window( + Context& context, + const std::string_view name, + const extent_type& size, + const Theme::alpha_type fill_alpha, + const WindowFlag flag + ) noexcept -> bool + { + return context.begin_window(name, size, fill_alpha, flag); + } + + auto end_window(Context& context) noexcept -> void + { + context.end_window(); + } + + auto begin_child_window( + Context& context, + const std::string_view name, + const extent_type& size, + const bool border, + const WindowFlag flag + ) noexcept -> void + { + context.begin_child_window(name, size, 0, flag, border); + } + + auto end_child_window(Context& context) noexcept -> void + { + context.end_child_window(); + } + + auto begin_tooltip_window(Context& context) noexcept -> void + { + // todo + constexpr std::string_view tooltip_window_name{"@WINDOW::TOOLTIP@"}; + constexpr internal::WindowFlag tooltip_window_flag + { + WindowFlag::NO_TITLEBAR | WindowFlag::NO_CLOSE | WindowFlag::NO_RESIZE | WindowFlag::NO_MOVE, + internal::WindowInternalFlag::CATEGORY_TOOLTIP + }; + + std::ignore = context.begin_window(tooltip_window_name, {}, .9f, tooltip_window_flag); + } + + auto end_tooltip_window(Context& context) noexcept -> void + { + const auto& window = context.current_window(); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(window.flag().is()); + + context.end_window(); + } + + auto draw_text(Context& context, const std::string_view utf8_text) noexcept -> void + { + auto& window = context.current_window(); + window.draw_text(context, utf8_text); + } + + auto draw_text_colored(Context& context, const std::string_view utf8_text, const Theme::color_type color) noexcept -> void + { + push_theme(context, ThemeCategory::TEXT, color); + draw_text(context, utf8_text); + pop_theme(context); + } + + auto draw_button(Context& context, const std::string_view utf8_text, const extent_type& size, const bool repeat_when_held) noexcept -> bool + { + auto& window = context.current_window(); + return window.draw_button(context, utf8_text, size, repeat_when_held); + } + + auto draw_small_button(Context& context, const std::string_view utf8_text, const bool repeat_when_held) noexcept -> bool + { + auto& window = context.current_window(); + return window.draw_small_button(context, utf8_text, repeat_when_held); + } + + auto draw_radio_button(Context& context, const std::string_view utf8_text, const bool checked) noexcept -> bool + { + auto& window = context.current_window(); + return window.draw_radio_button(context, utf8_text, checked); + } + + auto draw_checkbox(Context& context, const std::string_view utf8_text, const bool checked) noexcept -> bool + { + auto& window = context.current_window(); + return window.draw_checkbox(context, utf8_text, checked); + } + + auto draw_slider( + Context& context, + const std::string_view utf8_text, + float& reference, + const float min, + const float max, + const std::uint32_t decimal_precision, + const float power + ) noexcept -> bool + { + auto& window = context.current_window(); + return window.draw_slider(context, utf8_text, reference, min, max, decimal_precision, power); + } + + auto draw_slider_n( + Context& context, + const std::string_view utf8_text, + const std::span references, + const float min, + const float max, + const std::uint32_t decimal_precision, + const float power + ) noexcept -> bool + { + auto& window = context.current_window(); + return window.draw_slider_n(context, utf8_text, references, min, max, decimal_precision, power); + } + + auto draw_combo( + Context& context, + const std::string_view utf8_text, + const std::span selections, + std::size_t& selected, + const std::size_t show_selection_count + ) noexcept -> bool + { + auto& window = context.current_window(); + return window.draw_combo(context, utf8_text, selections, selected, show_selection_count); + } + + auto draw_combo( + Context& context, + const std::string_view utf8_text, + const std::span selections, + std::size_t& selected, + const std::size_t show_selection_count + ) noexcept -> bool + { + auto& window = context.current_window(); + return window.draw_combo(context, utf8_text, selections, selected, show_selection_count); + } + + auto draw_combo( + Context& context, + const std::string_view utf8_text, + const std::span selections, + std::size_t& selected, + const std::size_t show_selection_count + ) noexcept -> bool + { + auto& window = context.current_window(); + return window.draw_combo(context, utf8_text, selections, selected, show_selection_count); + } + + auto layout_same_line(Context& context, const Theme::value_type column_width, const Theme::value_type spacing_width) noexcept -> void + { + auto& window = context.current_window(); + window.same_line(context, column_width, spacing_width); + } + + auto push_item_width(Context& context, const Theme::value_type new_item_width) noexcept -> void + { + auto& window = context.current_window(); + window.push_item_width(context, new_item_width); + } + + auto pop_item_width(Context& context) noexcept -> void + { + auto& window = context.current_window(); + window.pop_item_width(context); + } + + auto push_text_wrap_width(Context& context, const Theme::value_type new_wrap_width) noexcept -> void + { + auto& window = context.current_window(); + window.push_text_wrap_width(context, new_wrap_width); + } + + auto pop_text_wrap_width(Context& context) noexcept -> void + { + auto& window = context.current_window(); + window.pop_text_wrap_width(context); + } + + auto get_content_region_max(const Context& context) noexcept -> extent_type + { + const auto& window = context.current_window(); + return window.content_region_max(context); + } + + auto get_window_content_region_min(const Context& context) noexcept -> extent_type + { + const auto& window = context.current_window(); + return window.window_content_region_min(context); + } + + auto get_window_content_region_max(const Context& context) noexcept -> extent_type + { + const auto& window = context.current_window(); + return window.window_content_region_max(context); + } + + auto is_item_hovered(const Context& context) noexcept -> bool + { + const auto& window = context.current_window(); + return window.is_item_hovered(context); + } + + auto is_item_focused(const Context& context) noexcept -> bool + { + const auto& window = context.current_window(); + return window.is_item_hovered(context); + } + + auto set_current_context(Context& context) noexcept -> void + { + g_context = std::addressof(context); + } + + auto get_current_context() noexcept -> Context& + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(g_context != nullptr); + + return *g_context; + } + + auto create_current_context() noexcept -> void + { + g_context = create_context(); + } + + auto destroy_current_context() noexcept -> void + { + auto& context = get_current_context(); + + destroy_context(context); + } + + auto set_default_font(const FontOption& option) noexcept -> Texture + { + auto& context = get_current_context(); + + return set_default_font(context, option); + } + + auto set_default_theme(const Theme& theme) noexcept -> void + { + auto& context = get_current_context(); + + set_default_theme(context, theme); + } + + auto push_theme(const ThemeCategory category, const Theme::color_type new_color) noexcept -> void + { + auto& context = get_current_context(); + + push_theme(context, category, new_color); + } + + auto pop_theme() noexcept -> void + { + auto& context = get_current_context(); + + pop_theme(context); + } + + auto get_io() noexcept -> IO& + { + auto& context = get_current_context(); + + return get_io(context); + } + + auto set_default_draw_list_flag(const DrawListFlag flag) noexcept -> void + { + auto& context = get_current_context(); + + set_default_draw_list_flag(context, flag); + } + + auto new_frame() noexcept -> void + { + auto& context = get_current_context(); + + return new_frame(context); + } + + auto end_frame() noexcept -> void + { + auto& context = get_current_context(); + + return end_frame(context); + } + + auto render() noexcept -> void + { + auto& context = get_current_context(); + + return render(context); + } + + auto get_draw_data() noexcept -> std::vector + { + auto& context = get_current_context(); + + return get_draw_data(context); + } + + auto set_next_window_point(const point_type& point) noexcept -> void + { + auto& context = get_current_context(); + + set_next_window_point(context, point); + } + + auto begin_window( + const std::string_view name, + const extent_type& size, + const Theme::value_type fill_alpha, + const WindowFlag flag + ) noexcept -> bool + { + auto& context = get_current_context(); + + return begin_window(context, name, size, fill_alpha, flag); + } + + auto end_window() noexcept -> void + { + auto& context = get_current_context(); + + return end_window(context); + } + + auto begin_child_window( + const std::string_view name, + const extent_type& size, + const bool border, + const WindowFlag flag + ) noexcept -> void + { + auto& context = get_current_context(); + + begin_child_window(context, name, size, border, flag); + } + + auto end_child_window() noexcept -> void + { + auto& context = get_current_context(); + + end_child_window(context); + } + + auto begin_tooltip_window() noexcept -> void + { + auto& context = get_current_context(); + + begin_tooltip_window(context); + } + + auto end_tooltip_window() noexcept -> void + { + auto& context = get_current_context(); + + end_tooltip_window(context); + } + + auto draw_text(const std::string_view utf8_text) noexcept -> void + { + auto& context = get_current_context(); + + draw_text(context, utf8_text); + } + + auto draw_text_colored(const std::string_view utf8_text, const Theme::color_type color) noexcept -> void + { + auto& context = get_current_context(); + + draw_text_colored(context, utf8_text, color); + } + + auto draw_button(const std::string_view utf8_text, const extent_type& size, const bool repeat_when_held) noexcept -> bool + { + auto& context = get_current_context(); + + return draw_button(context, utf8_text, size, repeat_when_held); + } + + auto draw_small_button(const std::string_view utf8_text, const bool repeat_when_held) noexcept -> bool + { + auto& context = get_current_context(); + + return draw_small_button(context, utf8_text, repeat_when_held); + } + + auto draw_radio_button(const std::string_view utf8_text, const bool checked) noexcept -> bool + { + auto& context = get_current_context(); + + return draw_radio_button(context, utf8_text, checked); + } + + auto draw_checkbox(const std::string_view utf8_text, const bool checked) noexcept -> bool + { + auto& context = get_current_context(); + + return draw_checkbox(context, utf8_text, checked); + } + + auto draw_slider( + const std::string_view utf8_text, + float& reference, + const float min, + const float max, + const std::uint32_t decimal_precision, + const float power + ) noexcept -> bool + { + auto& context = get_current_context(); + + return draw_slider(context, utf8_text, reference, min, max, decimal_precision, power); + } + + auto draw_slider_n( + const std::string_view utf8_text, + const std::span references, + const float min, + const float max, + const std::uint32_t decimal_precision, + const float power + ) noexcept -> bool + { + auto& context = get_current_context(); + + return draw_slider_n(context, utf8_text, references, min, max, decimal_precision, power); + } + + auto draw_combo( + const std::string_view utf8_text, + const std::span selections, + std::size_t& selected, + const std::size_t show_selection_count + ) noexcept -> bool + { + auto& context = get_current_context(); + + return draw_combo(context, utf8_text, selections, selected, show_selection_count); + } + + auto draw_combo( + const std::string_view utf8_text, + const std::span selections, + std::size_t& selected, + const std::size_t show_selection_count + ) noexcept -> bool + { + auto& context = get_current_context(); + + return draw_combo(context, utf8_text, selections, selected, show_selection_count); + } + + auto draw_combo( + const std::string_view utf8_text, + const std::span selections, + std::size_t& selected, + const std::size_t show_selection_count + ) noexcept -> bool + { + auto& context = get_current_context(); + + return draw_combo(context, utf8_text, selections, selected, show_selection_count); + } + + auto layout_same_line(const Theme::value_type column_width, const Theme::value_type spacing_width) noexcept -> void + { + auto& context = get_current_context(); + + layout_same_line(context, column_width, spacing_width); + } + + auto push_item_width(const Theme::value_type new_item_width) noexcept -> void + { + auto& context = get_current_context(); + + push_item_width(context, new_item_width); + } + + auto pop_item_width() noexcept -> void + { + auto& context = get_current_context(); + + pop_item_width(context); + } + + auto push_text_wrap_width(const Theme::value_type new_wrap_width) noexcept -> void + { + auto& context = get_current_context(); + + push_text_wrap_width(context, new_wrap_width); + } + + auto pop_text_wrap_width() noexcept -> void + { + auto& context = get_current_context(); + + pop_text_wrap_width(context); + } + + auto get_content_region_max() noexcept -> extent_type + { + const auto& context = get_current_context(); + + return get_content_region_max(context); + } + + auto get_window_content_region_min() noexcept -> extent_type + { + const auto& context = get_current_context(); + + return get_window_content_region_min(context); + } + + auto get_window_content_region_max() noexcept -> extent_type + { + const auto& context = get_current_context(); + + return get_window_content_region_max(context); + } + + auto is_item_hovered() noexcept -> bool + { + const auto& context = get_current_context(); + + return is_item_hovered(context); + } + + auto is_item_focused() noexcept -> bool + { + const auto& context = get_current_context(); + + return is_item_focused(context); + } + + auto test_theme() noexcept -> Theme + { + constexpr auto default_colors = []() noexcept -> Theme::colors_type + { + using enum ThemeCategory; + + Theme::colors_type colors{}; + + colors[static_cast(TEXT)] = primitive::colors::black; + + colors[static_cast(BORDER)] = primitive::colors::magenta; + colors[static_cast(BORDER_SHADOW)] = primitive::colors::red; + + colors[static_cast(WINDOW_BACKGROUND)] = primitive::colors::gains_boro; + + colors[static_cast(TITLEBAR)] = primitive::colors::light_coral; + colors[static_cast(TITLEBAR_COLLAPSED)] = primitive::colors::dark_khaki; + + colors[static_cast(RESIZE_GRIP)] = primitive::colors::gold; + colors[static_cast(RESIZE_GRIP_HOVERED)] = primitive::colors::peru; + colors[static_cast(RESIZE_GRIP_ACTIVATED)] = primitive::colors::powder_blue; + + colors[static_cast(SCROLLBAR_BACKGROUND)] = primitive::colors::white; + colors[static_cast(SCROLLBAR_GRAB)] = primitive::colors::dark_salmon; + colors[static_cast(SCROLLBAR_GRAB_HOVERED)] = primitive::colors::dark_green; + colors[static_cast(SCROLLBAR_GRAB_ACTIVATED)] = primitive::colors::dark_goldenrod; + + colors[static_cast(CLOSE_BUTTON)] = primitive::colors::red; + colors[static_cast(CLOSE_BUTTON_HOVERED)] = primitive::colors::violet_red; + colors[static_cast(CLOSE_BUTTON_ACTIVATED)] = primitive::colors::white; + + colors[static_cast(TOOLTIP_BACKGROUND)] = primitive::colors::black; + colors[static_cast(TOOLTIP_TEXT)] = primitive::colors::red; + + colors[static_cast(BUTTON)] = primitive::colors::sienna; + colors[static_cast(BUTTON_HOVERED)] = primitive::colors::slate_gray; + colors[static_cast(BUTTON_ACTIVATED)] = primitive::colors::steel_blue; + + colors[static_cast(FRAME_BACKGROUND)] = primitive::colors::steel_blue; + + colors[static_cast(RADIO_BUTTON_HOVERED)] = primitive::colors::powder_blue; + colors[static_cast(RADIO_BUTTON_ACTIVATED)] = primitive::colors::green_yellow; + + colors[static_cast(CHECKBOX_HOVERED)] = primitive::colors::powder_blue; + colors[static_cast(CHECKBOX_ACTIVATED)] = primitive::colors::green_yellow; + + colors[static_cast(SLIDER)] = primitive::colors::light_blue; + colors[static_cast(SLIDER_ACTIVATED)] = primitive::colors::light_pink; + + colors[static_cast(COMBO_ITEM)] = primitive::colors::red; + colors[static_cast(COMBO_ITEM_HOVERED)] = primitive::colors::violet_red; + colors[static_cast(COMBO_ITEM_ACTIVATED)] = primitive::colors::white; + + return colors; + }; + + return + { + .window_background_alpha = .65f, + .window_corner_rounding = 0, + .window_min_size = {64, 48}, + .window_resize_grip_size = {20, 20}, + .window_padding = {8, 8}, + .window_auto_fit_padding = {8, 8}, + .window_vertical_scrollbar_width = 10, + .item_default_width_factor = .65f, + .item_frame_padding = {4, 4}, + .item_spacing = {10, 5}, + .item_inner_spacing = {5, 5}, + .alpha = 1, + .colors = default_colors(), + .circle_segment_max_error = .3f, + .draw_curve_tessellation_tolerance = 1.25f, + }; + } + + auto show_theme_editor() noexcept -> bool + { + return Context::show_theme_editor(); + } +} diff --git a/src/gui/internal/context.hpp b/src/gui/internal/context.hpp new file mode 100644 index 00000000..eb668714 --- /dev/null +++ b/src/gui/internal/context.hpp @@ -0,0 +1,313 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include +#include + +#include +#include +#include + +namespace gal::prometheus +{ + namespace gui + { + namespace internal + { + class Font; + class DrawList; + class Window; + } + + class Context + { + public: + using value_type = extent_type::value_type; + + struct theme_color_mod + { + ThemeCategory category; + Theme::color_type old_color; + }; + + using window_type = internal::Window; + using widget_id_type = internal::widget_id_type; + + // < 0 + constexpr static value_type window_fill_alpha_not_set{-.99999f}; + + private: + bool initialized_; + + // ---------------------------------------------------------------------- + // DrawListFlag + DrawListSharedData + Font + Theme + + DrawListFlag draw_list_flag_; + + internal::DrawListSharedData draw_list_shared_data_; + + internal::Font font_; + + Theme theme_; + std::vector theme_mod_stack_; + + // ---------------------------------------------------------------------- + // IO + + IO io_; + // time elapsed since program launch, in seconds + time_type time_total_; + std::uint32_t frame_count_; + std::uint32_t frame_count_rendered_; + + internal::Mouse mouse_; + // todo: keyboard + + // ---------------------------------------------------------------------- + // WINDOW + + point_type window_default_spawn_position_; + + // All created windows + std::vector> window_hive_; + + // Root window only, child windows are managed by their parent window + // The order of the windows in this list determines the drawing order, the windows at the end are drawn last + std::vector window_root_list_; + + // Current window stack, push the stack when begin_window is called, and pop the stack when end_window is called (Nested calls are also included in this list) + std::vector window_current_stack_; + + // catch mouse + window_type* window_hovered_; + // catch mouse (for focus/move only) + window_type* window_hovered_root_; + // catch keyboard + window_type* window_focused_; + + // ---------------------------------------------------------------------- + // WIDGET + + // widget for mouse hovering in this frame + widget_id_type widget_hovered_; + // widget for mouse selecting in this frame + widget_id_type widget_activated_; + // widget for mouse hovering in previous frame + widget_id_type widget_activated_previous_frame_; + // widget for mouse selecting in this frame is still alive + bool widget_activated_still_alive_; + + // Currently open combo window + widget_id_type widget_activated_combo_id_; + + // ---------------------------------------------------------------------- + // DRAW LIST + + internal::draw_lists_type draw_lists_; + + Context() noexcept; + + public: + // ---------------------------------------------------------------------- + // Context + + [[nodiscard]] static auto create() noexcept -> Context*; + static auto destroy(Context& context) noexcept -> void; + + // ---------------------------------------------------------------------- + // DrawListFlag + + auto set_default_draw_list_flag(DrawListFlag flag) noexcept -> void; + + [[nodiscard]] auto current_draw_list_flag() const noexcept -> DrawListFlag; + // auto push_draw_list_flag(DrawListFlag flag) noexcept -> void; + // auto pop_draw_list_flag() noexcept -> void; + + // ---------------------------------------------------------------------- + // DrawListSharedData + + [[nodiscard]] auto current_draw_list_shared_data() const noexcept -> const internal::DrawListSharedData&; + // auto push_draw_list_shared_data(const DrawListSharedData& shared_data) noexcept -> void; + // auto pop_draw_list_shared_data() noexcept -> void; + + // ---------------------------------------------------------------------- + // Font + + [[nodiscard]] auto set_default_font(const FontOption& option) noexcept -> Texture; + + [[nodiscard]] auto current_font() const noexcept -> const internal::Font&; + + // ---------------------------------------------------------------------- + // Theme + + auto set_default_theme(const Theme& default_theme) noexcept -> void; + + [[nodiscard]] auto current_theme() const noexcept -> const Theme&; + auto push_theme(ThemeCategory category, Theme::color_type new_color) noexcept -> void; + auto pop_theme() noexcept -> void; + + [[nodiscard]] auto color_of(ThemeCategory category, Theme::value_type factor = 1) const noexcept -> Theme::color_type; + [[nodiscard]] auto color_of(const Theme& theme, ThemeCategory category, Theme::value_type factor = 1) const noexcept -> Theme::color_type; + + // ---------------------------------------------------------------------- + // IO + + [[nodiscard]] auto io() noexcept -> IO&; + + [[nodiscard]] auto io() const noexcept -> const IO&; + + [[nodiscard]] auto mouse() const noexcept -> const internal::Mouse&; + + enum class MouseState : std::uint8_t + { + NONE = 0, + + HOVERED = 1 << 0, + PRESSED = 1 << 1, + KEEPING = 1 << 2, + }; + + /** + * @brief Test the behavior of the mouse on the target widget + * @param id widget id + * @param area widget rect + * @param repeat + * @note If the left mouse button is clicked, the target widget is selected + */ + [[nodiscard]] auto test_mouse(widget_id_type id, const rect_type& area, bool repeat = false) noexcept -> std::underlying_type_t; + + /** + * @brief Test the behavior of the mouse on the target widget + * @param id widget id + * @param area widget rect + * @note Even if the left mouse button is clicked, the target widget will not be selected, + * this applies to some widget that do not want to be selected + */ + [[nodiscard]] auto queue_mouse(widget_id_type id, const rect_type& area) noexcept -> std::underlying_type_t; + + // ---------------------------------------------------------------------- + // WINDOW + + private: + /** + * @brief Finds the window with the given name + * @return Returns the corresponding window if it exists, otherwise it returns a null pointer + * @note The naming convention for child windows is parent_name.child_name + */ + [[nodiscard]] auto find_window(std::string_view name) noexcept -> window_type*; + + /** + * @brief If the target window does not exist, then create it, otherwise return the corresponding window + * @param name window name + * @param size window size (used only when creating window) + * @param flag window flag + */ + [[nodiscard]] auto find_or_create_window(std::string_view name, const extent_type& size, internal::WindowFlag flag) noexcept -> window_type&; + + public: + /** + * @brief Find the last window from the available windows in this frame + */ + [[nodiscard]] auto current_window() const noexcept -> window_type&; + + /** + * @brief Find the last parent window from the available windows in this frame + */ + [[nodiscard]] auto current_parent_window() const noexcept -> window_type&; + + /** + * @brief Find the last possible root (not child) window from the available windows in this frame + * @return If it is not found (if and only if there are no currently available windows, i.e. the window to be created is the first one), then the null pointer is returned + */ + [[nodiscard]] auto current_root_window() noexcept -> window_type*; + + auto set_next_window_point(const point_type& point) noexcept -> void; + + auto begin_window( + std::string_view name, + const extent_type& size, + Theme::value_type background_fill_alpha, + internal::WindowFlag flag + ) noexcept -> bool; + auto end_window() noexcept -> void; + + auto begin_child_window( + std::string_view name, + const extent_type& size, + Theme::alpha_type background_fill_alpha, + internal::WindowFlag flag, + bool border + ) noexcept -> void; + auto end_child_window() noexcept -> void; + + // ---------------------------------------------------------------------- + // WINDOW STATE + + /** + * @brief Test that the mouse is hovering over the target window + */ + [[nodiscard]] auto is_window_hovered(const window_type& window) const noexcept -> bool; + + /** + * @brief Find the first (more top-level) window that contains the location of the given point + */ + [[nodiscard]] auto find_hovered_window(const point_type& position, bool excludes_children) noexcept -> window_type*; + + /** + * @brief Focus on the target window (this determines the order in which the windows are drawn) + */ + auto focus_window(window_type& window) noexcept -> void; + + // ---------------------------------------------------------------------- + // WIDGET STATE + + // Whether the mouse is hovering over the target widget + [[nodiscard]] auto is_widget_hovered(widget_id_type id) const noexcept -> bool; + // Whether the mouse is hovering over the widget + [[nodiscard]] auto is_any_widget_hovered() const noexcept -> bool; + // Whether the mouse is selecting over the target widget + [[nodiscard]] auto is_widget_activated(widget_id_type id) const noexcept -> bool; + // Whether the mouse is selecting over the widget + [[nodiscard]] auto is_any_widget_activated() const noexcept -> bool; + + // Marks the current widget alive, returns whether it is currently selected (activated) or not + auto mark_widget_alive(widget_id_type id) noexcept -> bool; + // Marks that no widget is currently selected (activated) + auto mark_widget_dead(widget_id_type id = internal::invalid_widget_id) noexcept -> void; + + // Activate the specified combo widget (window) + auto mark_combo_alive(widget_id_type id) noexcept -> void; + // Deactivate the specified combo widget (window) + auto mark_combo_dead(widget_id_type id = internal::invalid_widget_id) noexcept -> void; + // Whether the target combo widget is active or not + [[nodiscard]] auto is_combo_activated(widget_id_type id) const noexcept -> bool; + + // ---------------------------------------------------------------------- + // RENDER + + [[nodiscard]] auto current_time() const noexcept -> time_type; + + [[nodiscard]] auto current_frame() const noexcept -> internal::frame_count_type; + + auto new_frame() noexcept -> void; + + auto end_frame() noexcept -> void; + + auto render() noexcept -> void; + + [[nodiscard]] auto get_draw_data() noexcept -> std::vector; + + // ---------------------------------------------------------------------- + // TEST + + static auto show_theme_editor() noexcept -> bool; + }; + } + + template<> + struct meta::user_defined::enum_is_flag : std::true_type {}; +} diff --git a/src/gui/internal/draw_list.cpp b/src/gui/internal/draw_list.cpp new file mode 100644 index 00000000..e599b4ce --- /dev/null +++ b/src/gui/internal/draw_list.cpp @@ -0,0 +1,1971 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +#include + +#include +#include + +#include +#include +#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE + +namespace +{ + using namespace gal::prometheus; + using namespace gui; + using namespace internal; + + [[nodiscard]] constexpr auto to_fixed_rect_corner_flag(const DrawFlag flag) noexcept -> DrawFlag + { + using enum DrawFlag; + + if ((flag & ROUND_CORNER_MASK) == NONE) + { + return ROUND_CORNER_ALL | flag; + } + + return flag; + } + + [[nodiscard]] constexpr auto to_fixed_normal(const float x, const float y) noexcept -> std::pair + { + if (const auto d = math::pow(x, 2) + math::pow(y, 2); + d > 1e-6f) + { + // fixme + const auto inv_len = [d] + { + // #if defined(__AVX512F__) + // __m512 d_v = _mm512_set1_ps(d); + // __m512 inv_len_v = _mm512_rcp14_ps(d_v); + // return _mm512_cvtss_f32(inv_len_v); + // #elif defined(__AVX__) + // __m256 d_v = _mm256_set1_ps(d); + // __m256 inv_len_v = _mm256_rcp_ps(d_v); + // return _mm256_cvtss_f32(inv_len_v); + // #elif defined(__SSE4_1__) or defined(__SSE3__) or defined(__SSE__) + // __m128 d_v = _mm_set_ss(d); + // __m128 inv_len_v = _mm_rcp_ss(d_v); + // return _mm_cvtss_f32(inv_len_v); + // #else + return 1.0f / d; + // #endif + }(); + + return {x * inv_len, y * inv_len}; + } + + return {x, y}; + } + + // fixme + constexpr std::size_t bezier_curve_casteljau_max_level = 10; + + constexpr auto bezier_cubic_calc = [](const point_type& p1, const point_type& p2, const point_type& p3, const point_type& p4, const float tolerance) noexcept -> point_type + { + const auto u = 1.f - tolerance; + + const auto w1 = math::pow(u, 3); + const auto w2 = 3 * math::pow(u, 2) * tolerance; + const auto w3 = 3 * u * math::pow(tolerance, 2); + const auto w4 = math::pow(tolerance, 3); + + return + { + p1.x * w1 + p2.x * w2 + p3.x * w3 + p4.x * w4, + p1.y * w1 + p2.y * w2 + p3.y * w3 + p4.y * w4 + }; + }; + + constexpr auto bezier_quadratic_calc = [](const point_type& p1, const point_type& p2, const point_type& p3, const float tolerance) noexcept -> point_type + { + const auto u = 1.f - tolerance; + + const auto w1 = math::pow(u, 2); + const auto w2 = 2 * u * tolerance; + const auto w3 = math::pow(tolerance, 2); + + return + { + p1.x * w1 + p2.x * w2 + p3.x * w3, + p1.y * w1 + p2.y * w2 + p3.y * w3 + }; + }; +} + +namespace gal::prometheus::gui::internal +{ + auto range_of_arc(const DrawArcFlag flag) noexcept -> std::pair + { + static_assert(DrawListSharedData::vertex_sample_points_count % 12 == 0); + constexpr auto factor = static_cast(DrawListSharedData::vertex_sample_points_count / 12); + + switch (flag) + { + case DrawArcFlag::Q1: { return std::make_pair(0 * factor, 3 * factor); } + case DrawArcFlag::Q2: { return std::make_pair(3 * factor, 6 * factor); } + case DrawArcFlag::Q3: { return std::make_pair(6 * factor, 9 * factor); } + case DrawArcFlag::Q4: { return std::make_pair(9 * factor, 12 * factor); } + case DrawArcFlag::TOP: { return std::make_pair(0 * factor, 6 * factor); } + case DrawArcFlag::BOTTOM: { return std::make_pair(6 * factor, 12 * factor); } + case DrawArcFlag::LEFT: { return std::make_pair(3 * factor, 9 * factor); } + case DrawArcFlag::RIGHT: { return std::make_pair(9 * factor, 15 * factor); } + case DrawArcFlag::ALL: { return std::make_pair(0 * factor, 12 * factor); } + case DrawArcFlag::Q1_CLOCK_WISH: { return std::make_pair(3 * factor, 0 * factor); } + case DrawArcFlag::Q2_CLOCK_WISH: { return std::make_pair(6 * factor, 3 * factor); } + case DrawArcFlag::Q3_CLOCK_WISH: { return std::make_pair(9 * factor, 6 * factor); } + case DrawArcFlag::Q4_CLOCK_WISH: { return std::make_pair(12 * factor, 9 * factor); } + case DrawArcFlag::TOP_CLOCK_WISH: { return std::make_pair(6 * factor, 0 * factor); } + case DrawArcFlag::BOTTOM_CLOCK_WISH: { return std::make_pair(12 * factor, 6 * factor); } + case DrawArcFlag::LEFT_CLOCK_WISH: { return std::make_pair(9 * factor, 3 * factor); } + case DrawArcFlag::RIGHT_CLOCK_WISH: { return std::make_pair(15 * factor, 9 * factor); } + case DrawArcFlag::ALL_CLOCK_WISH: { return std::make_pair(12 * factor, 0 * factor); } + default: { GAL_PROMETHEUS_ERROR_UNREACHABLE(); } // NOLINT(clang-diagnostic-covered-switch-default) + } + } + + class DrawList::Drawer + { + public: + memory::RefWrapper self; + + using path_list_type = std::vector; + + path_list_type path_list; + + [[nodiscard]] auto make_appender() noexcept -> PrimitiveAppender + { + auto& draw_list = self.get(); + + return {draw_list.command_list_.back(), draw_list.vertex_list_, draw_list.index_list_}; + } + + auto draw_polygon_line(const color_type color, const DrawFlag draw_flag, const float thickness) noexcept -> void + { + const auto path_point_count = path_list.size(); + const auto& path_point = path_list; + + if (path_point_count < 2 or color.alpha == 0) + { + return; + } + + const auto& draw_list = self.get(); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(draw_list.context_ != nullptr); + const auto& font = draw_list.context_->current_font(); + auto appender = make_appender(); + + const auto is_closed = (draw_flag & DrawFlag::CLOSED) != DrawFlag::NONE; + const auto segments_count = is_closed ? path_point_count : path_point_count - 1; + + const auto vertex_count = segments_count * 4; + const auto index_count = segments_count * 6; + appender.reserve(vertex_count, index_count); + + for (std::decay_t i = 0; i < segments_count; ++i) + { + const auto n = (i + 1) % path_point_count; + + const auto& p1 = path_point[i]; + const auto& p2 = path_point[n]; + + auto [normalized_x, normalized_y] = math::normalize(p2.x - p1.x, p2.y - p1.y); + normalized_x *= (thickness * .5f); + normalized_y *= (thickness * .5f); + + const auto current_vertex_index = static_cast(appender.vertex_count()); + const auto& opaque_uv = font.white_pixel_uv; + + appender.add_vertex(p1 + point_type{normalized_y, -normalized_x}, opaque_uv, color); + appender.add_vertex(p2 + point_type{normalized_y, -normalized_x}, opaque_uv, color); + appender.add_vertex(p2 + point_type{-normalized_y, normalized_x}, opaque_uv, color); + appender.add_vertex(p1 + point_type{-normalized_y, normalized_x}, opaque_uv, color); + + appender.add_index(current_vertex_index + 0, current_vertex_index + 1, current_vertex_index + 2); + appender.add_index(current_vertex_index + 0, current_vertex_index + 2, current_vertex_index + 3); + } + } + + auto draw_polygon_line_aa(const color_type color, const DrawFlag draw_flag, float thickness) noexcept -> void + { + const auto path_point_count = path_list.size(); + const auto& path_point = path_list; + + if (path_point_count < 2 or color.alpha == 0) + { + return; + } + + const auto& draw_list = self.get(); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(draw_list.context_ != nullptr); + const auto& font = draw_list.context_->current_font(); + const auto draw_list_flag = self.get().draw_list_flag_; + auto appender = make_appender(); + + const auto& opaque_uv = font.white_pixel_uv; + const auto transparent_color = color.transparent(); + + const auto is_closed = (draw_flag & DrawFlag::CLOSED) != DrawFlag::NONE; + const auto segments_count = is_closed ? path_point_count : path_point_count - 1; + const auto is_thick_line = thickness > 1.f; + + thickness = std::ranges::max(thickness, 1.f); + const auto thickness_integer = static_cast(thickness); + const auto thickness_fractional = thickness - static_cast(thickness_integer); + + const auto is_use_texture = + ( + (draw_list_flag & DrawListFlag::ANTI_ALIASED_LINE_USE_TEXTURE) == DrawListFlag::ANTI_ALIASED_LINE_USE_TEXTURE and + (thickness_integer < font.baked_line_max_width) and + (thickness_fractional <= .00001f) + ); + + const auto vertex_cont = is_use_texture ? (path_point_count * 2) : (is_thick_line ? path_point_count * 4 : path_point_count * 3); + const auto index_count = is_use_texture ? (segments_count * 6) : (is_thick_line ? segments_count * 18 : segments_count * 12); + appender.reserve(vertex_cont, index_count); + + // The first items are normals at each line point, then after that there are either 2 or 4 temp points for each line point + path_list_type temp_buffer{}; + temp_buffer.resize(path_point_count * ((is_use_texture or not is_thick_line) ? 3 : 5)); + auto temp_buffer_normals = std::span{temp_buffer.begin(), path_point_count}; + auto temp_buffer_points = std::span{temp_buffer.begin() + static_cast(path_point_count), temp_buffer.end()}; + + // Calculate normals (tangents) for each line segment + for (std::decay_t i = 0; i < segments_count; ++i) + { + const auto n = (i + 1) % path_point_count; + const auto d = path_point[n] - path_point[i]; + + const auto [normalized_x, normalized_y] = math::normalize(d.x, d.y); + temp_buffer_normals[i].x = normalized_y; + temp_buffer_normals[i].y = -normalized_x; + } + + if (not is_closed) + { + temp_buffer_normals[temp_buffer_normals.size() - 1] = temp_buffer_normals[temp_buffer_normals.size() - 2]; + } + + // If we are drawing a one-pixel-wide line without a texture, or a textured line of any width, we only need 2 or 3 vertices per point + if (is_use_texture or not is_thick_line) + { + // [PATH 1] Texture-based lines (thick or non-thick) + + // The width of the geometry we need to draw - this is essentially pixels for the line itself, plus "one pixel" for AA + const auto half_draw_size = is_use_texture ? ((thickness * .5f) + 1.f) : 1.f; + + // If line is not closed, the first and last points need to be generated differently as there are no normals to blend + if (not is_closed) + { + temp_buffer_points[0] = path_point[0] + temp_buffer_normals[0] * half_draw_size; + temp_buffer_points[1] = path_point[0] - temp_buffer_normals[0] * half_draw_size; + temp_buffer_points[(path_point_count - 1) * 2 + 0] = path_point[path_point_count - 1] + temp_buffer_normals[path_point_count - 1] * half_draw_size; + temp_buffer_points[(path_point_count - 1) * 2 + 1] = path_point[path_point_count - 1] - temp_buffer_normals[path_point_count - 1] * half_draw_size; + } + + const auto current_vertex_index = static_cast(appender.vertex_count()); + + // Generate the indices to form a number of triangles for each line segment, and the vertices for the line edges + // This takes points n and n+1 and writes into n+1, with the first point in a closed line being generated from the final one (as n+1 wraps) + auto vertex_index_for_start = current_vertex_index; + for (std::decay_t first_point_of_segment = 0; first_point_of_segment < segments_count; ++first_point_of_segment) + { + const auto second_point_of_segment = (first_point_of_segment + 1) % path_point_count; + const auto vertex_index_for_end = static_cast( + // closed + (first_point_of_segment + 1) == path_point_count + ? current_vertex_index + : (vertex_index_for_start + (is_use_texture ? 2 : 3)) + ); + + // Average normals + const auto d = (temp_buffer_normals[first_point_of_segment] + temp_buffer_normals[second_point_of_segment]) * .5f; + // dm_x, dm_y are offset to the outer edge of the AA area + auto [dm_x, dm_y] = to_fixed_normal(d.x, d.y); + dm_x *= half_draw_size; + dm_y *= half_draw_size; + + // Add temporary vertexes for the outer edges + temp_buffer_points[second_point_of_segment * 2 + 0] = path_point[second_point_of_segment] + point_type{dm_x, dm_y}; + temp_buffer_points[second_point_of_segment * 2 + 1] = path_point[second_point_of_segment] - point_type{dm_x, dm_y}; + + if (is_use_texture) + { + // Add indices for two triangles + + // right + appender.add_index(vertex_index_for_end + 0, vertex_index_for_start + 0, vertex_index_for_start + 1); + // left + appender.add_index(vertex_index_for_end + 1, vertex_index_for_start + 1, vertex_index_for_end + 0); + } + else + { + // Add indexes for four triangles + + // right 1 + appender.add_index(vertex_index_for_end + 0, vertex_index_for_start + 0, vertex_index_for_start + 2); + // right 2 + appender.add_index(vertex_index_for_start + 2, vertex_index_for_end + 2, vertex_index_for_end + 0); + // left 1 + appender.add_index(vertex_index_for_end + 1, vertex_index_for_start + 1, vertex_index_for_start + 0); + // left 2 + appender.add_index(vertex_index_for_start + 0, vertex_index_for_end + 0, vertex_index_for_end + 1); + } + + vertex_index_for_start = vertex_index_for_end; + } + + // Add vertexes for each point on the line + if (is_use_texture) + { + GAL_PROMETHEUS_ERROR_ASSUME(not font.baked_line_uv.empty(), "draw::FontAtlasFlag::NO_BAKED_LINE"); + + const auto& uv = font.baked_line_uv[thickness_integer]; + + const auto uv0 = uv.left_top(); + const auto uv1 = uv.right_bottom(); + for (std::decay_t i = 0; i < path_point_count; ++i) + { + // left-side outer edge + appender.add_vertex(temp_buffer_points[i * 2 + 0], uv0, color); + // right-side outer edge + appender.add_vertex(temp_buffer_points[i * 2 + 1], uv1, color); + } + } + else + { + // If we're not using a texture, we need the center vertex as well + for (std::decay_t i = 0; i < path_point_count; ++i) + { + // center of line + appender.add_vertex(path_point[i], opaque_uv, color); + // left-side outer edge + appender.add_vertex(temp_buffer_points[i * 2 + 0], opaque_uv, transparent_color); + // right-side outer edge + appender.add_vertex(temp_buffer_points[i * 2 + 1], opaque_uv, transparent_color); + } + } + } + else + { + // [PATH 2] Non-texture-based lines (non-thick) + + // we need to draw the solid line core and thus require four vertices per point + const auto half_inner_thickness = (thickness - 1.f) * .5f; + + // If line is not closed, the first and last points need to be generated differently as there are no normals to blend + if (not is_closed) + { + const auto point_last = path_point_count - 1; + temp_buffer_points[0] = path_point[0] + temp_buffer_normals[0] * (half_inner_thickness + 1.f); + temp_buffer_points[1] = path_point[0] + temp_buffer_normals[0] * (half_inner_thickness + 0.f); + temp_buffer_points[2] = path_point[0] - temp_buffer_normals[0] * (half_inner_thickness + 0.f); + temp_buffer_points[3] = path_point[0] - temp_buffer_normals[0] * (half_inner_thickness + 1.f); + temp_buffer_points[point_last * 4 + 0] = path_point[point_last] + temp_buffer_normals[point_last] * (half_inner_thickness + 1.f); + temp_buffer_points[point_last * 4 + 1] = path_point[point_last] + temp_buffer_normals[point_last] * (half_inner_thickness + 0.f); + temp_buffer_points[point_last * 4 + 2] = path_point[point_last] - temp_buffer_normals[point_last] * (half_inner_thickness + 0.f); + temp_buffer_points[point_last * 4 + 3] = path_point[point_last] - temp_buffer_normals[point_last] * (half_inner_thickness + 1.f); + } + + const auto current_vertex_index = static_cast(appender.vertex_count()); + + // Generate the indices to form a number of triangles for each line segment, and the vertices for the line edges + // This takes points n and n+1 and writes into n+1, with the first point in a closed line being generated from the final one (as n+1 wraps) + auto vertex_index_for_start = current_vertex_index; + for (std::decay_t first_point_of_segment = 0; first_point_of_segment < segments_count; ++first_point_of_segment) + { + const auto second_point_of_segment = (first_point_of_segment + 1) % path_point_count; + const auto vertex_index_for_end = static_cast( + (first_point_of_segment + 1) == path_point_count + ? current_vertex_index + : (vertex_index_for_start + 4) + ); + + // Average normals + const auto d = (temp_buffer_normals[first_point_of_segment] + temp_buffer_normals[second_point_of_segment]) * .5f; + const auto [dm_x, dm_y] = to_fixed_normal(d.x, d.y); + const auto dm_out_x = dm_x * (half_inner_thickness + 1.f); + const auto dm_out_y = dm_y * (half_inner_thickness + 1.f); + const auto dm_in_x = dm_x * (half_inner_thickness + 0.f); + const auto dm_in_y = dm_y * (half_inner_thickness + 0.f); + + // Add temporary vertices + temp_buffer_points[second_point_of_segment * 4 + 0] = path_point[second_point_of_segment] + point_type{dm_out_x, dm_out_y}; + temp_buffer_points[second_point_of_segment * 4 + 1] = path_point[second_point_of_segment] + point_type{dm_in_x, dm_in_y}; + temp_buffer_points[second_point_of_segment * 4 + 2] = path_point[second_point_of_segment] - point_type{dm_in_x, dm_in_y}; + temp_buffer_points[second_point_of_segment * 4 + 3] = path_point[second_point_of_segment] - point_type{dm_out_x, dm_out_y}; + + // Add indexes + appender.add_index(vertex_index_for_end + 1, vertex_index_for_end + 1, vertex_index_for_start + 2); + appender.add_index(vertex_index_for_start + 2, vertex_index_for_end + 2, vertex_index_for_end + 1); + appender.add_index(vertex_index_for_end + 1, vertex_index_for_start + 1, vertex_index_for_start + 0); + appender.add_index(vertex_index_for_start + 0, vertex_index_for_end + 0, vertex_index_for_end + 1); + appender.add_index(vertex_index_for_end + 2, vertex_index_for_start + 2, vertex_index_for_start + 3); + appender.add_index(vertex_index_for_start + 3, vertex_index_for_end + 3, vertex_index_for_end + 2); + + vertex_index_for_start = vertex_index_for_end; + } + + // Add vertices + for (std::decay_t i = 0; i < path_point_count; ++i) + { + appender.add_vertex(temp_buffer_points[i * 4 + 0], opaque_uv, transparent_color); + appender.add_vertex(temp_buffer_points[i * 4 + 1], opaque_uv, color); + appender.add_vertex(temp_buffer_points[i * 4 + 2], opaque_uv, color); + appender.add_vertex(temp_buffer_points[i * 4 + 2], opaque_uv, transparent_color); + } + } + } + + auto draw_convex_polygon_line_filled(const color_type color) noexcept -> void + { + const auto path_point_count = path_list.size(); + const auto& path_point = path_list; + + if (path_point_count < 3 or color.alpha == 0) + { + return; + } + + const auto& draw_list = self.get(); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(draw_list.context_ != nullptr); + const auto& font = draw_list.context_->current_font(); + auto appender = make_appender(); + + const auto vertex_count = path_point_count; + const auto index_count = (path_point_count - 2) * 3; + appender.reserve(vertex_count, index_count); + + const auto current_vertex_index = static_cast(appender.vertex_count()); + const auto& opaque_uv = font.white_pixel_uv; + + std::ranges::for_each( + path_point, + [&](const point_type& point) noexcept -> void + { + appender.add_vertex(point, opaque_uv, color); + } + ); + for (index_type i = 2; std::cmp_less(i, path_point_count); ++i) + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_index + i - 1 >= current_vertex_index); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_index + i >= current_vertex_index); + + appender.add_index(current_vertex_index + 0, current_vertex_index + i - 1, current_vertex_index + i); + } + } + + auto draw_convex_polygon_line_filled_aa(const color_type color) noexcept -> void + { + const auto path_point_count = path_list.size(); + const auto& path_point = path_list; + + if (path_point_count < 3 or color.alpha == 0) + { + return; + } + + const auto& draw_list = self.get(); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(draw_list.context_ != nullptr); + const auto& font = draw_list.context_->current_font(); + auto appender = make_appender(); + + const auto& opaque_uv = font.white_pixel_uv; + const auto transparent_color = color.transparent(); + + const auto vertex_count = path_point_count * 2; + const auto index_count = (path_point_count - 2) * 3 + path_point_count * 6; + appender.reserve(vertex_count, index_count); + + const auto current_vertex_inner_index = static_cast(appender.vertex_count()); + const auto current_vertex_outer_index = static_cast(appender.vertex_count() + 1); + + // Add indexes for fill + for (index_type i = 2; std::cmp_less(i, path_point_count); ++i) + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_inner_index + static_cast((i - 1) << 1) >= current_vertex_inner_index); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_inner_index + static_cast(i << 1) >= current_vertex_inner_index); + + appender.add_index( + current_vertex_inner_index + 0, + current_vertex_inner_index + static_cast((i - 1) << 1), + current_vertex_inner_index + static_cast(i << 1) + ); + } + + path_list_type temp_buffer{}; + temp_buffer.resize(path_point_count); + auto temp_buffer_normals = std::span{temp_buffer.begin(), path_point_count}; + + for (auto i = path_point_count - 1, n = static_cast(0); n < path_point_count; i = n++) + { + const auto d = path_point[n] - path_point[i]; + + const auto [normalized_x, normalized_y] = math::normalize(d.x, d.y); + temp_buffer_normals[i].x = normalized_y; + temp_buffer_normals[i].y = -normalized_x; + } + for (auto i = path_point_count - 1, n = static_cast(0); n < path_point_count; i = n++) + { + // Average normals + const auto d = (temp_buffer_normals[n] + temp_buffer_normals[i]) * .5f; + auto [dm_x, dm_y] = to_fixed_normal(d.x, d.y); + dm_x *= .5f; + dm_y *= .5f; + + // inner + appender.add_vertex(path_point[n] - point_type{dm_x, dm_y}, opaque_uv, color); + // outer + appender.add_vertex(path_point[n] + point_type{dm_x, dm_y}, opaque_uv, transparent_color); + + // Add indexes for fringes + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_inner_index + static_cast(n << 1) >= current_vertex_inner_index); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_inner_index + static_cast(i << 1) >= current_vertex_inner_index); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_outer_index + static_cast(i << 1) >= current_vertex_outer_index); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_outer_index + static_cast(i << 1) >= current_vertex_outer_index); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_outer_index + static_cast(n << 1) >= current_vertex_outer_index); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(current_vertex_inner_index + static_cast(n << 1) >= current_vertex_inner_index); + + appender.add_index( + current_vertex_inner_index + static_cast(n << 1), + current_vertex_inner_index + static_cast(i << 1), + current_vertex_outer_index + static_cast(i << 1) + ); + appender.add_index( + current_vertex_outer_index + static_cast(i << 1), + current_vertex_outer_index + static_cast(n << 1), + current_vertex_inner_index + static_cast(n << 1) + ); + } + } + + auto draw_rect_filled( + const rect_type& rect, + const color_type color_left_top, + const color_type color_right_top, + const color_type color_left_bottom, + const color_type color_right_bottom + ) noexcept -> void + { + const auto& draw_list = self.get(); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(draw_list.context_ != nullptr); + const auto& font = draw_list.context_->current_font(); + auto appender = make_appender(); + + // two triangle without path + constexpr size_type vertex_count = 4; + constexpr size_type index_count = 6; + appender.reserve(vertex_count, index_count); + + const auto& opaque_uv = font.white_pixel_uv; + + const auto current_vertex_index = static_cast(appender.vertex_count()); + + appender.add_vertex(rect.left_top(), opaque_uv, color_left_top); + appender.add_vertex(rect.right_top(), opaque_uv, color_right_top); + appender.add_vertex(rect.right_bottom(), opaque_uv, color_right_bottom); + appender.add_vertex(rect.left_bottom(), opaque_uv, color_left_bottom); + + appender.add_index(current_vertex_index + 0, current_vertex_index + 1, current_vertex_index + 2); + appender.add_index(current_vertex_index + 0, current_vertex_index + 2, current_vertex_index + 3); + } + + auto draw_text( + const Font& font, + const float font_size, + const point_type& p, + const color_type color, + const std::string_view utf8_text, + const float wrap_width + ) noexcept -> void + { + auto& draw_list = self.get(); + + const auto new_texture = draw_list.this_command_texture_id_ != font.texture_id; + + if (new_texture) + { + draw_list.push_texture_id(font.texture_id); + } + + text_draw(font, utf8_text, font_size, wrap_width, p, color, make_appender()); + + if (new_texture) + { + draw_list.pop_texture_id(); + } + } + + auto draw_image( + const texture_id_type texture_id, + const point_type& display_p1, + const point_type& display_p2, + const point_type& display_p3, + const point_type& display_p4, + const uv_type& uv_p1, + const uv_type& uv_p2, + const uv_type& uv_p3, + const uv_type& uv_p4, + const color_type color + ) noexcept -> void + { + auto& draw_list = self.get(); + + const auto new_texture = draw_list.this_command_texture_id_ != texture_id; + + if (new_texture) + { + draw_list.push_texture_id(texture_id); + } + + auto appender = make_appender(); + + // two triangle without path + constexpr size_type vertex_count = 4; + constexpr size_type index_count = 6; + appender.reserve(vertex_count, index_count); + + const auto current_vertex_index = static_cast(appender.vertex_count()); + + appender.add_vertex(display_p1, uv_p1, color); + appender.add_vertex(display_p2, uv_p2, color); + appender.add_vertex(display_p3, uv_p3, color); + appender.add_vertex(display_p4, uv_p4, color); + + appender.add_index(current_vertex_index + 0, current_vertex_index + 1, current_vertex_index + 2); + appender.add_index(current_vertex_index + 0, current_vertex_index + 2, current_vertex_index + 3); + + if (new_texture) + { + draw_list.pop_texture_id(); + } + } + + auto draw_image_rounded( + const texture_id_type texture_id, + const rect_type& display_rect, + const rect_type& uv_rect, + const color_type color, + float rounding, + DrawFlag flag + ) noexcept -> void + { + // @see `path_rect` + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(display_rect.valid() and not display_rect.empty()); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(uv_rect.valid() and not uv_rect.empty()); + + if (rounding >= .5f) + { + flag = to_fixed_rect_corner_flag(flag); + + const auto v = + (flag & DrawFlag::ROUND_CORNER_TOP) == DrawFlag::ROUND_CORNER_TOP or + (flag & DrawFlag::ROUND_CORNER_BOTTOM) == DrawFlag::ROUND_CORNER_BOTTOM; + const auto h = + (flag & DrawFlag::ROUND_CORNER_LEFT) == DrawFlag::ROUND_CORNER_LEFT or + (flag & DrawFlag::ROUND_CORNER_RIGHT) == DrawFlag::ROUND_CORNER_RIGHT; + + rounding = std::ranges::min(rounding, display_rect.width() * (v ? .5f : 1.f) - 1.f); + rounding = std::ranges::min(rounding, display_rect.height() * (h ? .5f : 1.f) - 1.f); + } + + if (rounding < .5f or (DrawFlag::ROUND_CORNER_MASK & flag) == DrawFlag::ROUND_CORNER_NONE) + { + draw_image( + texture_id, + display_rect.left_top(), + display_rect.right_top(), + display_rect.right_bottom(), + display_rect.left_bottom(), + uv_rect.left_top(), + uv_rect.right_top(), + uv_rect.right_bottom(), + uv_rect.left_bottom(), + color + ); + } + else + { + auto& draw_list = self.get(); + + const auto new_texture = draw_list.this_command_texture_id_ != texture_id; + + if (new_texture) + { + draw_list.push_texture_id(texture_id); + } + + const auto rounding_left_top = (flag & DrawFlag::ROUND_CORNER_LEFT_TOP) != DrawFlag::NONE ? rounding : 0; + const auto rounding_right_top = (flag & DrawFlag::ROUND_CORNER_RIGHT_TOP) != DrawFlag::NONE ? rounding : 0; + const auto rounding_left_bottom = (flag & DrawFlag::ROUND_CORNER_LEFT_BOTTOM) != DrawFlag::NONE ? rounding : 0; + const auto rounding_right_bottom = (flag & DrawFlag::ROUND_CORNER_RIGHT_BOTTOM) != DrawFlag::NONE ? rounding : 0; + + path_arc_fast({display_rect.left_top() + point_type{rounding_left_top, rounding_left_top}, rounding_left_top}, DrawArcFlag::Q2_CLOCK_WISH); + path_arc_fast({display_rect.right_top() + point_type{-rounding_right_top, rounding_right_top}, rounding_right_top}, DrawArcFlag::Q1_CLOCK_WISH); + path_arc_fast({display_rect.right_bottom() + point_type{-rounding_right_bottom, -rounding_right_bottom}, rounding_right_bottom}, DrawArcFlag::Q4_CLOCK_WISH); + path_arc_fast({display_rect.left_bottom() + point_type{rounding_left_bottom, -rounding_left_bottom}, rounding_left_bottom}, DrawArcFlag::Q3_CLOCK_WISH); + + const auto before_vertex_count = draw_list.vertex_list_.size(); + // draw + path_stroke(color); + const auto after_vertex_count = draw_list.vertex_list_.size(); + + // set uv manually + + const auto display_size = display_rect.size(); + const auto uv_size = uv_rect.size(); + const auto scale = uv_size / display_size; + + auto it = draw_list.vertex_list_.begin() + static_cast(before_vertex_count); + const auto end = draw_list.vertex_list_.begin() + static_cast(after_vertex_count); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(it < end); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(end == draw_list.vertex_list_.end()); + + // note: linear uv + const auto uv_min = uv_rect.left_top(); + // const auto uv_max = uv_rect.right_bottom(); + while (it != end) + { + const auto v = uv_min + (it->position - display_rect.left_top()) * scale; + + it->uv = + { + // std::ranges::clamp(v.x, uv_min.x, uv_max.x), + v.x, + // std::ranges::clamp(v.y, uv_min.y, uv_max.y) + v.y + }; + it += 1; + } + + if (new_texture) + { + draw_list.pop_texture_id(); + } + } + } + + auto path_clear() noexcept -> void + { + path_list.clear(); + } + + auto path_reserve(const std::size_t size) noexcept -> void + { + path_list.reserve(size); + } + + auto path_reserve_extra(const std::size_t size) noexcept -> void + { + path_reserve(path_list.size() + size); + } + + auto path_pin(const point_type& point) noexcept -> void + { + path_list.push_back(point); + } + + auto path_stroke(const color_type color, const DrawFlag flag, const float thickness) noexcept -> void + { + if (const auto draw_list_flag = self.get().draw_list_flag_; + (draw_list_flag & DrawListFlag::ANTI_ALIASED_LINE) != DrawListFlag::NONE) + { + draw_polygon_line_aa(color, flag, thickness); + } + else + { + draw_polygon_line(color, flag, thickness); + } + + path_clear(); + } + + auto path_stroke(const color_type color) noexcept -> void + { + if (const auto draw_list_flag = self.get().draw_list_flag_; + (draw_list_flag & DrawListFlag::ANTI_ALIASED_FILL) != DrawListFlag::NONE) + { + draw_convex_polygon_line_filled_aa(color); + } + else + { + draw_convex_polygon_line_filled(color); + } + + path_clear(); + } + + auto path_arc_fast(const circle_type& circle, const int from, const int to) noexcept -> void + { + const auto& [center, radius] = circle; + + if (radius < .5f) + { + path_pin(center); + return; + } + + const auto& draw_list = self.get(); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(draw_list.context_ != nullptr); + const auto& draw_list_shared_data = draw_list.context_->current_draw_list_shared_data(); + + // Calculate arc auto segment step size + auto step = DrawListSharedData::vertex_sample_points_count / draw_list_shared_data.circle_auto_segment_count(radius); + // Make sure we never do steps larger than one quarter of the circle + step = std::clamp(step, static_cast(1), DrawListSharedData::vertex_sample_points_count / 4); + + const auto sample_range = math::abs(to - from); + const auto next_step = step; + + auto extra_max_sample = false; + if (step > 1) + { + const auto overstep = sample_range % step; + if (overstep > 0) + { + extra_max_sample = true; + + // When we have overstepped to avoid awkwardly looking one long line and one tiny one at the end, + // distribute first step range evenly between them by reducing first step size. + step -= (step - overstep) / 2; + } + + path_reserve_extra(sample_range / step + 1 + (overstep > 0)); + } + else + { + path_reserve_extra(sample_range + 1); + } + + auto sample_index = from; + if (sample_index < 0 or std::cmp_greater_equal(sample_index, DrawListSharedData::vertex_sample_points_count)) + { + sample_index = sample_index % static_cast(DrawListSharedData::vertex_sample_points_count); + if (sample_index < 0) + { + sample_index += static_cast(DrawListSharedData::vertex_sample_points_count); + } + } + + if (to >= from) + { + for (int i = from; i <= to; i += static_cast(step), sample_index += static_cast(step), step = next_step) + { + // a_step is clamped to vertex_sample_points_count, so we have guaranteed that it will not wrap over range twice or more + if (std::cmp_greater_equal(sample_index, DrawListSharedData::vertex_sample_points_count)) + { + sample_index -= static_cast(DrawListSharedData::vertex_sample_points_count); + } + + const auto& sample_point = draw_list_shared_data.vertex_sample_point(sample_index); + + path_pin({center + sample_point * radius}); + } + } + else + { + for (int i = from; i >= to; i -= static_cast(step), sample_index -= static_cast(step), step = next_step) + { + // a_step is clamped to vertex_sample_points_count, so we have guaranteed that it will not wrap over range twice or more + if (sample_index < 0) + { + sample_index += static_cast(DrawListSharedData::vertex_sample_points_count); + } + + const auto& sample_point = draw_list_shared_data.vertex_sample_point(sample_index); + + path_pin({center + sample_point * radius}); + } + } + + if (extra_max_sample) + { + auto normalized_max_sample_index = to % static_cast(DrawListSharedData::vertex_sample_points_count); + if (normalized_max_sample_index < 0) + { + normalized_max_sample_index += DrawListSharedData::vertex_sample_points_count; + } + + const auto& sample_point = draw_list_shared_data.vertex_sample_point(normalized_max_sample_index); + + path_pin({center + sample_point * radius}); + } + } + + auto path_arc_fast(const circle_type& circle, const DrawArcFlag flag) noexcept -> void + { + const auto [from, to] = range_of_arc(flag); + + return path_arc_fast(circle, from, to); + } + + auto path_arc_n(const circle_type& circle, const float from, const float to, const std::uint32_t segments) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(to > from); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(from >= 0); + + const auto& [center, radius] = circle; + + if (radius < .5f) + { + path_pin(center); + return; + } + + path_reserve_extra(segments); + for (std::uint32_t i = 0; i < segments; ++i) + { + const auto a = from + static_cast(i) / static_cast(segments) * (to - from); + path_pin({center + point_type{math::cos(a), math::sin(a)} * radius}); + } + } + + auto path_arc(const circle_type& circle, const float from, const float to) noexcept -> void + { + const auto& [center, radius] = circle; + + if (radius < .5f) + { + path_pin(center); + return; + } + + const auto& draw_list = self.get(); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(draw_list.context_ != nullptr); + const auto& draw_list_shared_data = draw_list.context_->current_draw_list_shared_data(); + + // Automatic segment count + if (radius <= draw_list_shared_data.arc_fast_radius_cutoff) + { + const auto is_reversed = to < from; + + // We are going to use precomputed values for mid-samples. + // Determine first and last sample in lookup table that belong to the arc + const auto sample_from_f = DrawListSharedData::vertex_sample_points_count * from / (std::numbers::pi_v * 2); + const auto sample_to_f = DrawListSharedData::vertex_sample_points_count * to / (std::numbers::pi_v * 2); + + const auto sample_from = is_reversed ? static_cast(math::floor(sample_from_f)) : static_cast(math::ceil(sample_from_f)); + const auto sample_to = is_reversed ? static_cast(math::ceil(sample_to_f)) : static_cast(math::floor(sample_to_f)); + const auto sample_mid = is_reversed ? static_cast(std::ranges::max(sample_from - sample_to, 0)) : static_cast(std::ranges::max(sample_to - sample_from, 0)); + + const auto segment_from_angle = static_cast(sample_from) * std::numbers::pi_v * 2 / DrawListSharedData::vertex_sample_points_count; + const auto segment_to_angle = static_cast(sample_to) * std::numbers::pi_v * 2 / DrawListSharedData::vertex_sample_points_count; + + const auto emit_start = math::abs(segment_from_angle - from) >= 1e-5f; + const auto emit_end = math::abs(to - segment_to_angle) >= 1e-5f; + + if (emit_start) + { + // The quadrant must be the same, otherwise it is not continuous with the path drawn by `path_arc_fast`. + path_pin({center + point_type{math::cos(from), -math::sin(from)} * radius}); + } + if (sample_mid > 0) + { + path_arc_fast(circle, sample_from, sample_to); + } + if (emit_end) + { + // The quadrant must be the same, otherwise it is not continuous with the path drawn by `path_arc_fast`. + path_pin({center + point_type{math::cos(to), -math::sin(to)} * radius}); + } + } + else + { + const auto arc_length = to - from; + const auto circle_segment_count = draw_list_shared_data.circle_auto_segment_count(radius); + const auto arc_segment_count = std::ranges::max( + static_cast(math::ceil(static_cast(circle_segment_count) * arc_length / (std::numbers::pi_v * 2))), + static_cast(std::numbers::pi_v * 2 / arc_length) + ); + path_arc_n(circle, from, to, arc_segment_count); + } + } + + auto path_arc_elliptical_n(const ellipse_type& ellipse, const float from, const float to, const std::uint32_t segments) noexcept -> void + { + const auto& [center, radius, rotation] = ellipse; + const auto cos_theta = math::cos(rotation); + const auto sin_theta = math::sin(rotation); + + path_reserve_extra(segments); + for (std::uint32_t i = 0; i < segments; ++i) + { + const auto a = from + static_cast(i) / static_cast(segments) * (to - from); + const auto offset = point_type{math::cos(a), math::sin(a)} * radius; + const auto prime_x = offset.x * cos_theta - offset.y * sin_theta; + const auto prime_y = offset.x * sin_theta + offset.y * cos_theta; + path_pin({center + point_type{prime_x, prime_y}}); + } + } + + auto path_quadrilateral(const point_type& p1, const point_type& p2, const point_type& p3, const point_type& p4) noexcept -> void + { + path_pin(p1); + path_pin(p2); + path_pin(p3); + path_pin(p4); + } + + auto path_rect(const rect_type& rect, float rounding, DrawFlag flag) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(rect.valid() and not rect.empty()); + + if (rounding >= .5f) + { + flag = to_fixed_rect_corner_flag(flag); + + const auto v = + (flag & DrawFlag::ROUND_CORNER_TOP) == DrawFlag::ROUND_CORNER_TOP or + (flag & DrawFlag::ROUND_CORNER_BOTTOM) == DrawFlag::ROUND_CORNER_BOTTOM; + const auto h = + (flag & DrawFlag::ROUND_CORNER_LEFT) == DrawFlag::ROUND_CORNER_LEFT or + (flag & DrawFlag::ROUND_CORNER_RIGHT) == DrawFlag::ROUND_CORNER_RIGHT; + + rounding = std::ranges::min(rounding, rect.width() * (v ? .5f : 1.f) - 1.f); + rounding = std::ranges::min(rounding, rect.height() * (h ? .5f : 1.f) - 1.f); + } + + if (rounding < .5f or (DrawFlag::ROUND_CORNER_MASK & flag) == DrawFlag::ROUND_CORNER_NONE) + { + path_quadrilateral(rect.left_top(), rect.right_top(), rect.right_bottom(), rect.left_bottom()); + } + else + { + const auto rounding_left_top = (flag & DrawFlag::ROUND_CORNER_LEFT_TOP) != DrawFlag::NONE ? rounding : 0; + const auto rounding_right_top = (flag & DrawFlag::ROUND_CORNER_RIGHT_TOP) != DrawFlag::NONE ? rounding : 0; + const auto rounding_left_bottom = (flag & DrawFlag::ROUND_CORNER_LEFT_BOTTOM) != DrawFlag::NONE ? rounding : 0; + const auto rounding_right_bottom = (flag & DrawFlag::ROUND_CORNER_RIGHT_BOTTOM) != DrawFlag::NONE ? rounding : 0; + + path_arc_fast({rect.left_top() + point_type{rounding_left_top, rounding_left_top}, rounding_left_top}, DrawArcFlag::Q2_CLOCK_WISH); + path_arc_fast({rect.right_top() + point_type{-rounding_right_top, rounding_right_top}, rounding_right_top}, DrawArcFlag::Q1_CLOCK_WISH); + path_arc_fast({rect.right_bottom() + point_type{-rounding_right_bottom, -rounding_right_bottom}, rounding_right_bottom}, DrawArcFlag::Q4_CLOCK_WISH); + path_arc_fast({rect.left_bottom() + point_type{rounding_left_bottom, -rounding_left_bottom}, rounding_left_bottom}, DrawArcFlag::Q3_CLOCK_WISH); + } + } + + auto path_bezier_cubic_curve_casteljau( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const point_type& p4, + const float tessellation_tolerance, + const std::size_t level + ) noexcept -> void + { + const auto dx = p4.x - p1.x; + const auto dy = p4.y - p1.y; + const auto d2 = math::abs((p2.x - p4.x) * dy - (p2.y - p4.y) * dx); + const auto d3 = math::abs((p3.x - p4.x) * dy - (p3.y - p4.y) * dx); + + if (math::pow(d2 + d3, 2) < tessellation_tolerance * (math::pow(dx, 2) + math::pow(dy, 2))) + { + path_pin(p4); + } + else if (level < bezier_curve_casteljau_max_level) + { + const auto p_12 = (p1 + p2) * .5f; + const auto p_23 = (p2 + p3) * .5f; + const auto p_34 = (p3 + p4) * .5f; + const auto p_123 = (p_12 + p_23) * .5f; + const auto p_234 = (p_23 + p_34) * .5f; + const auto p_1234 = (p_123 + p_234) * .5f; + + path_bezier_cubic_curve_casteljau(p1, p_12, p_123, p_1234, tessellation_tolerance, level + 1); + path_bezier_cubic_curve_casteljau(p_1234, p_234, p_34, p4, tessellation_tolerance, level + 1); + } + } + + auto path_bezier_quadratic_curve_casteljau(const point_type& p1, const point_type& p2, const point_type& p3, const float tessellation_tolerance, const std::size_t level) noexcept -> void + { + const auto dx = p3.x - p1.x; + const auto dy = p3.y - p1.y; + const auto det = (p2.x - p3.x) * dy - (p2.y - p3.y) * dx; + + if (math::pow(det, 2) * 4.f < tessellation_tolerance * (math::pow(dx, 2) + math::pow(dy, 2))) + { + path_pin(p3); + } + else if (level < bezier_curve_casteljau_max_level) + { + const auto p_12 = (p1 + p2) * .5f; + const auto p_23 = (p2 + p3) * .5f; + const auto p_123 = (p_12 + p_23) * .5f; + + path_bezier_quadratic_curve_casteljau(p1, p_12, p_123, tessellation_tolerance, level + 1); + path_bezier_quadratic_curve_casteljau(p_123, p_23, p3, tessellation_tolerance, level + 1); + } + } + + auto path_bezier_curve(const point_type& p1, const point_type& p2, const point_type& p3, const point_type& p4, const std::uint32_t segments) noexcept -> void + { + const auto& draw_list = self.get(); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(draw_list.context_ != nullptr); + const auto& draw_list_shared_data = draw_list.context_->current_draw_list_shared_data(); + + path_pin(p1); + if (segments == 0) + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(draw_list_shared_data.curve_tessellation_tolerance > 0); + + path_reserve_extra(bezier_curve_casteljau_max_level * 2); + // auto-tessellated + path_bezier_cubic_curve_casteljau(p1, p2, p3, p4, draw_list_shared_data.curve_tessellation_tolerance, 0); + } + else + { + path_reserve_extra(segments); + const auto step = 1.f / static_cast(segments); + for (std::uint32_t i = 1; i <= segments; ++i) + { + path_pin(bezier_cubic_calc(p1, p2, p3, p4, step * static_cast(i))); + } + } + } + + auto path_bezier_quadratic_curve(const point_type& p1, const point_type& p2, const point_type& p3, const std::uint32_t segments) noexcept -> void + { + const auto& draw_list = self.get(); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(draw_list.context_ != nullptr); + const auto& draw_list_shared_data = draw_list.context_->current_draw_list_shared_data(); + + path_pin(p1); + if (segments == 0) + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(draw_list_shared_data.curve_tessellation_tolerance > 0); + + path_reserve_extra(bezier_curve_casteljau_max_level * 2); + // auto-tessellated + path_bezier_quadratic_curve_casteljau(p1, p2, p3, draw_list_shared_data.curve_tessellation_tolerance, 0); + } + else + { + path_reserve_extra(segments); + const auto step = 1.f / static_cast(segments); + for (std::uint32_t i = 1; i <= segments; ++i) + { + path_pin(bezier_quadratic_calc(p1, p2, p3, step * static_cast(i))); + } + } + } + }; + + auto DrawList::push_command() noexcept -> void + { + // fixme: If the window boundary is smaller than the rect boundary, the rect will no longer be valid. + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not this_command_clip_rect_.empty() and this_command_clip_rect_.valid()); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(this_command_texture_id_ != Font::invalid_texture_id); + + command_list_.emplace_back( + command_type + { + .clip_rect = this_command_clip_rect_, + .texture_id = this_command_texture_id_, + .index_offset = static_cast(index_list_.size()), + // set by draw_xxx + .element_count = 0 + } + ); + } + + auto DrawList::on_element_changed(const ChangedElement element) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not command_list_.empty()); + + const auto command_count = command_list_.size(); + const auto& [current_clip_rect, current_texture_id, current_index_offset, current_element_count] = command_list_.back(); + + if (current_element_count != 0) + { + if (element == ChangedElement::CLIP_RECT) + { + if (current_clip_rect != this_command_clip_rect_) + { + push_command(); + + return; + } + } + else if (element == ChangedElement::TEXTURE_ID) + { + if (current_texture_id != this_command_texture_id_) + { + push_command(); + + return; + } + } + else + { + GAL_PROMETHEUS_ERROR_DEBUG_UNREACHABLE(); + } + } + + // try to merge with previous command if it matches, else use current command + if (command_count > 1) + { + if ( + const auto& [previous_clip_rect, previous_texture, previous_index_offset, previous_element_count] = command_list_[command_count - 2]; + current_element_count == 0 and + ( + this_command_clip_rect_ == previous_clip_rect and + this_command_texture_id_ == previous_texture + ) and + // sequential + current_index_offset == previous_index_offset + previous_element_count + ) + { + command_list_.pop_back(); + return; + } + } + + if (element == ChangedElement::CLIP_RECT) + { + command_list_.back().clip_rect = this_command_clip_rect_; + } + else if (element == ChangedElement::TEXTURE_ID) + { + command_list_.back().texture_id = this_command_texture_id_; + } + else + { + GAL_PROMETHEUS_ERROR_DEBUG_UNREACHABLE(); + } + } + + DrawList::DrawList() noexcept + : + context_{nullptr}, + draw_list_flag_{DrawListFlag::NONE}, + this_command_clip_rect_{0, 0, 0, 0}, + this_command_texture_id_{Font::invalid_texture_id} {} + + auto DrawList::bind_context(Context& context) noexcept -> void + { + context_ = std::addressof(context); + + draw_list_flag_ = context.current_draw_list_flag(); + } + + auto DrawList::reset() noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(context_ != nullptr); + const auto& font = context_->current_font(); + + command_list_.clear(); + vertex_list_.clear(); + index_list_.clear(); + + // we don't know the size of the clip rect, so we need the user to set it + this_command_clip_rect_ = {}; + // the first texture is always the (default) font texture + this_command_texture_id_ = font.texture_id; + + // we always have a command ready in the buffer + command_list_.emplace_back( + command_type + { + .clip_rect = this_command_clip_rect_, + .texture_id = this_command_texture_id_, + .index_offset = static_cast(index_list_.size()), + // set by subsequent draw_xxx + .element_count = 0 + } + ); + } + + auto DrawList::command_list() const noexcept -> const command_list_type& + { + return command_list_; + } + + auto DrawList::vertex_list() const noexcept -> const vertex_list_type& + { + return vertex_list_; + } + + auto DrawList::index_list() const noexcept -> const index_list_type& + { + return index_list_; + } + + auto DrawList::push_clip_rect(const rect_type& rect, const bool intersect_with_current_clip_rect) noexcept -> rect_type& + { + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not rect.empty() and rect.valid()); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not command_list_.empty()); + + const auto& [current_clip_rect, current_texture, current_index_offset, current_element_count] = command_list_.back(); + + this_command_clip_rect_ = intersect_with_current_clip_rect ? rect.combine_min(current_clip_rect) : rect; + + on_element_changed(ChangedElement::CLIP_RECT); + return command_list_.back().clip_rect; + } + + auto DrawList::pop_clip_rect() noexcept -> void + { + // at least one command + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(command_list_.size() > 1); + this_command_clip_rect_ = command_list_[command_list_.size() - 2].clip_rect; + + on_element_changed(ChangedElement::CLIP_RECT); + } + + auto DrawList::push_texture_id(const texture_id_type texture) noexcept -> void + { + this_command_texture_id_ = texture; + + on_element_changed(ChangedElement::TEXTURE_ID); + } + + auto DrawList::pop_texture_id() noexcept -> void + { + // at least one command + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(command_list_.size() > 1); + this_command_texture_id_ = command_list_[command_list_.size() - 2].texture_id; + + on_element_changed(ChangedElement::TEXTURE_ID); + } + + auto DrawList::line( + const point_type& from, + const point_type& to, + const color_type color, + const float thickness + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_pin(from); + drawer.path_pin(to); + + drawer.path_stroke(color, DrawFlag::NONE, thickness); + } + + auto DrawList::triangle( + const point_type& a, + const point_type& b, + const point_type& c, + const color_type color, + const float thickness + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_pin(a); + drawer.path_pin(b); + drawer.path_pin(c); + + drawer.path_stroke(color, DrawFlag::CLOSED, thickness); + } + + auto DrawList::triangle_filled( + const point_type& a, + const point_type& b, + const point_type& c, + const color_type color + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_pin(a); + drawer.path_pin(b); + drawer.path_pin(c); + + drawer.path_stroke(color); + } + + auto DrawList::rect( + const rect_type& rect, + const color_type color, + const float rounding, + const DrawFlag flag, + const float thickness + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_rect(rect, rounding, flag); + + drawer.path_stroke(color, DrawFlag::CLOSED, thickness); + } + + auto DrawList::rect( + const point_type& left_top, + const point_type& right_bottom, + const color_type color, + const float rounding, + const DrawFlag flag, + const float thickness + ) noexcept -> void + { + return rect({left_top, right_bottom}, color, rounding, flag, thickness); + } + + auto DrawList::rect_filled( + const rect_type& rect, + const color_type color, + const float rounding, + const DrawFlag flag + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + if (rounding < .5f or (DrawFlag::ROUND_CORNER_MASK & flag) == DrawFlag::ROUND_CORNER_NONE) + { + drawer.draw_rect_filled(rect, color, color, color, color); + } + else + { + drawer.path_rect(rect, rounding, flag); + drawer.path_stroke(color); + } + } + + auto DrawList::rect_filled( + const point_type& left_top, + const point_type& right_bottom, + const color_type color, + const float rounding, + const DrawFlag flag + ) noexcept -> void + { + return rect_filled({left_top, right_bottom}, color, rounding, flag); + } + + auto DrawList::rect_filled( + const rect_type& rect, + const color_type color_left_top, + const color_type color_right_top, + const color_type color_left_bottom, + const color_type color_right_bottom + ) noexcept -> void + { + if (color_left_top.alpha == 0 or color_right_top.alpha == 0 or color_left_bottom.alpha == 0 or color_right_bottom.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.draw_rect_filled(rect, color_left_top, color_right_top, color_left_bottom, color_right_bottom); + } + + auto DrawList::rect_filled( + const point_type& left_top, + const point_type& right_bottom, + const color_type color_left_top, + const color_type color_right_top, + const color_type color_left_bottom, + const color_type color_right_bottom + ) noexcept -> void + { + return rect_filled({left_top, right_bottom}, color_left_top, color_right_top, color_left_bottom, color_right_bottom); + } + + auto DrawList::quadrilateral( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const point_type& p4, + const color_type color, + const float thickness + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_quadrilateral(p1, p2, p3, p4); + + drawer.path_stroke(color, DrawFlag::CLOSED, thickness); + } + + auto DrawList::quadrilateral_filled( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const point_type& p4, + const color_type color + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_quadrilateral(p1, p2, p3, p4); + + drawer.path_stroke(color); + } + + auto DrawList::circle_n( + const circle_type& circle, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept -> void + { + if (color.alpha == 0 or circle.radius < .5f or segments < 3) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_arc_n(circle, 0, std::numbers::pi_v * 2, segments); + + drawer.path_stroke(color, DrawFlag::CLOSED, thickness); + } + + auto DrawList::circle_n( + const point_type& center, + const float radius, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept -> void + { + return circle_n({center, radius}, color, segments, thickness); + } + + auto DrawList::ellipse_n( + const ellipse_type& ellipse, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept -> void + { + if (color.alpha == 0 or ellipse.radius.width < .5f or ellipse.radius.height < .5f or segments < 3) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_arc_elliptical_n(ellipse, 0, std::numbers::pi_v * 2, segments); + + drawer.path_stroke(color, DrawFlag::CLOSED, thickness); + } + + void DrawList::ellipse_n( + const point_type& center, + const extent_type& radius, + const float rotation, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept + { + return ellipse_n({center, radius, rotation}, color, segments, thickness); + } + + auto DrawList::circle_n_filled( + const circle_type& circle, + const color_type color, + const std::uint32_t segments + ) noexcept -> void + { + if (color.alpha == 0 or circle.radius < .5f or segments < 3) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_arc_n(circle, 0, std::numbers::pi_v * 2, segments); + + drawer.path_stroke(color); + } + + auto DrawList::circle_n_filled( + const point_type& center, + const float radius, + const color_type color, + const std::uint32_t segments + ) noexcept -> void + { + return circle_n_filled({center, radius}, color, segments); + } + + auto DrawList::ellipse_n_filled( + const ellipse_type& ellipse, + const color_type color, + const std::uint32_t segments + ) noexcept -> void + { + if (color.alpha == 0 or ellipse.radius.width < .5f or ellipse.radius.height < .5f or segments < 3) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_arc_elliptical_n(ellipse, 0, std::numbers::pi_v * 2, segments); + + drawer.path_stroke(color); + } + + auto DrawList::ellipse_n_filled( + const point_type& center, + const extent_type& radius, + const float rotation, + const color_type color, + const std::uint32_t segments + ) noexcept -> void + { + return ellipse_n_filled({center, radius, rotation}, color, segments); + } + + void DrawList::circle( + const circle_type& circle, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept + { + if (color.alpha == 0 or circle.radius < .5f) + { + return; + } + + if (segments == 0) + { + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_arc_fast(circle, 0, DrawListSharedData::vertex_sample_points_count - 1); + + drawer.path_stroke(color, DrawFlag::CLOSED, thickness); + } + else + { + const auto clamped_segments = std::ranges::clamp(segments, DrawListSharedData::circle_segments_min, DrawListSharedData::circle_segments_max); + + circle_n(circle, color, clamped_segments, thickness); + } + } + + auto DrawList::circle( + const point_type& center, + const float radius, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept -> void + { + return circle({center, radius}, color, segments, thickness); + } + + auto DrawList::circle_filled( + const circle_type& circle, + const color_type color, + const std::uint32_t segments + ) noexcept -> void + { + if (color.alpha == 0 or circle.radius < .5f) + { + return; + } + + if (segments == 0) + { + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_arc_fast(circle, 0, DrawListSharedData::vertex_sample_points_count - 1); + + drawer.path_stroke(color); + } + else + { + const auto clamped_segments = std::ranges::clamp(segments, DrawListSharedData::circle_segments_min, DrawListSharedData::circle_segments_max); + + circle_n_filled(circle, color, clamped_segments); + } + } + + auto DrawList::circle_filled( + const point_type& center, + const float radius, + const color_type color, + const std::uint32_t segments + ) noexcept -> void + { + circle_filled({center, radius}, color, segments); + } + + auto DrawList::ellipse( + const ellipse_type& ellipse, + const color_type color, + std::uint32_t segments, + const float thickness + ) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(context_ != nullptr); + const auto& draw_list_shared_data = context_->current_draw_list_shared_data(); + + if (color.alpha == 0 or ellipse.radius.width < .5f or ellipse.radius.height < .5f) + { + return; + } + + if (segments == 0) + { + // fixme + segments = draw_list_shared_data.circle_auto_segment_count(std::ranges::max(ellipse.radius.width, ellipse.radius.height)); + } + + ellipse_n(ellipse, color, segments, thickness); + } + + auto DrawList::ellipse( + const point_type& center, + const extent_type& radius, + const float rotation, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept -> void + { + return ellipse({center, radius, rotation}, color, segments, thickness); + } + + auto DrawList::ellipse_filled( + const ellipse_type& ellipse, + const color_type color, + std::uint32_t segments + ) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(context_ != nullptr); + const auto& draw_list_shared_data = context_->current_draw_list_shared_data(); + + if (color.alpha == 0 or ellipse.radius.width < .5f or ellipse.radius.height < .5f) + { + return; + } + + if (segments == 0) + { + // fixme + segments = draw_list_shared_data.circle_auto_segment_count(std::ranges::max(ellipse.radius.width, ellipse.radius.height)); + } + + ellipse_n_filled(ellipse, color, segments); + } + + auto DrawList::ellipse_filled( + const point_type& center, + const extent_type& radius, + const float rotation, + const color_type color, + const std::uint32_t segments + ) noexcept -> void + { + return ellipse_filled({center, radius, rotation}, color, segments); + } + + auto DrawList::bezier_cubic( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const point_type& p4, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_bezier_curve(p1, p2, p3, p4, segments); + + drawer.path_stroke(color, DrawFlag::NONE, thickness); + } + + auto DrawList::bezier_quadratic( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const color_type color, + const std::uint32_t segments, + const float thickness + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.path_bezier_quadratic_curve(p1, p2, p3, segments); + + drawer.path_stroke(color, DrawFlag::NONE, thickness); + } + + auto DrawList::text( + const Font& font, + const float font_size, + const point_type& point, + const color_type color, + const std::string_view utf8_text, + float wrap_width + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + if (wrap_width == text_wrap_width_not_set) // NOLINT(clang-diagnostic-float-equal) + { + wrap_width = Font::no_auto_wrap; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.draw_text(font, font_size, point, color, utf8_text, wrap_width); + } + + auto DrawList::text( + const float font_size, + const point_type& point, + const color_type color, + const std::string_view utf8_text, + const float wrap_width + ) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(context_ != nullptr); + const auto& font = context_->current_font(); + + text(font, font_size, point, color, utf8_text, wrap_width); + } + + auto DrawList::image( + const texture_id_type texture_id, + const point_type& display_p1, + const point_type& display_p2, + const point_type& display_p3, + const point_type& display_p4, + const uv_type& uv_p1, + const uv_type& uv_p2, + const uv_type& uv_p3, + const uv_type& uv_p4, + const color_type color + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.draw_image(texture_id, display_p1, display_p2, display_p3, display_p4, uv_p1, uv_p2, uv_p3, uv_p4, color); + } + + auto DrawList::image( + const texture_id_type texture_id, + const rect_type& display_rect, + const rect_type& uv_rect, + const color_type color + ) noexcept -> void + { + image( + texture_id, + display_rect.left_top(), + display_rect.right_top(), + display_rect.right_bottom(), + display_rect.left_bottom(), + uv_rect.left_top(), + uv_rect.right_top(), + uv_rect.right_bottom(), + uv_rect.left_bottom(), + color + ); + } + + auto DrawList::image( + const texture_id_type texture_id, + const point_type& display_left_top, + const point_type& display_right_bottom, + const uv_type& uv_left_top, + const uv_type& uv_right_bottom, + const color_type color + ) noexcept -> void + { + image(texture_id, {display_left_top, display_right_bottom}, {uv_left_top, uv_right_bottom}, color); + } + + auto DrawList::image_rounded( + const texture_id_type texture_id, + const rect_type& display_rect, + const float rounding, + const DrawFlag flag, + const rect_type& uv_rect, + const color_type color + ) noexcept -> void + { + if (color.alpha == 0) + { + return; + } + + Drawer drawer{.self = *this, .path_list = {}}; + + drawer.draw_image_rounded(texture_id, display_rect, uv_rect, color, rounding, flag); + } + + auto DrawList::image_rounded( + const texture_id_type texture_id, + const point_type& display_left_top, + const point_type& display_right_bottom, + const float rounding, + const DrawFlag flag, + const uv_type& uv_left_top, + const uv_type& uv_right_bottom, + const color_type color + ) noexcept -> void + { + image_rounded(texture_id, {display_left_top, display_right_bottom}, rounding, flag, {uv_left_top, uv_right_bottom}, color); + } +} diff --git a/src/gui/internal/draw_list.hpp b/src/gui/internal/draw_list.hpp new file mode 100644 index 00000000..1aa1c779 --- /dev/null +++ b/src/gui/internal/draw_list.hpp @@ -0,0 +1,492 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include + +namespace gal::prometheus +{ + namespace gui::internal + { + enum class DrawFlag : std::uint8_t + { + NONE = 0, + // specify that shape should be closed + // @see DrawList::draw_polygon_line + // @see DrawList::draw_polygon_line_aa + // @see DrawList::path_stroke + CLOSED = 1 << 0, + // enable rounding left-top corner only (when rounding > 0.0f, we default to all corners) + // @see DrawList::path_rect + // @see DrawList::rect + // @see DrawList::rect_filled + ROUND_CORNER_LEFT_TOP = 1 << 1, + // enable rounding right_top corner only (when rounding > 0.0f, we default to all corners) + // @see DrawList::path_rect + // @see DrawList::rect + // @see DrawList::rect_filled + ROUND_CORNER_RIGHT_TOP = 1 << 2, + // enable rounding left-bottom corner only (when rounding > 0.0f, we default to all corners) + // @see DrawList::path_rect + // @see DrawList::rect + // @see DrawList::rect_filled + ROUND_CORNER_LEFT_BOTTOM = 1 << 3, + // enable rounding right-bottom corner only (when rounding > 0.0f, we default to all corners) + // @see DrawList::path_rect + // @see DrawList::rect + // @see DrawList::rect_filled + ROUND_CORNER_RIGHT_BOTTOM = 1 << 4, + // disable rounding on all corners (when rounding > 0.0f) + ROUND_CORNER_NONE = 1 << 5, + + ROUND_CORNER_LEFT = ROUND_CORNER_LEFT_TOP | ROUND_CORNER_LEFT_BOTTOM, + ROUND_CORNER_TOP = ROUND_CORNER_LEFT_TOP | ROUND_CORNER_RIGHT_TOP, + ROUND_CORNER_RIGHT = ROUND_CORNER_RIGHT_TOP | ROUND_CORNER_RIGHT_BOTTOM, + ROUND_CORNER_BOTTOM = ROUND_CORNER_LEFT_BOTTOM | ROUND_CORNER_RIGHT_BOTTOM, + + ROUND_CORNER_ALL = ROUND_CORNER_LEFT_TOP | ROUND_CORNER_RIGHT_TOP | ROUND_CORNER_LEFT_BOTTOM | ROUND_CORNER_RIGHT_BOTTOM, + ROUND_CORNER_DEFAULT = ROUND_CORNER_ALL, + ROUND_CORNER_MASK = ROUND_CORNER_ALL | ROUND_CORNER_NONE, + }; + + enum class DrawArcFlag : std::uint8_t + { + // [0~3) + Q1 = 1 << 0, + // [3~6) + Q2 = 1 << 1, + // [6~9) + Q3 = 1 << 2, + // [9~12) + Q4 = 1 << 3, + + RIGHT_TOP = Q1, + LEFT_TOP = Q2, + LEFT_BOTTOM = Q3, + RIGHT_BOTTOM = Q4, + TOP = Q1 | Q2, + BOTTOM = Q3 | Q4, + LEFT = Q2 | Q3, + RIGHT = Q1 | Q4, + ALL = Q1 | Q2 | Q3 | Q4, + + // [3, 0) + Q1_CLOCK_WISH = 1 << 4, + // [6, 3) + Q2_CLOCK_WISH = 1 << 5, + // [9, 6) + Q3_CLOCK_WISH = 1 << 6, + // [12, 9) + Q4_CLOCK_WISH = 1 << 7, + + RIGHT_TOP_CLOCK_WISH = Q1_CLOCK_WISH, + LEFT_TOP_CLOCK_WISH = Q2_CLOCK_WISH, + LEFT_BOTTOM_CLOCK_WISH = Q3_CLOCK_WISH, + RIGHT_BOTTOM_CLOCK_WISH = Q4_CLOCK_WISH, + TOP_CLOCK_WISH = Q1_CLOCK_WISH | Q2_CLOCK_WISH, + BOTTOM_CLOCK_WISH = Q3_CLOCK_WISH | Q4_CLOCK_WISH, + LEFT_CLOCK_WISH = Q2_CLOCK_WISH | Q3_CLOCK_WISH, + RIGHT_CLOCK_WISH = Q1_CLOCK_WISH | Q4_CLOCK_WISH, + ALL_CLOCK_WISH = Q1_CLOCK_WISH | Q2_CLOCK_WISH | Q3_CLOCK_WISH | Q4_CLOCK_WISH, + }; + } + + namespace meta::user_defined + { + template<> + struct enum_is_flag : std::true_type {}; + + template<> + struct enum_is_flag : std::true_type {}; + } + + namespace gui::internal + { + [[nodiscard]] auto range_of_arc(DrawArcFlag flag) noexcept -> std::pair; + + class Font; + + class DrawList + { + public: + using size_type = DrawData::size_type; + + using command_type = DrawData::command_type; + + using vertex_list_type = DrawData::vertex_list_type; + using index_list_type = DrawData::index_list_type; + using command_list_type = DrawData::command_list_type; + + // not set ==> Font::no_auto_wrap + constexpr static float text_wrap_width_not_set = -1.f; + + private: + class Drawer; + + Context* context_; + + DrawListFlag draw_list_flag_; + + // vertex_list: v1-v2-v3-v4 + v5-v6-v7-v8 + v9-v10-v11 => rect0 + rect1(clipped by rect0) + triangle0(clipped by rect1) + // index_list: 0/1/2-0/2/3 + 4/5/6-4/6/7 + 8/9/10 + // command_list: + // 0: .clip_rect = {0, 0, root_window_width, root_window_height}, .index_offset = 0, .element_count = root_window_element_count + 6 (two triangles => 0/1/2-0/2/3) + // 1: .clip_rect = {max(rect0.left, rect1.left), max(rect0.top, rect1.top), min(rect0.right, rect1.right), min(rect0.bottom, rect1.bottom)}, .index_offset = root_window_element_count + 6, .element_count = 6 (two triangles => 4/5/6-4/6/7) + // 2: .clip_rect = {...}, .index_offset = root_window_element_count + 12, .element_count = 3 (one triangle => 8/9/10) + command_list_type command_list_; + vertex_list_type vertex_list_; + index_list_type index_list_; + + rect_type this_command_clip_rect_; + texture_id_type this_command_texture_id_; + + auto push_command() noexcept -> void; + + enum class ChangedElement : std::uint8_t + { + CLIP_RECT, + TEXTURE_ID, + }; + + auto on_element_changed(ChangedElement element) noexcept -> void; + + public: + DrawList() noexcept; + + // ---------------------------------------------------------------------------- + // CONTEXT + + auto bind_context(Context& context) noexcept -> void; + + // ---------------------------------------------------------------------------- + // RESET + + auto reset() noexcept -> void; + + // ---------------------------------------------------------------------------- + // DRAW DATA + + [[nodiscard]] auto command_list() const noexcept -> const command_list_type&; + + [[nodiscard]] auto vertex_list() const noexcept -> const vertex_list_type&; + + [[nodiscard]] auto index_list() const noexcept -> const index_list_type&; + + // ---------------------------------------------------------------------------- + // CLIP RECT & TEXTURE + + auto push_clip_rect(const rect_type& rect, bool intersect_with_current_clip_rect) noexcept -> rect_type&; + + auto pop_clip_rect() noexcept -> void; + + auto push_texture_id(texture_id_type texture) noexcept -> void; + + auto pop_texture_id() noexcept -> void; + + // ---------------------------------------------------------------------------- + // PRIMITIVE + + auto line( + const point_type& from, + const point_type& to, + color_type color, + float thickness = 1.f + ) noexcept -> void; + + auto triangle( + const point_type& a, + const point_type& b, + const point_type& c, + color_type color, + float thickness = 1.f + ) noexcept -> void; + + auto triangle_filled( + const point_type& a, + const point_type& b, + const point_type& c, + color_type color + ) noexcept -> void; + + auto rect( + const rect_type& rect, + color_type color, + float rounding = .0f, + DrawFlag flag = DrawFlag::ROUND_CORNER_ALL, + float thickness = 1.f + ) noexcept -> void; + + auto rect( + const point_type& left_top, + const point_type& right_bottom, + color_type color, + float rounding = .0f, + DrawFlag flag = DrawFlag::ROUND_CORNER_ALL, + float thickness = 1.f + ) noexcept -> void; + + auto rect_filled( + const rect_type& rect, + color_type color, + float rounding = .0f, + DrawFlag flag = DrawFlag::ROUND_CORNER_ALL + ) noexcept -> void; + + auto rect_filled( + const point_type& left_top, + const point_type& right_bottom, + color_type color, + float rounding = .0f, + DrawFlag flag = DrawFlag::ROUND_CORNER_ALL + ) noexcept -> void; + + auto rect_filled( + const rect_type& rect, + color_type color_left_top, + color_type color_right_top, + color_type color_left_bottom, + color_type color_right_bottom + ) noexcept -> void; + + auto rect_filled( + const point_type& left_top, + const point_type& right_bottom, + color_type color_left_top, + color_type color_right_top, + color_type color_left_bottom, + color_type color_right_bottom + ) noexcept -> void; + + auto quadrilateral( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const point_type& p4, + color_type color, + float thickness = 1.f + ) noexcept -> void; + + auto quadrilateral_filled( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const point_type& p4, + color_type color + ) noexcept -> void; + + auto circle_n( + const circle_type& circle, + color_type color, + std::uint32_t segments, + float thickness = 1.f + ) noexcept -> void; + + auto circle_n( + const point_type& center, + float radius, + color_type color, + std::uint32_t segments, + float thickness = 1.f + ) noexcept -> void; + + auto ellipse_n( + const ellipse_type& ellipse, + color_type color, + std::uint32_t segments, + float thickness = 1.f + ) noexcept -> void; + + auto ellipse_n( + const point_type& center, + const extent_type& radius, + float rotation, + color_type color, + std::uint32_t segments, + float thickness = 1.f + ) noexcept -> void; + + auto circle_n_filled( + const circle_type& circle, + color_type color, + std::uint32_t segments + ) noexcept -> void; + + auto circle_n_filled( + const point_type& center, + float radius, + color_type color, + std::uint32_t segments + ) noexcept -> void; + + auto ellipse_n_filled( + const ellipse_type& ellipse, + color_type color, + std::uint32_t segments + ) noexcept -> void; + + auto ellipse_n_filled( + const point_type& center, + const extent_type& radius, + float rotation, + color_type color, + std::uint32_t segments + ) noexcept -> void; + + auto circle( + const circle_type& circle, + color_type color, + std::uint32_t segments = 0, + float thickness = 1.f + ) noexcept -> void; + + auto circle( + const point_type& center, + float radius, + color_type color, + std::uint32_t segments = 0, + float thickness = 1.f + ) noexcept -> void; + + auto circle_filled( + const circle_type& circle, + color_type color, + std::uint32_t segments = 0 + ) noexcept -> void; + + auto circle_filled( + const point_type& center, + float radius, + color_type color, + std::uint32_t segments = 0 + ) noexcept -> void; + + auto ellipse( + const ellipse_type& ellipse, + color_type color, + std::uint32_t segments = 0, + float thickness = 1.f + ) noexcept -> void; + + auto ellipse( + const point_type& center, + const extent_type& radius, + float rotation, + color_type color, + std::uint32_t segments = 0, + float thickness = 1.f + ) noexcept -> void; + + auto ellipse_filled( + const ellipse_type& ellipse, + color_type color, + std::uint32_t segments = 0 + ) noexcept -> void; + + auto ellipse_filled( + const point_type& center, + const extent_type& radius, + float rotation, + color_type color, + std::uint32_t segments = 0 + ) noexcept -> void; + + auto bezier_cubic( + const point_type& p1, + const point_type& p2, + const point_type& p3, + const point_type& p4, + color_type color, + std::uint32_t segments = 0, + float thickness = 1.f + ) noexcept -> void; + + auto bezier_quadratic( + const point_type& p1, + const point_type& p2, + const point_type& p3, + color_type color, + std::uint32_t segments = 0, + float thickness = 1.f + ) noexcept -> void; + + // ---------------------------------------------------------------------------- + // TEXT + + auto text( + const Font& font, + float font_size, + const point_type& point, + color_type color, + std::string_view utf8_text, + float wrap_width = text_wrap_width_not_set + ) noexcept -> void; + + auto text( + float font_size, + const point_type& point, + color_type color, + std::string_view utf8_text, + float wrap_width = text_wrap_width_not_set + ) noexcept -> void; + + // ---------------------------------------------------------------------------- + // IMAGE + + // p1________ p2 + // | | + // | | + // p4|_______| p3 + auto image( + texture_id_type texture_id, + const point_type& display_p1, + const point_type& display_p2, + const point_type& display_p3, + const point_type& display_p4, + const uv_type& uv_p1 = {0, 0}, + const uv_type& uv_p2 = {1, 0}, + const uv_type& uv_p3 = {1, 1}, + const uv_type& uv_p4 = {0, 1}, + color_type color = primitive::colors::white + ) noexcept -> void; + + auto image( + texture_id_type texture_id, + const rect_type& display_rect, + const rect_type& uv_rect = {0, 0, 1, 1}, + color_type color = primitive::colors::white + ) noexcept -> void; + + auto image( + texture_id_type texture_id, + const point_type& display_left_top, + const point_type& display_right_bottom, + const uv_type& uv_left_top = {0, 0}, + const uv_type& uv_right_bottom = {1, 1}, + color_type color = primitive::colors::white + ) noexcept -> void; + + auto image_rounded( + texture_id_type texture_id, + const rect_type& display_rect, + float rounding = .0f, + DrawFlag flag = DrawFlag::NONE, + const rect_type& uv_rect = {0, 0, 1, 1}, + color_type color = primitive::colors::white + ) noexcept -> void; + + auto image_rounded( + texture_id_type texture_id, + const point_type& display_left_top, + const point_type& display_right_bottom, + float rounding = .0f, + DrawFlag flag = DrawFlag::NONE, + const uv_type& uv_left_top = {0, 0}, + const uv_type& uv_right_bottom = {1, 1}, + color_type color = primitive::colors::white + ) noexcept -> void; + }; + } +} diff --git a/src/gui/internal/font.cpp b/src/gui/internal/font.cpp new file mode 100644 index 00000000..8e48b1dd --- /dev/null +++ b/src/gui/internal/font.cpp @@ -0,0 +1,531 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include FT_FREETYPE_H // +// #include FT_MODULE_H // +// #include FT_GLYPH_H // +// #include FT_SYNTHESIS_H // + +#define STB_RECT_PACK_IMPLEMENTATION +#include + +#include +#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE + +#include + +namespace +{ + struct ft_type + { + FT_Library library; + FT_Face face; + + [[nodiscard]] constexpr auto valid() const noexcept -> bool + { + return library and face; + } + }; + + [[nodiscard]] auto create_ft(const std::string_view font_path, const std::uint32_t pixel_height) noexcept -> ft_type + { + FT_Library ft_library; + if (FT_Init_FreeType(&ft_library)) + { + // Could not initialize FreeType library + return {.library = nullptr, .face = nullptr}; + } + + FT_Face ft_face; + if (FT_New_Face(ft_library, font_path.data(), 0, &ft_face)) + { + FT_Done_FreeType(ft_library); + // Could not load font + return {.library = nullptr, .face = nullptr}; + } + + FT_Set_Pixel_Sizes(ft_face, 0, pixel_height); + return {.library = ft_library, .face = ft_face}; + } + + auto destroy_ft(const ft_type ft) noexcept -> void + { + auto [ft_library, ft_face] = ft; + + FT_Done_Face(ft_face); + FT_Done_FreeType(ft_library); + } + + using namespace gal::prometheus; + using namespace gui; + + auto do_load_font(const FontOption& option, internal::Font& font) noexcept -> Texture + { + font.font_path = std::format("{}-{}px", option.font_path, option.pixel_height); + font.pixel_height = option.pixel_height; + if (option.baked_line_max_width == 0 or option.baked_line_max_width > FontOption::default_baked_line_max_width) + { + font.baked_line_max_width = FontOption::default_baked_line_max_width; + } + else + { + font.baked_line_max_width = option.baked_line_max_width; + } + font.scale = option.scale; + font.display_offset = option.display_offset; + font.white_pixel_uv = option.white_pixel_uv; + + Texture texture{font.texture_id}; + + auto ft = create_ft(option.font_path, option.pixel_height); + if (not ft.valid()) + { + return texture; + } + + auto [library, face] = ft; + + // =============================== + + std::vector rects; + + // baked line + constexpr auto id_baked_line = std::numeric_limits::min() + 0; + { + const auto baked_line_uv_width = font.baked_line_max_width + 1; + const auto baked_line_uv_height = font.baked_line_max_width + 2; + + font.baked_line_uv.reserve(baked_line_uv_height); + + rects.emplace_back( + stbrp_rect + { + .id = id_baked_line, + .w = static_cast(baked_line_uv_width), + .h = static_cast(baked_line_uv_height), + .x = 0, + .y = 0, + .was_packed = 0 + } + ); + } + + // =============================== + + std::ranges::for_each( + option.glyph_ranges, + [&face, &rects](const auto& pair) noexcept -> void + { + const auto [from, to] = pair; + + for (auto c = from; c <= to; ++c) + { + if (FT_Load_Char(face, c, FT_LOAD_RENDER)) + { + continue; + } + + const auto& g = face->glyph; + rects.emplace_back( + stbrp_rect + { + .id = std::bit_cast(c), + .w = static_cast(g->bitmap.width), + .h = static_cast(g->bitmap.rows), + .x = static_cast(g->bitmap_left), + .y = static_cast(g->bitmap_top), + .was_packed = 0 + } + ); + } + } + ); + + // =============================== + + // todo: texture size? + const auto size = [&rects]() + { + Texture::size_type total_area = 0; + Texture::size_type max_width = 0; + Texture::size_type max_height = 0; + + for (const auto& [id, w, h, x, y, was_packed]: rects) + { + total_area += w * h; + max_width = std::ranges::max(max_width, static_cast(w)); + max_height = std::ranges::max(max_height, static_cast(h)); + } + + const auto min_side = static_cast(std::sqrt(total_area)); + const auto max_side = std::ranges::max(max_width, max_height); + + return std::bit_ceil(std::ranges::max(min_side, max_side)); + }(); + auto atlas_width = size; + auto atlas_height = size; + + stbrp_context rp_context; + std::vector nodes{atlas_width}; + while (true) + { + stbrp_init_target(&rp_context, static_cast(atlas_width), static_cast(atlas_height), nodes.data(), static_cast(nodes.size())); + if (stbrp_pack_rects(&rp_context, rects.data(), static_cast(rects.size()))) + { + break; + } + + atlas_width *= 2; + atlas_height *= 2; + nodes.resize(atlas_width); + } + + // =============================== + + // note: We don't necessarily overwrite all the memory, but it doesn't matter. + // auto texture_data = std::make_unique(static_cast(atlas_width * atlas_height)); + auto texture_data = std::make_unique_for_overwrite(static_cast(atlas_width * atlas_height)); + + const extent_type texture_uv_scale{1.f / static_cast(atlas_width), 1.f / static_cast(atlas_height)}; + + // =============================== + + for (const auto& [id, rect_width, rect_height, rect_x, rect_y, was_packed]: rects) + { + if (id == id_baked_line) + { + using value_type = point_type::value_type; + constexpr std::uint32_t white_color = 0xff'ff'ff'ff; + + if (font.white_pixel_uv.x < 0 or font.white_pixel_uv.y < 0) + { + // hacky: baked line rect area, one pixel + { + const auto x = rect_x + rect_y * atlas_width; + texture_data[x] = white_color; + + const auto uv_x = static_cast(static_cast(rect_x) + .5f) * texture_uv_scale.width; + const auto uv_y = static_cast(static_cast(rect_y) + .5f) * texture_uv_scale.height; + + font.white_pixel_uv = point_type{uv_x, uv_y}; + } + } + + // ◿ + for (stbrp_coord y = 0; y < rect_height; ++y) + { + const auto line_width = y; + const auto offset_y = (rect_y + y) * atlas_width; + + for (stbrp_coord x = line_width; x > 0; --x) + { + const auto offset_x = rect_x + (rect_width - x); + const auto index = offset_x + offset_y; + texture_data[index] = white_color; + } + + const auto p_x = rect_x + (rect_width - line_width); + const auto p_y = rect_y + y; + const auto width = line_width; + constexpr auto height = .5f; + + const auto uv_x = static_cast(p_x) * texture_uv_scale.width; + const auto uv_y = static_cast(p_y) * texture_uv_scale.height; + const auto uv_width = static_cast(width) * texture_uv_scale.width; + const auto uv_height = static_cast(height) * texture_uv_scale.height; + + font.baked_line_uv.emplace_back(uv_x, uv_y, uv_width, uv_height); + } + + continue; + } + + const auto c = std::bit_cast(id); + + // todo + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(c <= std::numeric_limits::max()); + + if (FT_Load_Char(face, c, FT_LOAD_RENDER)) + { + continue; + } + + const auto& g = face->glyph; + + for (std::uint32_t y = 0; y < g->bitmap.rows; ++y) + { + for (std::uint32_t x = 0; x < g->bitmap.width; ++x) + { + const auto index = rect_x + x + (rect_y + y) * atlas_width; + const auto a = g->bitmap.buffer[x + y * g->bitmap.pitch] << 24; + const auto color = + // A + a | + // B + std::uint32_t{0xff} << 16 | + // G + std::uint32_t{0xff} << 8 | + // R + std::uint32_t{0xff}; + texture_data[index] = color; + } + } + + const auto p_x = static_cast(g->bitmap_left); + const auto p_y = static_cast(g->bitmap_top); + const auto p_width = static_cast(g->bitmap.width); + const auto p_height = static_cast(g->bitmap.rows); + + const auto uv_x = static_cast(rect_x) * texture_uv_scale.width; + const auto uv_y = static_cast(rect_y) * texture_uv_scale.height; + const auto uv_width = static_cast(g->bitmap.width) * texture_uv_scale.width; + const auto uv_height = static_cast(g->bitmap.rows) * texture_uv_scale.height; + + const internal::Font::glyph_type glyph{ + .rect = {p_x, p_y, p_width, p_height}, + .uv = {uv_x, uv_y, uv_width, uv_height}, + .advance_x = static_cast(g->advance.x) / 64.f + }; + + font.glyphs.insert_or_assign(static_cast(c), glyph); + } + + font.fallback_glyph = font.glyphs[option.fallback_char]; + + // =============================== + + destroy_ft(ft); + + texture.width = atlas_width; + texture.height = atlas_height; + texture.data = std::move(texture_data); + return texture; + } +} + +namespace gal::prometheus::gui +{ + Texture::Texture(texture_id_type& texture_id) noexcept + : width{0}, + height{0}, + data{nullptr}, + id{texture_id} {} + + Texture::~Texture() noexcept + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME( + id != internal::Font::invalid_texture_id, + "Texture is not bound to a GPU resource id!" + ); + } + + auto Texture::valid() const noexcept -> bool + { + return data != nullptr; + } + + // ReSharper disable once CppMemberFunctionMayBeConst + auto Texture::bind(const texture_id_type texture_id) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME( + this->valid(), + "Only valid textures can be bound to a GPU resource id" + ); + + this->id.get() = texture_id; + } + + namespace internal + { + Font::Font() noexcept + : + pixel_height{std::numeric_limits::min()}, + baked_line_max_width{std::numeric_limits::min()}, + fallback_glyph{}, + scale{1}, + display_offset{0, 0}, + texture_id{invalid_texture_id} {} + + auto Font::load(const FontOption& option) noexcept -> Texture + { + auto texture = do_load_font(option, *this); + return texture; + } + + auto Font::loaded() const noexcept -> bool + { + return not glyphs.empty() and texture_id != invalid_texture_id; + } + + auto text_size( + const Font& font, + const std::basic_string_view utf8_text, + const float font_size, + const float wrap_width, + std::basic_string& out_text + ) noexcept -> extent_type + { + // todo + auto utf16_text = chars::convert(utf8_text); + static_assert(std::is_same_v); + + const auto line_height = font_size; + const auto scale = line_height / (static_cast(font.pixel_height) * font.scale); + const auto& glyphs = font.glyphs; + const auto& fallback_glyph = font.fallback_glyph; + + float max_width = 0; + float current_width = 0; + float total_height = line_height; + + auto it_input_current = utf16_text.begin(); + const auto it_input_end = utf16_text.end(); + + while (it_input_current != it_input_end) + { + const auto this_char = *it_input_current; + it_input_current += 1; + + if (this_char == u'\n') + { + max_width = std::ranges::max(max_width, current_width); + current_width = 0; + total_height += line_height; + } + else + { + const auto& [glyph_rect, glyph_uv, glyph_advance_x] = [&glyphs, &fallback_glyph](const auto c) -> const auto& + { + if (const auto it = glyphs.find(c); + it != glyphs.end()) + { + return it->second; + } + + return fallback_glyph; + }(this_char); + + if (const auto advance_x = glyph_advance_x * scale; + current_width + advance_x > wrap_width) + { + max_width = std::ranges::max(max_width, current_width); + current_width = advance_x; + total_height += line_height; + } + else + { + current_width += advance_x; + } + } + } + + max_width = std::ranges::max(max_width, current_width); + out_text = std::move(utf16_text); + + return {max_width, total_height}; + } + + auto text_size( + const Font& font, + const std::basic_string_view utf8_text, + const float font_size, + const float wrap_width + ) noexcept -> extent_type + { + std::basic_string out; + return text_size(font, utf8_text, font_size, wrap_width, out); + } + + auto text_draw( + const Font& font, + const std::basic_string_view utf8_text, + const float font_size, + const float wrap_width, + const point_type point, + const Theme::color_type color, + PrimitiveAppender appender + ) noexcept -> void + { + // todo + auto utf16_text = chars::convert(utf8_text); + static_assert(std::is_same_v); + + const auto newline_count = std::ranges::count(utf16_text, u'\n'); + const auto text_size_exclude_newline = utf16_text.size() - newline_count; + + const auto vertex_count = 4 * text_size_exclude_newline; + const auto index_count = 6 * text_size_exclude_newline; + appender.reserve(vertex_count, index_count); + + const auto line_height = font_size; + const auto scale = line_height / static_cast(font.pixel_height) * font.scale; + const auto& glyphs = font.glyphs; + const auto& fallback_glyph = font.fallback_glyph; + + auto cursor = point + point_type{0, line_height}; + + auto it_input_current = utf16_text.begin(); + const auto it_input_end = utf16_text.end(); + + while (it_input_current != it_input_end) + { + const auto this_char = *it_input_current; + it_input_current += 1; + + if (this_char == u'\n') + { + cursor.x = point.x; + cursor.y += line_height; + continue; + } + + const auto& [glyph_rect, glyph_uv, glyph_advance_x] = [&glyphs, &fallback_glyph](const auto c) -> const auto& + { + if (const auto it = glyphs.find(c); + it != glyphs.end()) + { + return it->second; + } + + return fallback_glyph; + }(this_char); + + const auto advance_x = glyph_advance_x * scale; + if (cursor.x + advance_x > point.x + wrap_width) + { + cursor.x = point.x; + cursor.y += line_height; + } + + const rect_type char_rect + { + cursor + point_type{glyph_rect.left_top().x, -glyph_rect.left_top().y} * scale, + glyph_rect.size() * scale + }; + cursor.x += advance_x; + + const auto current_vertex_index = static_cast(appender.vertex_count()); + + appender.add_vertex(char_rect.left_top(), glyph_uv.left_top(), color); + appender.add_vertex(char_rect.right_top(), glyph_uv.right_top(), color); + appender.add_vertex(char_rect.right_bottom(), glyph_uv.right_bottom(), color); + appender.add_vertex(char_rect.left_bottom(), glyph_uv.left_bottom(), color); + + appender.add_index(current_vertex_index + 0, current_vertex_index + 1, current_vertex_index + 2); + appender.add_index(current_vertex_index + 0, current_vertex_index + 2, current_vertex_index + 3); + } + } + } +} diff --git a/src/gui/internal/font.hpp b/src/gui/internal/font.hpp new file mode 100644 index 00000000..b8c708a2 --- /dev/null +++ b/src/gui/internal/font.hpp @@ -0,0 +1,99 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include + +#include + +namespace gal::prometheus::gui::internal +{ + class Font final + { + public: + using value_type = FontOption::value_type; + using char_type = FontOption::char_type; + + #if defined(GAL_PROMETHEUS_COMPILER_MSVC) + GAL_PROMETHEUS_COMPILER_DISABLE_WARNING_PUSH + // Variable '%1$s' is uninitialized. Always initialize a member variable (type.6). + GAL_PROMETHEUS_COMPILER_DISABLE_WARNING(26495) + #endif + + struct glyph_type + { + rect_type rect; + rect_type uv; + float advance_x; + }; + + #if defined(GAL_PROMETHEUS_COMPILER_MSVC) + GAL_PROMETHEUS_COMPILER_DISABLE_WARNING_POP + #endif + + using glyphs_type = std::unordered_map; + + using glyph_value_type = FontOption::glyph_value_type; + using glyph_ranges_type = FontOption::glyph_ranges_type; + + using baked_line_uv_type = std::vector; + + constexpr static texture_id_type invalid_texture_id = 0; + + // Line breaks only when a line break ('\n') is encountered (this means that text beyond the content area will be clipped) + constexpr static value_type no_auto_wrap = 99999999.f; + + std::string font_path; + std::uint32_t pixel_height; + std::uint32_t baked_line_max_width; + + glyphs_type glyphs; + glyph_type fallback_glyph; + + value_type scale; + extent_type display_offset; + point_type white_pixel_uv; + baked_line_uv_type baked_line_uv; + + texture_id_type texture_id; + + Font() noexcept; + + auto load(const FontOption& option) noexcept -> Texture; + + // --------------------------------------------------------- + + [[nodiscard]] auto loaded() const noexcept -> bool; + }; + + // ========================================= + // DRAW TEXT + + [[nodiscard]] auto text_size( + const Font& font, + std::basic_string_view utf8_text, + float font_size, + float wrap_width, + std::basic_string& out_text + ) noexcept -> extent_type; + + [[nodiscard]] auto text_size( + const Font& font, + std::basic_string_view utf8_text, + float font_size, + float wrap_width + ) noexcept -> extent_type; + + auto text_draw( + const Font& font, + std::basic_string_view utf8_text, + float font_size, + float wrap_width, + point_type point, + Theme::color_type color, + PrimitiveAppender appender + ) noexcept -> void; +} diff --git a/src/gui/internal/gui.inl b/src/gui/internal/gui.inl new file mode 100644 index 00000000..c3943c0a --- /dev/null +++ b/src/gui/internal/gui.inl @@ -0,0 +1,104 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +namespace gal::prometheus::gui +{ + template + auto draw_radio_button(Context& context, std::string_view utf8_text, T& reference, const std::type_identity_t& identifier) noexcept -> bool + { + const auto prev_selected = reference == identifier; + const auto pressed = gui::draw_radio_button(context, utf8_text, prev_selected); + + if (pressed) + { + reference = identifier; + } + return pressed; + } + + template + auto draw_checkbox( + Context& context, + const std::string_view utf8_text, + T& reference, + const std::type_identity_t& checked_identifier, + const std::type_identity_t& unchecked_identifier + ) noexcept -> bool + { + const auto prev_checked = reference == checked_identifier; + const auto checked = gui::draw_checkbox(context, utf8_text, prev_checked); + + if (checked != prev_checked) + { + if (prev_checked) + { + reference = unchecked_identifier; + } + else + { + reference = checked_identifier; + } + } + return checked; + } + + template + requires std::is_arithmetic_v + auto draw_slider( + Context& context, + const std::string_view utf8_text, + T& reference, + const ValueType min, + const std::type_identity_t max, + const std::uint32_t decimal_precision, + const float power + ) noexcept -> bool + { + auto v = static_cast(reference); + + const auto result = gui::draw_slider( + context, + utf8_text, + v, + static_cast(min), + static_cast(max), + decimal_precision, + power + ); + reference = v; + + return result; + } + + template + requires std::is_arithmetic_v + auto draw_slider( + Context& context, + T&& object, + const ValueType min, + const std::type_identity_t max, + const std::uint32_t decimal_precision, + const float power + ) noexcept -> bool + { + auto& ref = meta::member_of_name(std::forward(object)); + auto v = static_cast(ref); + + const auto result = gui::draw_slider( + context, + MemberName, + v, + static_cast(min), + static_cast(max), + decimal_precision, + power + ); + ref = v; + + return result; + } +} diff --git a/src/gui/internal/mouse.cpp b/src/gui/internal/mouse.cpp new file mode 100644 index 00000000..21bd9be0 --- /dev/null +++ b/src/gui/internal/mouse.cpp @@ -0,0 +1,148 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include +#include + +#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE + +namespace gal::prometheus::gui::internal +{ + auto Mouse::is_down(const Context& context, MouseKey key) const noexcept -> bool + { + std::ignore = context; + + const auto index = static_cast(key); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(index < mouse_key_count); + + return key_statuses[index].down; + } + + auto Mouse::is_clicked(const Context& context, MouseKey key, const bool repeat) const noexcept -> bool + { + const auto& io = context.io(); + + const auto index = static_cast(key); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(index < mouse_key_count); + + const auto& key_status = key_statuses[index]; + const auto down_time = key_status.down_time; + + if (down_time == Mouse::time_just_start) // NOLINT(clang-diagnostic-float-equal) + { + return true; + } + + if (repeat and down_time > io.mouse_repeat_click_delay) + { + const auto v1 = std::fmodf(down_time - io.mouse_repeat_click_delay, io.mouse_repeat_click_rate) > io.mouse_repeat_click_rate * .5f; + const auto v2 = std::fmodf(down_time - io.delta_time, io.mouse_repeat_click_rate) > io.mouse_repeat_click_rate * .5f; + + return v1 != v2; + } + + return false; + } + + auto Mouse::is_double_clicked(const Context& context, MouseKey key) const noexcept -> bool + { + std::ignore = context; + + const auto index = static_cast(key); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(index < mouse_key_count); + + const auto& key_status = key_statuses[index]; + + return key_status.double_clicked; + } + + auto Mouse::tick(const Context& context) noexcept -> void + { + // ---------------------- + // read context + const auto& io = context.io(); + const auto time = context.current_time(); + + position_current = io.mouse_position; + wheel = io.mouse_wheel; + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(key_statuses.size() == io.mouse_button_state.state.size()); + key_statuses[static_cast(MouseKey::LEFT)].down = io.mouse_button_state[MouseKey::LEFT]; + key_statuses[static_cast(MouseKey::MIDDLE)].down = io.mouse_button_state[MouseKey::MIDDLE]; + key_statuses[static_cast(MouseKey::RIGHT)].down = io.mouse_button_state[MouseKey::RIGHT]; + key_statuses[static_cast(MouseKey::X1)].down = io.mouse_button_state[MouseKey::X1]; + key_statuses[static_cast(MouseKey::X2)].down = io.mouse_button_state[MouseKey::X2]; + + // ---------------------- + // update mouse state + + if (position_current.x < 0 and position_current.y < 0) + { + position_current = position_unknown; + } + + if (position_current == position_unknown and position_previous == position_unknown) + { + // mouse just appeared or disappeared + position_delta = {0, 0}; + } + else + { + position_delta = (position_current - position_previous).to(); + } + position_previous = position_current; + + static_assert(time_not_start < 0); + std::ranges::for_each( + key_statuses, + [&](auto& key_status) noexcept -> void + { + if (key_status.down) + { + if (key_status.down_time < 0) + { + // not start + key_status.down_time = time_just_start; + } + else + { + key_status.down_time += io.delta_time; + } + } + else + { + key_status.down_time = time_not_start; + } + + // just start + key_status.clicked = key_status.down_time == time_just_start; // NOLINT(clang-diagnostic-float-equal) + key_status.double_clicked = false; + + if (key_status.clicked) + { + // time_not_start < 0 + if (time - key_status.click_time < io.mouse_double_click_interval_threshold) + { + if (position_current.distance(key_status.click_position) < io.mouse_double_click_distance_threshold) + { + key_status.double_clicked = true; + } + + // so the third click isn't turned into a double click + key_status.click_time = time_not_start; + } + else + { + key_status.click_time = time; + key_status.click_position = position_current; + } + } + } + ); + } +} diff --git a/src/gui/internal/mouse.hpp b/src/gui/internal/mouse.hpp new file mode 100644 index 00000000..e62b8b20 --- /dev/null +++ b/src/gui/internal/mouse.hpp @@ -0,0 +1,62 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include + +namespace gal::prometheus::gui::internal +{ + class Mouse final + { + public: + using value_type = extent_type::value_type; + + // < 0 + constexpr static point_type position_unknown{-.999999f, -.999999f}; + // < 0 + constexpr static auto time_not_start = time_type{-.999999f}; + constexpr static auto time_just_start = time_type{0.f}; + + struct key_status_type + { + bool down{false}; + bool pad{false}; + + bool clicked{false}; + bool double_clicked{false}; + + // Duration of the "pressed" state of the key + time_type down_time{time_not_start}; + + // Time of last press + time_type click_time{time_not_start}; + // Position of last press + point_type click_position{position_unknown}; + }; + + point_type position_current{position_unknown}; + point_type position_previous{position_unknown}; + extent_type position_delta{0, 0}; + + std::array key_statuses{}; + + value_type wheel{0}; + + // --------------------------------------------- + // STATE + + [[nodiscard]] auto is_down(const Context& context, MouseKey key) const noexcept -> bool; + + [[nodiscard]] auto is_clicked(const Context& context, MouseKey key, bool repeat = false) const noexcept -> bool; + + [[nodiscard]] auto is_double_clicked(const Context& context, MouseKey key) const noexcept -> bool; + + // --------------------------------------------- + // UPDATE (PER FRAME) + + auto tick(const Context& context) noexcept -> void; + }; +} diff --git a/src/gui/internal/window.cpp b/src/gui/internal/window.cpp new file mode 100644 index 00000000..9db8c771 --- /dev/null +++ b/src/gui/internal/window.cpp @@ -0,0 +1,2696 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +#include + +#include +#include + +#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE + +namespace +{ + using namespace gal::prometheus; + using namespace gui; + + // ReSharper disable once CppInconsistentNaming + using MouseState = Context::MouseState; +} + +namespace gal::prometheus::gui::internal +{ + class Window::IdMaker final + { + public: + memory::RefWrapper self; + + private: + [[nodiscard]] static auto make(const std::string_view string) noexcept -> widget_id_type + { + return functional::hash(string); + } + + [[nodiscard]] static auto make(const widget_id_type seed, const std::string_view string) noexcept -> widget_id_type + { + return functional::hash_combine_2(seed, make(string)); + } + + [[nodiscard]] static auto make(const void* pointer) noexcept -> widget_id_type + { + return static_cast(reinterpret_cast(pointer)); + } + + [[nodiscard]] static auto make(const widget_id_type seed, const void* pointer) noexcept -> widget_id_type + { + return functional::hash_combine_2(seed, make(pointer)); + } + + [[nodiscard]] static auto make(const widget_id_type value) noexcept -> widget_id_type + { + const auto* pointer = reinterpret_cast(static_cast(value)); // NOLINT(performance-no-int-to-ptr) + return make(pointer); + } + + [[nodiscard]] static auto make(const widget_id_type seed, const widget_id_type value) noexcept -> widget_id_type + { + return functional::hash_combine_2(seed, make(value)); + } + + public: + auto make_id() noexcept -> void + { + auto& window = self.get(); + + const auto id = make(window.name_); + window.id_stack_.push_back(id); + } + + [[nodiscard]] auto make_id(Context& context, const std::string_view string) const noexcept -> widget_id_type + { + auto& window = self.get(); + + // id_stack.front() -> window id + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not window.id_stack_.empty()); + + const auto seed = window.id_stack_.back(); + const auto id = make(seed, string); + + context.mark_widget_alive(id); + + return id; + } + + [[nodiscard]] auto make_id(Context& context, const void* pointer) const noexcept -> widget_id_type + { + auto& window = self.get(); + + // id_stack.front() -> window id + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not window.id_stack_.empty()); + + const auto seed = window.id_stack_.back(); + const auto id = make(seed, pointer); + + context.mark_widget_alive(id); + + return id; + } + + [[nodiscard]] auto make_id(Context& context, const widget_id_type value) const noexcept -> widget_id_type + { + auto& window = self.get(); + + // id_stack.front() -> window id + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not window.id_stack_.empty()); + + const auto seed = window.id_stack_.back(); + const auto id = make(seed, value); + + context.mark_widget_alive(id); + + return id; + } + }; + + class Window::Drawer final + { + public: + memory::RefWrapper self; + + // ----------------------------------- + // FONT + + [[nodiscard]] auto font_size(const Context& context) const noexcept -> value_type + { + std::ignore = this; + + const auto& font = context.current_font(); + + // todo: scale? + return static_cast(font.pixel_height) * font.scale; + } + + // ----------------------------------- + // PADDING + + [[nodiscard]] auto window_padding(const Context& context) const noexcept -> extent_type + { + const auto& window = self.get(); + + const auto& theme = context.current_theme(); + + if (window.flag_.is() and not window.flag_.is()) + { + return {1, 1}; + } + + return theme.window_padding; + } + + // ----------------------------------- + // TITLEBAR + + [[nodiscard]] auto titlebar_height(const Context& context) const noexcept -> value_type + { + const auto& window = self.get(); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not window.flag_.is()); + + const auto& theme = context.current_theme(); + + return font_size(context) + theme.item_frame_padding.height * 2; + } + + [[nodiscard]] auto titlebar_rect(const Context& context) const noexcept -> rect_type + { + const auto& window = self.get(); + + return {window.point_, window.size_full_.width, titlebar_height(context)}; + } + + [[nodiscard]] auto titlebar_rect(const Context& context, const value_type height) const noexcept -> rect_type + { + std::ignore = context; + + const auto& window = self.get(); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(window.flag_.is()); + + return {window.point_, window.size_full_.width, height}; + } + + // ----------------------------------- + // RESIZE-GRIP + + [[nodiscard]] auto resize_grip_size(const Context& context) const noexcept -> extent_type + { + std::ignore = this; + + const auto& theme = context.current_theme(); + + return theme.window_resize_grip_size; + } + + [[nodiscard]] auto resize_grip_rect(const Context& context) const noexcept -> rect_type + { + const auto& window = self.get(); + + const auto s = resize_grip_size(context); + + // RIGHT-BOTTOM-CORNER + const point_type p{window.point_.x + window.size_.width - s.width, window.point_.y + window.size_.height - s.height}; + return {p, s}; + } + + // ----------------------------------- + // CLOSE BUTTON + + [[nodiscard]] auto close_button_size(const Context& context) const noexcept -> extent_type + { + const auto& window = self.get(); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not window.flag_.is()); + + const auto height = titlebar_height(context); + + return {height - 4, height - 4}; + } + + [[nodiscard]] auto close_button_rect(const Context& context) const noexcept -> rect_type + { + const auto& window = self.get(); + + const auto s = close_button_size(context); + + // LEFT-TOP-CORNER + const point_type p{window.point_.x + window.size_.width - s.width - 3, window.point_.y + 2}; + return {p, s}; + } + + // ----------------------------------- + // CANVAS + + auto adjust_item_size(const Context& context, const extent_type& size) noexcept -> void + { + const auto& theme = context.current_theme(); + + auto& window = self.get(); + auto& canvas = window.canvas_; + + const auto line_height = std::ranges::max(canvas.height_current_line, size.height); + + // Always align ourselves on pixel boundaries + canvas.cursor_previous_line = + // previous point + canvas.cursor_current_line + + // X: item width + // Y: 0 + extent_type{size.width, 0}; + canvas.cursor_current_line = + // todo X: start point X + columns offset + // Y: previous Y + item height + item spacing + {window.point_.x + 8, canvas.cursor_current_line.y + line_height + theme.item_spacing.height}; + // window.point_ + extent_type{8, canvas.cursor_current_line.y + line_height + theme.item_spacing.height}; + + canvas.height_previous_line = line_height; + canvas.height_current_line = 0; + + window.size_of_content_ = window.size_of_content_.combine_max(extent_type{canvas.cursor_previous_line.x, canvas.cursor_current_line.y + window.scroll_y_} - window.point_); + } + + // cursor position is relative to window position + [[nodiscard]] auto cursor_position(const Context& context) const noexcept -> point_type + { + std::ignore = context; + + const auto& window = self.get(); + const auto& canvas = window.canvas_; + + return canvas.cursor_previous_line - window.point_; + } + + // cursor position in screen space + [[nodiscard]] auto cursor_screen_position(const Context& context) const noexcept -> point_type + { + std::ignore = context; + + const auto& window = self.get(); + const auto& canvas = window.canvas_; + + return canvas.cursor_current_line; + } + + auto set_cursor_position_x(const Context& context, const value_type x) noexcept -> void + { + std::ignore = context; + + auto& window = self.get(); + auto& canvas = window.canvas_; + + canvas.cursor_current_line.x = window.point_.x + x; + } + + auto set_cursor_position_y(const Context& context, const value_type y) noexcept -> void + { + std::ignore = context; + + auto& window = self.get(); + auto& canvas = window.canvas_; + + canvas.cursor_current_line.y = window.point_.y + y; + } + + auto set_cursor_position(const Context& context, const point_type& point) noexcept -> void + { + set_cursor_position_x(context, point.x); + set_cursor_position_y(context, point.y); + } + + auto set_scroll_position(const Context& context, const value_type y) noexcept -> void + { + std::ignore = context; + + auto& window = self.get(); + + window.scroll_next_y_ = y; + } + + // adjust scrolling position to center into the current cursor position + auto set_scroll_position_here(const Context& context) noexcept -> void + { + const auto& window = self.get(); + const auto& canvas = window.canvas_; + + const auto middle = window.point_.y + window.size_full_.height * .5f; + + auto y = canvas.cursor_current_line.y - window.scroll_y_ - window_padding(context).height; + + y -= middle; + if (not window.flag_.is()) + { + y -= titlebar_height(context); + } + + set_scroll_position(context, y); + } + + // auto test_last_item(const Context& context, const rect_type& rect) noexcept -> void + // { + // auto& window = self.get(); + // auto& canvas = window.canvas_; + // + // canvas.last_item_rect = rect; + // canvas.last_item_focused = false; + // canvas.last_item_hovered = window.is_hovered(context, rect); + // } + // + // [[nodiscard]] auto is_visible_area(const Context& context, const rect_type& rect) const noexcept -> bool + // { + // std::ignore = context; + // + // auto& window = self.get(); + // + // const auto& last = window.clip_rect_stack_.back(); + // return last.intersects(rect); + // } + + [[nodiscard]] auto test_last_item_visible(const Context& context, const rect_type& rect) noexcept -> bool + { + auto& window = self.get(); + auto& canvas = window.canvas_; + + const auto& last = window.clip_rect_stack_.back(); + const auto visible = last.intersects(rect); + + canvas.last_item_rect = rect; + canvas.last_item_focused = false; + canvas.last_item_hovered = visible ? window.is_hovered(context, rect) : false; + + return visible; + } + + // ----------------------------------- + // FRAME + + auto draw_widget_frame(const Context& context, const rect_type& rect, const color_type color) noexcept -> void + { + const auto& theme = context.current_theme(); + + auto& window = self.get(); + + window.draw_list_.rect_filled(rect, color); + if (window.flag_.is()) + { + const point_type outer_point{rect.left_top() + extent_type{.5f, .5f}}; + const extent_type outer_size{rect.size() - extent_type{.5f, .5f}}; + + const point_type inner_point{rect.left_top() + extent_type{1.5f, 1.5f}}; + const extent_type inner_size{rect.size() - extent_type{.5f, .5f}}; + + window.draw_list_.rect({inner_point, inner_size}, context.color_of(theme, ThemeCategory::BORDER_SHADOW)); + window.draw_list_.rect({outer_point, outer_size}, context.color_of(theme, ThemeCategory::BORDER)); + } + } + + auto draw_widget_frame(const Context& context, const circle_type& circle, const color_type color) noexcept -> void + { + const auto& theme = context.current_theme(); + + auto& window = self.get(); + + window.draw_list_.circle_filled(circle, color); + if (window.flag_.is()) + { + const point_type outer_point{circle.center() + extent_type{.5f, .5f}}; + const auto outer_radius = circle.radius - .5f; + + const point_type inner_point{circle.center() + extent_type{1.5f, 1.5f}}; + const auto inner_radius = circle.radius - .5f; + + window.draw_list_.circle({inner_point, inner_radius}, context.color_of(theme, ThemeCategory::BORDER_SHADOW)); + window.draw_list_.circle({outer_point, outer_radius}, context.color_of(theme, ThemeCategory::BORDER)); + } + } + + auto draw_widget_frame(const Context& context, const point_type& a, const point_type& b, const point_type& c) noexcept -> void + { + const auto& theme = context.current_theme(); + + auto& window = self.get(); + + if (window.flag_.is()) + { + const auto offset_a = a + extent_type{1.5f, 1.5f}; + const auto offset_b = b + extent_type{1.5f, 1.5f}; + const auto offset_c = c + extent_type{1.5f, 1.5f}; + + window.draw_list_.triangle_filled(offset_a, offset_b, offset_c, context.color_of(theme, ThemeCategory::BORDER_SHADOW)); + } + window.draw_list_.triangle_filled(a, b, c, context.color_of(theme, ThemeCategory::BORDER)); + } + }; + + class Window::Anonymous final + { + public: + memory::RefWrapper self; + + template + requires (std::is_same_v or std::is_same_v>) + auto draw_slider( + Context& context, + const std::string_view utf8_text, + T& reference, + const float min, + const float max, + const std::uint32_t decimal_precision, + const float power + ) noexcept -> bool + { + constexpr auto is_list = std::is_same_v>; + if constexpr (is_list) + { + const auto list_size = reference.size(); + if (list_size == 1) + { + return this->draw_slider(context, utf8_text, reference[0], min, max, decimal_precision, power); + } + } + + auto& window = self.get(); + + if (window.skip_item_) + { + return false; + } + window.accessed_ = true; + + const auto& theme = context.current_theme(); + const auto& font = context.current_font(); + Drawer drawer{.self = window}; + const IdMaker id_maker{.self = window}; + + const auto font_size = drawer.font_size(context); + + const auto do_draw_slider = [&](const widget_id_type id, ValueType& ref_value, const rect_type& slider_rect, const rect_type& frame_rect) noexcept -> bool + { + const auto slider_point = slider_rect.left_top(); + const auto slider_size = slider_rect.size(); + + const auto frame_point = frame_rect.left_top(); + const auto frame_size = frame_rect.size(); + + const auto state = context.test_mouse(id, slider_rect, false); + + // draw □ (frame + slider + text) + bool value_changed = false; + + // frame + drawer.draw_widget_frame(context, frame_rect, context.color_of(theme, ThemeCategory::FRAME_BACKGROUND)); + + // slider + + // todo + constexpr float grab_size_in_pixels = 10.f; + + const auto slider_effective_width = slider_size.width - grab_size_in_pixels; + const auto slider_effective_x1 = slider_point.x + grab_size_in_pixels * .5f; + const auto slider_effective_x2 = slider_point.x + slider_size.width - grab_size_in_pixels * .5f; + + const auto linear_zero_pos = [=]() noexcept -> float + { + if (min * max < 0) + { + // different sign + const auto linear_dist_min_to_0 = std::powf(std::fabs(.0f - min), 1.f / power); + const auto linear_dist_max_to_0 = std::powf(std::fabs(max - .0f), 1.f / power); + return linear_dist_min_to_0 / (linear_dist_min_to_0 + linear_dist_max_to_0); + } + + // same sign + return min < 0 ? 1.f : .0f; + }(); + + if (state & MouseState::KEEPING) + { + const auto mouse_position = context.mouse().position_current; + const auto normalized_x = std::ranges::clamp((mouse_position.x - slider_effective_x1) / slider_effective_width, .0f, 1.f); + + // account for logarithmic scale on both sides of the zero + auto value = [=]() noexcept -> float + { + if (normalized_x < linear_zero_pos) + { + // rescale to the negative range before powering + auto v = 1.f - (normalized_x / linear_zero_pos); + v = std::powf(v, power); + + const auto negative_part_max = std::ranges::min(max, .0f); + return std::lerp(negative_part_max, min, v); + } + + // rescale to the positive range before powering + auto v = normalized_x; + if (std::fabs(linear_zero_pos - 1.f) > 1e-6) + { + v = (v - linear_zero_pos) / (1.f - linear_zero_pos); + } + v = std::powf(v, power); + + const auto positive_part_min = std::ranges::max(min, .0f); + return std::lerp(positive_part_min, max, v); + }(); + + const auto min_step = 1.f / std::powf(10.f, static_cast(decimal_precision)); + const auto remainder = std::fmodf(value, min_step); + + if (remainder <= min_step * .5f) + { + value -= remainder; + } + else + { + value += (min_step - remainder); + } + + // if (ref_value != value) // NOLINT(clang-diagnostic-float-equal) + if (std::fabs(ref_value - value) > 1e-6f) + { + ref_value = value; + value_changed = true; + } + } + + // grab + { + const auto v = [=]() noexcept -> float + { + const auto clamped = std::ranges::clamp(ref_value, min, max); + if (clamped < .0f) + { + const auto f = 1.f - (clamped - min) / (std::ranges::min(.0f, max) - min); + return (1.f - std::powf(f, 1.f / power)) * linear_zero_pos; + } + + const auto f = (clamped - std::ranges::max(0.f, min)) / (max - std::ranges::max(0.f, min)); + return linear_zero_pos + std::powf(f, 1.f / power) * (1.f - linear_zero_pos); + }(); + + const auto x = std::lerp(slider_effective_x1, slider_effective_x2, v); + const point_type grab_point{x - grab_size_in_pixels * .5f, frame_point.y + 2.f}; + const extent_type grab_size{grab_size_in_pixels, frame_size.height - 4.f}; + const rect_type grab_rect{grab_point, grab_size}; + + if (state & MouseState::PRESSED) + { + window.draw_list_.rect_filled( + grab_rect, + context.color_of(theme, ThemeCategory::SLIDER_ACTIVATED), + theme.window_corner_rounding, + DrawFlag::ROUND_CORNER_ALL + ); + } + else + { + window.draw_list_.rect_filled( + grab_rect, + context.color_of(theme, ThemeCategory::SLIDER), + theme.window_corner_rounding, + DrawFlag::ROUND_CORNER_ALL + ); + } + } + + // text + const auto value_text = std::format("{:.{}f}", ref_value, decimal_precision); + const auto value_text_size = internal::text_size(font, value_text, font_size, Font::no_auto_wrap); + const point_type value_text_point{slider_point.x + slider_size.width / 2 - value_text_size.width / 2, frame_point.y + theme.item_frame_padding.height}; + window.draw_list_.text( + font, + font_size, + value_text_point, + context.color_of(theme, ThemeCategory::TEXT), + value_text + ); + + return value_changed; + }; + + const auto text_size = internal::text_size(font, utf8_text, font_size, Font::no_auto_wrap); + + const auto last_item_width = window.canvas_.item_width.back(); + + const auto total_frame_point = window.canvas_.cursor_current_line; + const auto total_frame_size = extent_type{last_item_width, text_size.height} + theme.item_frame_padding * 2; + const rect_type total_frame_rect{total_frame_point, total_frame_size}; + + const auto total_slider_point = total_frame_point + theme.item_frame_padding; + const auto total_slider_size = total_frame_size - theme.item_frame_padding * 2; + const rect_type total_slider_rect{total_slider_point, total_slider_size}; + + drawer.adjust_item_size(context, total_frame_size); + + // □ text / □ □ .. □ □ text + window.same_line(context, auto_size, theme.item_inner_spacing.width); + + // text + const auto text_point = window.canvas_.cursor_current_line + theme.item_frame_padding; + const rect_type text_rect{text_point, text_size}; + drawer.adjust_item_size(context, text_size); + + const rect_type total_rect{total_frame_point, text_rect.right_bottom()}; + // if (not drawer.is_visible_area(context, total_rect)) + // { + // // invisible + // return false; + // } + // drawer.test_last_item(context, total_rect); + if (not drawer.test_last_item_visible(context, total_rect)) + { + // invisible + return false; + } + + bool value_changed = false; + + if constexpr (is_list) + { + const auto list_size = reference.size(); + + // width: (total - spacing) / size + // height: total + const auto each_frame_size = extent_type + { + (total_frame_size.width - theme.item_inner_spacing.width * static_cast(list_size - 1)) / static_cast(list_size), + total_frame_size.height + }; + const auto each_slider_size = each_frame_size - theme.item_frame_padding * 2; + + const auto offset_x = each_frame_size.width + theme.item_inner_spacing.width; + + // draw n slider + // note: when drawing multiple sliders, the widget id is based on the slider index, not the label text + // note: to avoid different sliders getting the same id (since ids are based on index), the label text is pushed in here as the seed + window.push_id(context, utf8_text); + { + for (const auto view = reference | std::views::enumerate; + auto [index, ref_value]: view) + { + const auto id = id_maker.make_id(context, index); + + // x: total + offset * n + // y: total + const point_type frame_point + { + total_frame_point.x + offset_x * static_cast(index), + total_frame_point.y + }; + const rect_type frame_rect{frame_point, each_frame_size}; + + // x: total + offset * n + // y: total + const point_type slider_point + { + total_slider_point.x + offset_x * static_cast(index), + total_slider_point.y + }; + const rect_type slider_rect{slider_point, each_slider_size}; + + value_changed |= do_draw_slider(id, ref_value, slider_rect, frame_rect); + } + } + window.pop_id(context); + } + else + { + // draw one slider + const auto id = id_maker.make_id(context, utf8_text); + value_changed |= do_draw_slider(id, reference, total_slider_rect, total_frame_rect); + } + + // draw text + window.draw_list_.text( + font, + font_size, + text_rect.left_top(), + context.color_of(theme, ThemeCategory::TEXT), + utf8_text, + text_rect.width() + ); + + return value_changed; + } + + template + auto draw_combo( + Context& context, + const std::string_view utf8_text, + const std::span selections, + std::size_t& selected, + const std::size_t show_selection_count + ) noexcept -> bool + { + auto& window = self.get(); + + if (window.skip_item_) + { + return false; + } + window.accessed_ = true; + + const auto& theme = context.current_theme(); + const auto& font = context.current_font(); + Drawer drawer{.self = window}; + const IdMaker id_maker{.self = window}; + + const auto font_size = drawer.font_size(context); + + const auto text_size = internal::text_size(font, utf8_text, font_size, Font::no_auto_wrap); + + const auto last_item_width = window.canvas_.item_width.back(); + + const auto frame_point = window.canvas_.cursor_current_line; + const auto frame_size = extent_type{last_item_width, text_size.height} + theme.item_frame_padding * 2; + const rect_type frame_rect{frame_point, frame_size}; + + const auto drop_down_button_width = font_size + theme.item_frame_padding.width * 2; + const auto dropdown_button_point = frame_rect.right_top() - extent_type{drop_down_button_width, 0}; + const auto dropdown_button_size = extent_type{drop_down_button_width, drop_down_button_width}; + const rect_type dropdown_button_rect{dropdown_button_point, dropdown_button_size}; + + const auto selection_point = frame_point + theme.item_frame_padding; + const auto selection_size = frame_size - theme.item_frame_padding * 2; + // const rect_type selection_rect{selection_point, selection_size}; + + drawer.adjust_item_size(context, frame_size); + + // □ text + window.same_line(context, auto_size, theme.item_inner_spacing.width); + + // text + const auto text_point = window.canvas_.cursor_current_line + theme.item_frame_padding; + const rect_type text_rect{text_point, text_size}; + drawer.adjust_item_size(context, text_size); + + const rect_type total_rect{frame_point, text_rect.right_bottom()}; + // if (not drawer.is_visible_area(context, total_rect)) + // { + // // invisible + // return false; + // } + // drawer.test_last_item(context, total_rect); + if (not drawer.test_last_item_visible(context, total_rect)) + { + // invisible + return false; + } + + bool value_changed = false; + + const auto id = id_maker.make_id(context, utf8_text); + // note: Leave the frame inactive so that clicking on it again does not accidentally re-open the dropdown window again + const auto state = context.queue_mouse(id, frame_rect); + + // draw frame + drawer.draw_widget_frame(context, frame_rect, context.color_of(theme, ThemeCategory::FRAME_BACKGROUND)); + + // draw dropdown button frame + { + if (state & MouseState::HOVERED) + { + drawer.draw_widget_frame(context, dropdown_button_rect, context.color_of(theme, ThemeCategory::BUTTON_HOVERED)); + } + else + { + drawer.draw_widget_frame(context, dropdown_button_rect, context.color_of(theme, ThemeCategory::BUTTON)); + } + + const auto r = font_size * .5f; + const auto center = dropdown_button_rect.center() - extent_type{0, r * .25f}; + + const auto a = center + extent_type{0, 1} * r; + const auto b = center + extent_type{-.667f, -.5f} * r; + const auto c = center + extent_type{.667f, -.5f} * r; + + drawer.draw_widget_frame(context, a, b, c); + } + + // fixme: leave empty or default? + if (selected >= selections.size()) + { + window.draw_list_.text( + font, + font_size, + selection_point, + context.color_of(theme, ThemeCategory::TEXT), + "", + selection_size.width + ); + } + else + { + const auto& string = selections[selected]; + + window.draw_list_.text( + font, + font_size, + selection_point, + context.color_of(theme, ThemeCategory::TEXT), + string, + selection_size.width + ); + } + + // draw text + window.draw_list_.text( + font, + font_size, + text_rect.left_top(), + context.color_of(theme, ThemeCategory::TEXT), + utf8_text, + text_rect.width() + ); + + // draw dropdown menu + bool menu_toggled = false; + if (state & MouseState::PRESSED) + { + menu_toggled = true; + + // If the combo is already open, click again to close the combo, otherwise open the combo + if (context.is_combo_activated(id)) + { + context.mark_combo_dead(); + } + else + { + context.mark_combo_alive(id); + } + } + + if (context.is_combo_activated(id)) + { + // The contents of the dropdown window should not affect the layout of the parent window, so after drawing the dropdown window, we reset the parent window canvas cursor + const auto backup_position = drawer.cursor_position(context); + { + // const auto dropdown_offset_x = theme.item_inner_spacing.width; + constexpr auto dropdown_offset_x = value_type{0}; + const auto dropdown_height = + (text_size.height + theme.item_spacing.height) * static_cast(std::ranges::min(selections.size(), show_selection_count)) + + theme.window_padding.height; + + constexpr std::string_view combo_window_name{"@WINDOW::COMBO@"}; + const WindowFlag combo_window_flag + { + window.flag_.is() ? gui::WindowFlag::BORDERED : gui::WindowFlag::NONE, + WindowInternalFlag::CATEGORY_COMBO + }; + + const auto dropdown_point = point_type{frame_point.x + dropdown_offset_x, frame_point.y + frame_size.height}; + const auto dropdown_size = extent_type{frame_size.width - dropdown_offset_x, dropdown_height}; + + // begin dropdown window (with background) + context.begin_child_window(combo_window_name, dropdown_size, 1, combo_window_flag, false); + { + auto& combo_window = *window.children_this_frame_.back(); + auto& combo_window_canvas = combo_window.canvas_; + IdMaker combo_window_id_maker{.self = combo_window}; + Drawer combo_window_drawer{.self = combo_window}; + + const auto item_height = font_size; + + // Close the dropdown if the user interacts with another widget (unless the widget is the dropdown window's scrollbar) + bool combo_item_active = false; + combo_item_active |= context.is_widget_activated(combo_window.id_of_scrollbar(context)); + + for (const auto view = selections | std::views::enumerate; + const auto [index, selection]: view) + { + const auto item_selected = std::cmp_equal(index, selected); + const auto item_id = combo_window_id_maker.make_id(context, selection); + + const auto item_point = point_type{dropdown_point.x, combo_window_canvas.cursor_current_line.y - theme.item_spacing.height / 2}; + const auto item_size = extent_type{dropdown_size.width, item_height + theme.item_spacing.height}; + const rect_type item_rect{item_point, item_size}; + + auto item_state = context.test_mouse(item_id, item_rect); + combo_item_active |= context.is_widget_activated(item_id); + + if (item_state & MouseState::HOVERED or item_selected) + { + const auto color = [&]noexcept -> Theme::color_type + { + if (item_state & MouseState::HOVERED) + { + if (item_state & MouseState::KEEPING) + { + return context.color_of(theme, ThemeCategory::COMBO_ITEM_ACTIVATED); + } + return context.color_of(theme, ThemeCategory::COMBO_ITEM_HOVERED); + } + + return context.color_of(theme, ThemeCategory::COMBO_ITEM); + }(); + + combo_window_drawer.draw_widget_frame(context, item_rect, color); + } + + const auto& selection_text = selections[index]; + combo_window.draw_text(context, selection_text); + + // Scroll to the selected item when opening the window + if (item_selected and menu_toggled) + { + combo_window_drawer.set_scroll_position_here(context); + } + + // Close the dropdown window after selecting any item + if (item_state & MouseState::PRESSED) + { + context.mark_widget_dead(); + context.mark_combo_dead(); + + value_changed = true; + selected = index; + + break; + } + } + + if (not combo_item_active and context.is_any_widget_activated()) + { + context.mark_combo_dead(); + } + } + gui::end_child_window(context); + } + window.canvas_.cursor_current_line = backup_position; + } + + return value_changed; + } + }; + + Window::Window( + const std::string_view name, + const WindowFlag flag, + const point_type& point, + const extent_type& size, + Window* root + ) noexcept + : canvas_ + { + .cursor_start_line = {0, 0}, + .cursor_current_line = {0, 0}, + .cursor_previous_line = {0, 0}, + .height_current_line = 0, + .height_previous_line = 0, + .last_item_rect = {0, 0, 0, 0}, + .last_item_hovered = false, + .last_item_focused = false, + .item_width = {}, + .text_wrap_width = {} + }, + name_{name}, + flag_{flag}, + root_{root}, + point_{point}, + size_full_{size}, + size_{size}, + size_of_content_{0, 0}, + default_item_width_{0}, + scroll_y_{0}, + scroll_next_y_{0}, + scroll_y_visible_{false}, + visible_{false}, + collapsed_{false}, + skip_item_{true}, + accessed_{false}, + auto_fit_only_grows_{false}, + auto_fit_frames_{-1}, + last_drawn_frame_{frame_count_uninitialized} + { + // window id + IdMaker{.self = *this}.make_id(); + + if (flag.is()) + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(root != nullptr); + } + else + { + root_ = this; + } + + // auto-fit + // if (size.width < 1e-3f or size.height < 1e-3f) + { + auto_fit_only_grows_ = true; + auto_fit_frames_ = 2; + } + } + + auto Window::reset(const WindowFlag flag, const extent_type& size) noexcept -> void + { + flag_ = flag; + + if (flag_.is()) + { + size_full_ = size; + } + } + + auto Window::handle_inputs(const Context& context) noexcept -> void + { + const auto& mouse = context.mouse(); + + // scroll + if (not flag_.is()) + { + // todo + constexpr auto scroll_weight = static_cast(5); + scroll_next_y_ -= mouse.wheel * Drawer{.self = *this}.font_size(context) * scroll_weight; + } + } + + auto Window::begin_window( + Context& context, + Window* parent, + alpha_type background_fill_alpha + ) noexcept -> bool + { + // This function can be called multiple times per frame, + // but is only initialized the first time it is called (after that we can just append the contents) + + const auto current_frame_count = context.current_frame(); + const auto is_first_draw_this_frame = current_frame_count != last_drawn_frame_; + + const auto display_size = context.io().display_size; + + // --------------------------------- + // initialize the window, set the window's clip rect + { + if (is_first_draw_this_frame) + { + // bind context + draw_list_.bind_context(context); + // clear render data + draw_list_.reset(); + // clear all child + children_this_frame_.clear(); + // show + visible_ = true; + // clear clip rect + clip_rect_stack_.clear(); + // clear all ids + id_stack_.clear(); + // generate a new id + IdMaker{.self = *this}.make_id(); + + if (flag_.is()) + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(parent != nullptr); + parent->children_this_frame_.push_back(this); + + // Moves the child window to the current cursor position of the parent window + point_ = parent->canvas_.cursor_current_line; + } + } + + // Outer clipping rectangle (window area) + if (flag_.is()) + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(parent != nullptr); + const auto& last = parent->clip_rect_stack_.back(); + + // the viewing area of the child window must not exceed that of the parent window + push_clip_rect(context, last); + } + else + { + // entire display area + push_clip_rect(context, {0, 0, display_size}); + } + } + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(clip_rect_stack_.size() == 1); + + // --------------------------------- + // initialize the canvas, set the canvas's clip rect + bool close_button_pressed = false; + { + auto drawer = Drawer{*this}; + + const auto& mouse = context.mouse(); + const auto& font = context.current_font(); + const auto& theme = context.current_theme(); + + const auto font_size = drawer.font_size(context); + const auto has_titlebar = not flag_.is(); + + const auto is_child_window = flag_.is(); + const auto is_tooltip_window = flag_.is(); + + if (is_child_window) + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not has_titlebar, "The child window is not allowed to contain a titlebar!"); + } + if (is_tooltip_window) + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not has_titlebar, "The tootip window is not allowed to contain a titlebar!"); + } + + if (is_first_draw_this_frame) + { + if (last_drawn_frame_ + 1 < current_frame_count) + { + // The current window was not drawn in the last frame (or even many frames before that), this is usually because the window was just created, or the window was not visible before + // Focus on the current window + context.focus_window(*this); + + if (is_tooltip_window) + { + // In order to ensure that all the contents of the tooltip are visible, we delay the display of its contents by one frame + visible_ = false; + auto_fit_only_grows_ = false; + auto_fit_frames_ = 2; + } + } + last_drawn_frame_ = current_frame_count; + + // Follows the mouse if it's a tooltip window + if (is_tooltip_window) + { + // todo: offset + point_ = mouse.position_current + extent_type{32, 16} - theme.item_frame_padding * 2; + } + else + { + // If the user drags the window, we determine the new position of the window before the frame is drawn to avoid lag + if (const auto activated = context.mark_widget_alive(id_of_move(context)); + activated) + { + if (mouse.is_down(context, MouseKey::LEFT)) + { + // select current window + context.focus_window(*this); + + if (not flag_.is()) + { + const auto delta = mouse.position_delta; + + // dragging + point_ += delta; + } + } + else + { + // No widgets are active + context.mark_widget_dead(); + } + } + } + + if (not is_child_window) + { + const auto pad = extent_type{font_size * 2.f, font_size * 2.f}; + + // Limit the current window from moving outside the program's visual area + point_ = point_.clamp( + (pad - size_).to(), + (display_size - pad).to() + ); + + // Limit the current window to be not too small + size_full_ = size_full_.combine_max(pad); + } + + // set default item width + if ( + size_.width > 0 and + not is_tooltip_window and + not flag_.is() + ) + { + default_item_width_ = size_.width * theme.item_default_width_factor; + } + else + { + // If this value is too small, the width of the widgets that use item_width will also be small, + // if the width of the newly created (showed) window depends on these widgets, + // the window may not be able to display the all widgets in its entirety + // default_item_width_ = theme.window_min_size.width * theme.item_default_width_factor; + default_item_width_ = 300; + } + + // apply and clamp scrolling + { + scroll_y_ = std::ranges::max(scroll_next_y_, .0f); + + if (not collapsed_ and not skip_item_) + { + const float max_scroll_y = std::ranges::max(.0f, size_of_content_.height - size_full_.height); + scroll_y_ = std::ranges::min(scroll_y_, max_scroll_y); + } + + scroll_next_y_ = scroll_y_; + } + + if (has_titlebar) + { + // `double left-click` on the `titlebar` of the `current window` to collapse the current window + if (context.is_window_hovered(*this)) + { + if (const auto rect = drawer.titlebar_rect(context); + is_hovered(context, rect) and + mouse.is_double_clicked(context, MouseKey::LEFT) + ) + { + // select current window + context.focus_window(*this); + + // collapse window by double-clicking on titlebar + collapsed_ = not collapsed_; + } + } + } + else + { + // Windows without a titlebar are not allowed to collapse + collapsed_ = false; + } + + if (collapsed_) + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(has_titlebar); + + // titlebar only + const auto rect = drawer.titlebar_rect(context); + size_ = rect.size(); + + draw_list_.rect_filled( + rect, + context.color_of(theme, ThemeCategory::TITLEBAR_COLLAPSED), + theme.window_corner_rounding + ); + + if (flag_.is()) + { + constexpr auto offset = extent_type{1, 1}; + + draw_list_.rect( + {rect.point + offset, rect.extent}, + context.color_of(theme, ThemeCategory::BORDER), + theme.window_corner_rounding + ); + draw_list_.rect( + rect, + context.color_of(theme, ThemeCategory::BORDER), + theme.window_corner_rounding + ); + } + } + else + { + size_ = size_full_; + + auto resize_grip_color = context.color_of(theme, ThemeCategory::RESIZE_GRIP); + + // The tooltip window always adapts to the size of the content + if (is_tooltip_window) + { + if (auto_fit_frames_ > 0) + { + size_full_ = size_of_content_ + theme.window_padding - extent_type{0, theme.item_spacing.height}; + } + } + else + { + const auto auto_fit_size = + (size_of_content_ + theme.window_auto_fit_padding).clamp( + theme.window_min_size, + display_size - theme.window_auto_fit_padding + ); + + if (flag_.is()) + { + size_full_ = auto_fit_size; + } + else if (auto_fit_frames_ > 0) + { + // Auto-fit only grows during the first few frames + if (auto_fit_only_grows_) + { + size_full_ = size_full_.combine_max(auto_fit_size); + } + else + { + size_full_ = auto_fit_size; + } + } + else if (not flag_.is()) + { + // resize grip + const auto rect = drawer.resize_grip_rect(context); + const auto id = id_of_resize(context); + + const auto state = context.test_mouse(id, rect); + + if (state & MouseState::KEEPING) + { + resize_grip_color = context.color_of(theme, ThemeCategory::RESIZE_GRIP_ACTIVATED); + } + else if (state & MouseState::HOVERED) + { + resize_grip_color = context.color_of(theme, ThemeCategory::RESIZE_GRIP_HOVERED); + } + + if (state & MouseState::KEEPING) + { + // `double left-click` on the `resize-grip` to auto-fit the current window + // allows the mouse to be offset from the current window area when resizing + if (mouse.is_double_clicked(context, MouseKey::LEFT)) + { + [[maybe_unused]] const auto old_size = size_; + + // auto-fit + size_ = auto_fit_size; + size_full_ = auto_fit_size; + } + else + { + const auto delta = mouse.position_delta; + [[maybe_unused]] const auto old_size = size_; + + // resize + size_ = theme.window_min_size.combine_max(size_full_ + delta); + size_full_ = size_; + } + } + } + } + + // At this point, we can already determine the size of the window + const auto current_titlebar_rect = [&]() noexcept -> rect_type + { + if (has_titlebar) + { + return drawer.titlebar_rect(context); + } + + return drawer.titlebar_rect(context, 0); + }(); + + // background rect + if (background_fill_alpha > 0) + { + draw_list_.rect_filled( + {point_, size_}, + context.color_of(theme, ThemeCategory::WINDOW_BACKGROUND, background_fill_alpha), + theme.window_corner_rounding + ); + } + + // titlebar rect + if (has_titlebar) + { + draw_list_.rect_filled( + current_titlebar_rect, + context.color_of(theme, ThemeCategory::TITLEBAR), + theme.window_corner_rounding, + DrawFlag::ROUND_CORNER_TOP + ); + + // titlebar border + if (flag_.is()) + { + draw_list_.line( + current_titlebar_rect.left_bottom(), + current_titlebar_rect.right_bottom(), + context.color_of(theme, ThemeCategory::BORDER) + ); + } + } + + // background border + if (flag_.is()) + { + constexpr auto offset = extent_type{1, 1}; + + draw_list_.rect( + {point_ + offset, size_}, + context.color_of(theme, ThemeCategory::BORDER_SHADOW), + theme.window_corner_rounding + ); + draw_list_.rect( + {point_, size_}, + context.color_of(theme, ThemeCategory::BORDER), + theme.window_corner_rounding + ); + } + + // scrollbar + if ( + // no scrollbar + flag_.is() or + // window space is greater than the space required for content + size_.height > size_of_content_.height) + { + scroll_y_visible_ = false; + } + else + { + scroll_y_visible_ = true; + + const auto window_rect = rect(); + + const auto right_top = window_rect.right_top(); + const auto right_bottom = window_rect.right_bottom(); + + const auto background_y1 = right_top.y + current_titlebar_rect.height() + 1; + const auto background_y2 = right_bottom.y - 1; + const auto background_height = background_y2 - background_y1; + + // scrollbar background + const point_type background_point{right_bottom.x - theme.window_vertical_scrollbar_width, background_y1}; + const extent_type background_size{theme.window_vertical_scrollbar_width, background_height}; + const rect_type background_rect{background_point, background_size}; + draw_list_.rect_filled( + background_rect, + context.color_of(theme, ThemeCategory::SCROLLBAR_BACKGROUND) + ); + + // scrollbar area + constexpr extent_type scrollbar_area_offset{3, 3}; + const auto scrollbar_area_point = background_point + scrollbar_area_offset; + const auto scrollbar_area_size = background_size - scrollbar_area_offset * 2; + const rect_type scrollbar_area_rect{scrollbar_area_point, scrollbar_area_size}; + + const auto grab_size_y_normalized = std::ranges::clamp( + size_.height / std::ranges::max(size_of_content_.height, size_.height), + .0f, + 1.f + ); + const auto grab_size_y = scrollbar_area_size.height * grab_size_y_normalized; + + auto grab_color = context.color_of(theme, ThemeCategory::SCROLLBAR_GRAB); + if (grab_size_y_normalized < 1.f) + { + const auto id = id_of_scrollbar(context); + + const auto state = context.test_mouse(id, scrollbar_area_rect); + + if (state & MouseState::KEEPING) + { + grab_color = context.color_of(theme, ThemeCategory::SCROLLBAR_GRAB_ACTIVATED); + + const auto y_normalized = std::ranges::clamp( + (mouse.position_current.y - (scrollbar_area_point.y + grab_size_y * .5f)) / (scrollbar_area_size.height - grab_size_y), + .0f, + 1.f + ) * (1.f - grab_size_y_normalized); + + scroll_y_ = size_of_content_.height * y_normalized; + scroll_next_y_ = scroll_y_; + } + else if (state & MouseState::HOVERED) + { + grab_color = context.color_of(theme, ThemeCategory::SCROLLBAR_GRAB_HOVERED); + } + } + + // Normalized height of the grab + const auto y_normalized = std::ranges::clamp( + scroll_y_ / std::ranges::max(.00001f, size_of_content_.height), + .0f, + 1.f + ); + + const auto grab_point = scrollbar_area_point + extent_type{0, scrollbar_area_size.height * y_normalized}; + const auto grab_size = extent_type{scrollbar_area_size.width, scrollbar_area_size.height * grab_size_y_normalized}; + const rect_type grab_rect{grab_point, grab_size}; + + draw_list_.rect_filled(grab_rect, grab_color); + } + + // resize-grip + if (not flag_.is()) + { + const auto rect = drawer.resize_grip_rect(context); + // if (const auto rounding = theme.window_corner_rounding; + // rounding == 0) // NOLINT(clang-diagnostic-float-equal) + // { + // draw_list_.triangle_filled( + // rect.right_bottom(), + // rect.left_bottom(), + // rect.right_top(), + // resize_grip_color + // ); + // } + // else + // { + // todo + draw_list_.triangle_filled( + rect.right_bottom(), + rect.left_bottom(), + rect.right_top(), + resize_grip_color + ); + // } + } + } + + // reset contents size for auto-fitting + size_of_content_ = {0, 0}; + if (auto_fit_frames_ > 0) + { + auto_fit_frames_ -= 1; + } + + // titlebar context + if (has_titlebar) + { + if (not flag_.is()) + { + const auto rect = drawer.close_button_rect(context); + const auto id = id_of_close(context); + + const auto state = context.test_mouse(id, rect); + + auto close_button_color = context.color_of(theme, ThemeCategory::CLOSE_BUTTON); + + if (state & MouseState::HOVERED) + { + if (state & MouseState::KEEPING) + { + close_button_color = context.color_of(theme, ThemeCategory::CLOSE_BUTTON_ACTIVATED); + } + else + { + close_button_color = context.color_of(theme, ThemeCategory::CLOSE_BUTTON_HOVERED); + } + } + + const auto center = rect.center(); + + // circle + draw_list_.circle_filled( + center, + std::ranges::max(2.f, rect.height() * .5f), + close_button_color, + 16 + ); + + // × + if (state & MouseState::HOVERED) + { + const auto x = rect.extent.width * .5f * .667f - 1.f; + const auto y = rect.extent.height * .5f * .667f - 1.f; + + draw_list_.line( + center + extent_type{x, y}, + center + extent_type{-x, -y}, + context.color_of(theme, ThemeCategory::TEXT) + ); + draw_list_.line( + center + extent_type{x, -y}, + center + extent_type{-x, y}, + context.color_of(theme, ThemeCategory::TEXT) + ); + } + + close_button_pressed = state & MouseState::PRESSED; + } + + // title text + const auto text_point = point_ + theme.item_frame_padding; + const auto text_size = internal::text_size(font, name_, font_size, Font::no_auto_wrap); + + const auto max_width = size_.width - theme.item_frame_padding.width * 2; + const auto max_height = size_.height; + + if (const auto do_clip = text_size.width > max_width or text_size.height > max_height; + do_clip) + { + const auto width = std::ranges::min(text_size.width, max_width); + const auto height = std::ranges::min(text_size.height, max_height); + + // If the titlebar is not large enough to accommodate the title content, + // simply discard the content that exceeds the space, + // rather than displaying the content on a new line + push_clip_rect(context, {text_point, width, height}); + draw_list_.text( + font, + font_size, + text_point, + context.color_of(theme, ThemeCategory::TEXT), + name_, + width + ); + pop_clip_rect(context); + } + else + { + draw_list_.text( + font, + font_size, + text_point, + context.color_of(theme, ThemeCategory::TEXT), + name_, + text_size.width + ); + } + } + + // init canvas + { + const auto current_titlebar_rect = [&]() noexcept -> rect_type + { + if (has_titlebar) + { + return drawer.titlebar_rect(context); + } + + return drawer.titlebar_rect(context, 0); + }(); + + canvas_.cursor_start_line = + // window start point + point_ + + // todo X: columns offset + // Y: titlebar + padding + extent_type{8, current_titlebar_rect.height() + drawer.window_padding(context).height} + + // Y: scrollbar + extent_type{0, -scroll_y_}; + canvas_.cursor_current_line = canvas_.cursor_start_line; + canvas_.cursor_previous_line = canvas_.cursor_current_line; + + canvas_.height_current_line = 0; + canvas_.height_previous_line = 0; + + canvas_.item_width.clear(); + canvas_.item_width.push_back(default_item_width_); + + canvas_.text_wrap_width.clear(); + static_assert(DrawList::text_wrap_width_not_set < 0); + canvas_.text_wrap_width.push_back(DrawList::text_wrap_width_not_set); + } + } + + // Inner clipping rectangle (canvas area) + { + const auto window_rect = rect(); + const auto current_window_padding = drawer.window_padding(context); + const auto current_titlebar_rect = [&]() noexcept -> rect_type + { + if (has_titlebar) + { + return drawer.titlebar_rect(context); + } + + return drawer.titlebar_rect(context, 0); + }(); + + const point_type clip_rect_point + { + current_titlebar_rect.left_bottom().x + current_window_padding.width * .5f + .5f, + current_titlebar_rect.left_bottom().y + .5f + }; + const extent_type clip_rect_size + { + current_titlebar_rect.width() - current_window_padding.width - (scroll_y_visible_ ? theme.window_vertical_scrollbar_width : 0), + window_rect.height() - current_titlebar_rect.height() - 2 + }; + const rect_type clip_rect{clip_rect_point, clip_rect_size}; + + push_clip_rect(context, clip_rect); + } + + // Child windows may not be visible (located in areas not visible to the parent window), we collapse it manually (so that we can skip the widgets drawn on it earlier) + if (is_child_window) + { + const auto last_clip_rect = clip_rect_stack_.back(); + collapsed_ = not last_clip_rect.valid(); + + // We also hide the window from rendering because we've already added its border to the command list + // (we could perform the check earlier in the function, but it is simpler at this point) + if (collapsed_) + { + visible_ = false; + } + } + + if (theme.alpha <= 0) + { + visible_ = false; + } + + skip_item_ = collapsed_ or (not visible_ and auto_fit_frames_ == 0); + + if (is_first_draw_this_frame) + { + accessed_ = false; + } + } + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(clip_rect_stack_.size() == 2); + + return close_button_pressed; + } + + auto Window::end_window(Context& context) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(clip_rect_stack_.size() == 2); + + // canvas rect + pop_clip_rect(context); + + // window rect + pop_clip_rect(context); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(clip_rect_stack_.empty()); + + // window_data.root = nullptr; + } + + auto Window::begin_child_window( + Context& context, + const std::string_view name, + const alpha_type background_fill_alpha, + extent_type size, + const bool border, + WindowFlag flag + ) noexcept -> void + { + const auto& theme = context.current_theme(); + + flag |= gui::WindowFlag::NO_TITLEBAR | gui::WindowFlag::NO_CLOSE | gui::WindowFlag::NO_RESIZE | gui::WindowFlag::NO_MOVE; + flag |= WindowInternalFlag::CHILD_WINDOW; + + if (border) + { + flag |= gui::WindowFlag::BORDERED; + } + + const auto content_region = content_region_max(context); + const auto remaining_size = content_region - (canvas_.cursor_current_line - point_); + + if (size.width <= 0) + { + flag |= WindowInternalFlag::CHILD_WINDOW_AUTO_FIT_X; + + size.width = std::ranges::max(remaining_size.width, theme.window_min_size.width); + } + if (size.height <= 0) + { + flag |= WindowInternalFlag::CHILD_WINDOW_AUTO_FIT_Y; + + size.height = std::ranges::max(remaining_size.height, theme.window_min_size.height); + } + + const auto child_window_name = std::format("{}.{}", name_, name); + context.begin_window(child_window_name, size, background_fill_alpha, flag); + auto& window = context.current_window(); + + if (border and not flag_.is()) + { + auto& child = *children_this_frame_.back(); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(std::addressof(window) == std::addressof(child)); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(child.name_ == child_window_name); + + // border only indicates whether the child window has a border, not the widgets of the child window + child.flag_ &= ~gui::WindowFlag::BORDERED; + } + } + + // ReSharper disable once CppParameterMayBeConstPtrOrRef + auto Window::end_child_window(Context& context, Window& child) noexcept -> void + { + context.end_window(); + + // When using autofill child window, we don't provide the width/height to `adjust_item_size` so that it doesn't feed back into automatic size-fitting + auto size = child.size_; + if (child.flag_.is()) + { + size.width = 0; + } + if (child.flag_.is()) + { + size.height = 0; + } + + Drawer drawer{.self = *this}; + drawer.adjust_item_size(context, size); + } + + auto Window::render(Context& context, draw_lists_type& draw_lists) const noexcept -> void + { + if (not visible_) + { + return; + } + + draw_lists.emplace_back(draw_list_); + + std::ranges::for_each( + children_this_frame_, + [&](auto& child) noexcept -> void + { + child->render(context, draw_lists); + } + ); + } + + auto Window::draw_text(Context& context, const std::string_view utf8_text) noexcept -> void + { + if (skip_item_) + { + return; + } + accessed_ = true; + + const auto& theme = context.current_theme(); + const auto& font = context.current_font(); + Drawer drawer{.self = const_cast(*this)}; + + const auto font_size = drawer.font_size(context); + + static_assert(DrawList::text_wrap_width_not_set < 0); + const auto wrap_width = canvas_.text_wrap_width.back(); + const auto wrap_enabled = wrap_width >= .0f; + + if (constexpr std::size_t long_text_threshold = 1500; + utf8_text.size() > long_text_threshold and not wrap_enabled) + { + const auto clip_rect = clip_rect_stack_.back(); + const auto clip_rect_left_top = clip_rect.left_top(); + const auto clip_rect_left_bottom = clip_rect.left_bottom(); + + extent_type text_size{0, 0}; + + // Only text that is in the visible area is rendered + if (const auto visible_area_height = clip_rect_left_bottom.y - canvas_.cursor_current_line.y; + visible_area_height > 0) + { + const auto invisible_area_height = clip_rect_left_top.y - canvas_.cursor_current_line.y; + + const auto line_height = drawer.font_size(context); + const auto invisible_lines = static_cast(invisible_area_height / line_height - 1); + const auto visible_lines = static_cast(visible_area_height / line_height - 1); + + const auto has_invisible_lines = invisible_lines > 0; + + auto view = utf8_text | std::views::split('\n'); + auto visible_view = view | std::views::drop(has_invisible_lines ? invisible_lines : 0) | std::views::take(visible_lines); + + auto text_point = canvas_.cursor_current_line; + + if (has_invisible_lines) + { + text_point.y += static_cast(invisible_lines) * line_height; + } + + for (const auto sub: visible_view) + { + const std::string_view sub_string{sub}; + const auto this_line_text_size = internal::text_size( + font, + sub_string, + font_size, + Font::no_auto_wrap + ); + + // draw one line + draw_list_.text( + font, + font_size, + text_point, + context.color_of(theme, ThemeCategory::TEXT), + sub_string, + Font::no_auto_wrap + ); + + text_point.y += line_height; + text_size.width = std::ranges::max(text_size.width, this_line_text_size.width); + } + + text_size.height = static_cast(std::ranges::distance(view)) * line_height; + } + + const rect_type rect{canvas_.cursor_current_line, text_size}; + + drawer.adjust_item_size(context, text_size); + // todo: test visible? + // drawer.test_last_item(context, rect); + std::ignore = drawer.test_last_item_visible(context, rect); + } + else + { + const auto this_wrap_width = wrap_enabled ? wrap_width : Font::no_auto_wrap; + + const auto text_point = canvas_.cursor_current_line; + const auto text_size = internal::text_size( + font, + utf8_text, + font_size, + this_wrap_width + ); + const rect_type rect{text_point, text_size}; + + drawer.adjust_item_size(context, text_size); + // if (not drawer.is_visible_area(context, rect)) + // { + // return; + // } + // drawer.test_last_item(context, rect); + if (not drawer.test_last_item_visible(context, rect)) + { + // invisible + return; + } + + draw_list_.text( + font, + font_size, + text_point, + context.color_of(theme, ThemeCategory::TEXT), + utf8_text, + this_wrap_width + ); + } + } + + auto Window::draw_button(Context& context, const std::string_view utf8_text, extent_type size, const bool repeat_when_held) noexcept -> bool + { + if (skip_item_) + { + return false; + } + accessed_ = true; + + const auto& theme = context.current_theme(); + const auto& font = context.current_font(); + Drawer drawer{.self = const_cast(*this)}; + const IdMaker id_maker{.self = *this}; + + const auto font_size = drawer.font_size(context); + + const auto text_size = internal::text_size(font, utf8_text, font_size, Font::no_auto_wrap); + + if (size.width <= 0) + { + size.width = text_size.width; + } + if (size.height <= 0) + { + size.height = text_size.height; + } + + const auto button_point = canvas_.cursor_current_line; + const auto button_size = size + theme.item_frame_padding * 2; + const rect_type button_rect{button_point, button_size}; + + drawer.adjust_item_size(context, button_size); + // if (not drawer.is_visible_area(context, button_rect)) + // { + // // invisible + // return false; + // } + // drawer.test_last_item(context, button_rect); + if (not drawer.test_last_item_visible(context, button_rect)) + { + // invisible + return false; + } + + const auto id = id_maker.make_id(context, utf8_text); + const auto state = context.test_mouse(id, button_rect, repeat_when_held); + + color_type button_color = context.color_of(theme, ThemeCategory::BUTTON); + { + if (state & MouseState::KEEPING or state & MouseState::PRESSED) + { + button_color = context.color_of(theme, ThemeCategory::BUTTON_ACTIVATED); + } + else if (state & MouseState::HOVERED) + { + button_color = context.color_of(theme, ThemeCategory::BUTTON_HOVERED); + } + } + + // draw □ + drawer.draw_widget_frame(context, button_rect, button_color); + + const auto text_area_point = button_rect.left_top() + theme.item_frame_padding; + + // the given size may be smaller than the required size of the text, in which case the button text needs to be truncated + const auto clip = size.width < text_size.width or size.height < text_size.height; + if (clip) + { + const rect_type rect + { + text_area_point, + // Allow extra to draw over the horizontal padding to make it visible that text doesn't fit + button_rect.right_bottom() - extent_type{0, theme.item_frame_padding.height} + }; + + push_clip_rect(context, rect); + } + + const auto text_offset = (size - text_size).combine_max({0, 0}) * .5f; + const auto text_point = + text_area_point + + text_offset; + + // draw text + draw_list_.text( + font, + font_size, + text_point, + context.color_of(theme, ThemeCategory::TEXT), + utf8_text, + button_rect.width() + ); + + if (clip) + { + pop_clip_rect(context); + } + + return state & MouseState::PRESSED; + } + + auto Window::draw_small_button(Context& context, const std::string_view utf8_text, const bool repeat_when_held) noexcept -> bool + { + if (skip_item_) + { + return false; + } + accessed_ = true; + + const auto& theme = context.current_theme(); + const auto& font = context.current_font(); + Drawer drawer{.self = const_cast(*this)}; + const IdMaker id_maker{.self = *this}; + + const auto font_size = drawer.font_size(context); + + const auto text_size = internal::text_size(font, utf8_text, font_size, Font::no_auto_wrap); + + const auto button_point = canvas_.cursor_current_line; + const auto button_size = text_size + theme.item_frame_padding * 2; + const rect_type button_rect{button_point, button_size}; + + drawer.adjust_item_size(context, button_size); + // if (not drawer.is_visible_area(context, button_rect)) + // { + // // invisible + // return false; + // } + // drawer.test_last_item(context, button_rect); + if (not drawer.test_last_item_visible(context, button_rect)) + { + // invisible + return false; + } + + const auto id = id_maker.make_id(context, utf8_text); + const auto state = context.test_mouse(id, button_rect, repeat_when_held); + + color_type button_color = context.color_of(theme, ThemeCategory::BUTTON); + { + if (state & MouseState::KEEPING or state & MouseState::PRESSED) + { + button_color = context.color_of(theme, ThemeCategory::BUTTON_ACTIVATED); + } + else if (state & MouseState::HOVERED) + { + button_color = context.color_of(theme, ThemeCategory::BUTTON_HOVERED); + } + } + + // draw □ + drawer.draw_widget_frame(context, button_rect, button_color); + + const auto text_area_point = button_rect.left_top() + theme.item_frame_padding; + + // draw text + draw_list_.text( + font, + font_size, + text_area_point, + context.color_of(theme, ThemeCategory::TEXT), + utf8_text, + button_rect.width() + ); + + return state & MouseState::PRESSED; + } + + auto Window::draw_radio_button(Context& context, const std::string_view utf8_text, const bool checked) noexcept -> bool + { + if (skip_item_) + { + return false; + } + accessed_ = true; + + const auto& theme = context.current_theme(); + const auto& font = context.current_font(); + Drawer drawer{.self = const_cast(*this)}; + const IdMaker id_maker{.self = *this}; + + const auto font_size = drawer.font_size(context); + + const auto text_size = internal::text_size(font, utf8_text, font_size, Font::no_auto_wrap); + + // ○ + text + + // ○, diameter equals string rect height + const auto check_point = canvas_.cursor_current_line; + const auto check_size = extent_type{text_size.height + theme.item_frame_padding.height * 2 - 1, text_size.height + theme.item_frame_padding.height * 2 - 1}; + const rect_type check_rect{check_point, check_size}; + const circle_type check_circle{check_point + check_size / 2, check_size.width / 2}; + drawer.adjust_item_size(context, check_size); + + // ○ text + same_line(context, auto_size, theme.item_inner_spacing.width); + + // text + const auto text_point = canvas_.cursor_current_line + extent_type{0, theme.item_frame_padding.height}; + const rect_type text_rect{text_point, text_size}; + drawer.adjust_item_size(context, text_size); + + const rect_type total_rect{check_rect.left_top(), text_rect.right_bottom()}; + // if (not drawer.is_visible_area(context, total_rect)) + // { + // // invisible + // return false; + // } + // drawer.test_last_item(context, total_rect); + if (not drawer.test_last_item_visible(context, total_rect)) + { + // invisible + return false; + } + + const auto id = id_maker.make_id(context, utf8_text); + // fixme: test check_rect or total_rect? + const auto state = context.test_mouse(id, check_rect, false); + + // draw ○ + if (state & MouseState::HOVERED) + { + drawer.draw_widget_frame(context, check_circle, context.color_of(theme, ThemeCategory::RADIO_BUTTON_HOVERED)); + } + else + { + drawer.draw_widget_frame(context, check_circle, context.color_of(theme, ThemeCategory::FRAME_BACKGROUND)); + } + + if (checked) + { + const auto check_fill_point = check_point + theme.item_inner_spacing; + const auto check_fill_size = check_size - theme.item_inner_spacing * 2; + const circle_type check_fill_circle{check_fill_point + check_fill_size / 2, check_fill_size.width / 2}; + draw_list_.circle_filled(check_fill_circle, context.color_of(theme, ThemeCategory::RADIO_BUTTON_ACTIVATED)); + } + + // draw text + draw_list_.text( + font, + font_size, + text_rect.left_top(), + context.color_of(theme, ThemeCategory::TEXT), + utf8_text, + text_rect.width() + ); + + return state & MouseState::PRESSED; + } + + auto Window::draw_checkbox(Context& context, const std::string_view utf8_text, bool checked) noexcept -> bool + { + if (skip_item_) + { + return false; + } + accessed_ = true; + + const auto& theme = context.current_theme(); + const auto& font = context.current_font(); + Drawer drawer{.self = const_cast(*this)}; + const IdMaker id_maker{.self = *this}; + + const auto font_size = drawer.font_size(context); + + const auto text_size = internal::text_size(font, utf8_text, font_size, Font::no_auto_wrap); + + // □ + text + const auto width = text_size.height; + + // □, side length equals string rect height + const auto check_point = canvas_.cursor_current_line; + const auto check_size = extent_type{width + theme.item_frame_padding.height * 2, text_size.height + theme.item_frame_padding.height * 2}; + const rect_type check_rect{check_point, check_size}; + drawer.adjust_item_size(context, check_size); + + // □ text + same_line(context, auto_size, theme.item_inner_spacing.width); + + // text + const auto text_point = canvas_.cursor_current_line + extent_type{0, theme.item_frame_padding.height}; + const rect_type text_rect{text_point, text_size}; + drawer.adjust_item_size(context, text_size); + + const rect_type total_rect{check_rect.left_top(), text_rect.right_bottom()}; + // if (not drawer.is_visible_area(context, total_rect)) + // { + // // invisible + // return false; + // } + // drawer.test_last_item(context, total_rect); + if (not drawer.test_last_item_visible(context, total_rect)) + { + // invisible + return false; + } + + const auto id = id_maker.make_id(context, utf8_text); + // fixme: test check_rect or total_rect? + const auto state = context.test_mouse(id, check_rect, false); + + // draw □ + if (state & MouseState::HOVERED) + { + drawer.draw_widget_frame(context, check_rect, context.color_of(theme, ThemeCategory::CHECKBOX_HOVERED)); + } + else + { + drawer.draw_widget_frame(context, check_rect, context.color_of(theme, ThemeCategory::FRAME_BACKGROUND)); + } + + if (state & MouseState::PRESSED) + { + checked = not checked; + } + + if (checked) + { + const auto check_fill_point = check_point + theme.item_inner_spacing; + const auto check_fill_size = check_size - theme.item_inner_spacing * 2; + const rect_type check_fill_rect{check_fill_point, check_fill_size}; + draw_list_.rect_filled(check_fill_rect, context.color_of(theme, ThemeCategory::CHECKBOX_ACTIVATED)); + } + + // draw text + draw_list_.text( + font, + font_size, + text_rect.left_top(), + context.color_of(theme, ThemeCategory::TEXT), + utf8_text, + text_rect.width() + ); + + return checked; + } + + auto Window::draw_slider( + Context& context, + const std::string_view utf8_text, + float& reference, + const float min, + const float max, + const std::uint32_t decimal_precision, + const float power + ) noexcept -> bool + { + Anonymous anonymous{.self = *this}; + + return anonymous.draw_slider(context, utf8_text, reference, min, max, decimal_precision, power); + } + + auto Window::draw_slider_n( + Context& context, + const std::string_view utf8_text, + std::span references, + const float min, + const float max, + const std::uint32_t decimal_precision, + const float power + ) noexcept -> bool + { + Anonymous anonymous{.self = *this}; + + return anonymous.draw_slider(context, utf8_text, references, min, max, decimal_precision, power); + } + + auto Window::draw_combo( + Context& context, + const std::string_view utf8_text, + const std::span selections, + std::size_t& selected, + const std::size_t show_selection_count + ) noexcept -> bool + { + Anonymous anonymous{.self = *this}; + + return anonymous.draw_combo(context, utf8_text, selections, selected, show_selection_count); + } + + auto Window::draw_combo( + Context& context, + const std::string_view utf8_text, + const std::span selections, + std::size_t& selected, + const std::size_t show_selection_count + ) noexcept -> bool + { + Anonymous anonymous{.self = *this}; + + return anonymous.draw_combo(context, utf8_text, selections, selected, show_selection_count); + } + + auto Window::draw_combo( + Context& context, + const std::string_view utf8_text, + const std::span selections, + std::size_t& selected, + const std::size_t show_selection_count + ) noexcept -> bool + { + Anonymous anonymous{.self = *this}; + + return anonymous.draw_combo(context, utf8_text, selections, selected, show_selection_count); + } + + // ReSharper disable once CppParameterMayBeConstPtrOrRef + auto Window::same_line(Context& context, const value_type column_width, value_type spacing_width) noexcept -> void + { + if (collapsed_) + { + return; + } + + const auto& theme = context.current_theme(); + + canvas_.height_current_line = canvas_.height_previous_line; + canvas_.cursor_current_line = canvas_.cursor_previous_line; + + if (column_width < 0) + { + if (spacing_width < 0) + { + spacing_width = theme.item_spacing.width; + } + + canvas_.cursor_current_line.x += spacing_width; + } + else + { + spacing_width = std::ranges::max(spacing_width, static_cast(0)); + + canvas_.cursor_current_line.x = column_width + spacing_width; + } + } + + // ReSharper disable once CppParameterMayBeConstPtrOrRef + auto Window::push_item_width(Context& context, const Theme::value_type new_item_width) noexcept -> void + { + std::ignore = context; + + canvas_.item_width.push_back(new_item_width > 0 ? new_item_width : default_item_width_); + } + + // ReSharper disable once CppParameterMayBeConstPtrOrRef + auto Window::pop_item_width(Context& context) noexcept -> void + { + std::ignore = context; + + canvas_.item_width.pop_back(); + } + + // ReSharper disable once CppParameterMayBeConstPtrOrRef + auto Window::push_text_wrap_width(Context& context, const Theme::value_type new_wrap_width) noexcept -> void + { + std::ignore = context; + + canvas_.text_wrap_width.push_back(new_wrap_width); + } + + // ReSharper disable once CppParameterMayBeConstPtrOrRef + auto Window::pop_text_wrap_width(Context& context) noexcept -> void + { + std::ignore = context; + + canvas_.text_wrap_width.pop_back(); + } + + auto Window::id_of_move(Context& context) const noexcept -> widget_id_type + { + constexpr std::string_view name{"@WINDOW::MOVE@"}; + return IdMaker{.self = const_cast(*this)}.make_id(context, name); + } + + auto Window::id_of_close(Context& context) const noexcept -> widget_id_type + { + constexpr std::string_view name{"@WINDOW::CLOSE@"}; + return IdMaker{.self = const_cast(*this)}.make_id(context, name); + } + + auto Window::id_of_resize(Context& context) const noexcept -> widget_id_type + { + constexpr std::string_view name{"@WINDOW::RESIZE@"}; + return IdMaker{.self = const_cast(*this)}.make_id(context, name); + } + + auto Window::id_of_scrollbar(Context& context) const noexcept -> widget_id_type + { + constexpr std::string_view name{"@WINDOW::SCROLLBAR@"}; + return IdMaker{.self = const_cast(*this)}.make_id(context, name); + } + + auto Window::push_id(Context& context, const std::string_view string) noexcept -> void + { + const auto id = IdMaker{.self = *this}.make_id(context, string); + id_stack_.push_back(id); + } + + auto Window::push_id(Context& context, const void* pointer) noexcept -> void + { + const auto id = IdMaker{.self = *this}.make_id(context, pointer); + id_stack_.push_back(id); + } + + auto Window::push_id(Context& context, const widget_id_type value) noexcept -> void + { + const auto id = IdMaker{.self = *this}.make_id(context, value); + id_stack_.push_back(id); + } + + // ReSharper disable once CppParameterMayBeConstPtrOrRef + auto Window::pop_id(Context& context) noexcept -> void + { + std::ignore = context; + + id_stack_.pop_back(); + } + + // ReSharper disable once CppParameterMayBeConstPtrOrRef + auto Window::push_clip_rect(Context& context, const rect_type& rect, const bool clipped) noexcept -> void + { + std::ignore = context; + + const auto clip_rect = [&]() noexcept -> rect_type + { + if (clipped and not clip_rect_stack_.empty()) + { + // clip to a new rect + const auto last = clip_rect_stack_.back(); + return last.combine_min(rect); + } + + return rect; + }(); + + clip_rect_stack_.push_back(clip_rect); + draw_list_.push_clip_rect(clip_rect, false); + } + + // ReSharper disable once CppParameterMayBeConstPtrOrRef + auto Window::pop_clip_rect(Context& context) noexcept -> void + { + clip_rect_stack_.pop_back(); + + if (clip_rect_stack_.empty()) + { + const auto size = context.io().display_size; + draw_list_.push_clip_rect({0, 0, size}, false); + } + else + { + draw_list_.push_clip_rect(clip_rect_stack_.back(), false); + } + } + + auto Window::name() const noexcept -> std::string_view + { + return name_; + } + + auto Window::flag() const noexcept -> WindowFlag + { + return flag_; + } + + auto Window::position() const noexcept -> point_type + { + return point_; + } + + auto Window::width() const noexcept -> value_type + { + return size_.width; + } + + auto Window::height() const noexcept -> value_type + { + return size_.height; + } + + auto Window::size() const noexcept -> extent_type + { + return size_; + } + + auto Window::rect() const noexcept -> rect_type + { + return {point_, size_}; + } + + auto Window::visible() const noexcept -> bool + { + return visible_; + } + + auto Window::collapsed() const noexcept -> bool + { + return collapsed_; + } + + auto Window::root() const noexcept -> const Window& + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(root_ != nullptr); + + return *root_; + } + + auto Window::content_region_max(const Context& context) const noexcept -> extent_type + { + const auto& theme = context.current_theme(); + const Drawer drawer{.self = const_cast(*this)}; + + auto size = size_ - drawer.window_padding(context); + + // todo + if (scroll_y_visible_) + { + size.width -= theme.window_vertical_scrollbar_width; + } + + return size; + } + + auto Window::window_content_region_min(const Context& context) const noexcept -> extent_type + { + const Drawer drawer{.self = const_cast(*this)}; + + // titlebar + padding + const auto titlebar_height = drawer.titlebar_height(context); + const auto padding = drawer.window_padding(context); + + return extent_type{0, titlebar_height} + padding; + } + + auto Window::window_content_region_max(const Context& context) const noexcept -> extent_type + { + const auto& theme = context.current_theme(); + const Drawer drawer{.self = const_cast(*this)}; + + auto size = size_ - drawer.window_padding(context); + + if (scroll_y_visible_) + { + size.width -= theme.window_vertical_scrollbar_width; + } + + return size; + } + + auto Window::is_hovered(const Context& context, const rect_type& rect) const noexcept -> bool + { + const auto& mouse = context.mouse(); + const auto position = mouse.position_current; + + const auto clipped = [&]() noexcept -> rect_type + { + if (not clip_rect_stack_.empty()) + { + const auto& last = clip_rect_stack_.back(); + + return rect.combine_min(last); + } + + return rect; + }(); + + return clipped.includes(position); + } + + auto Window::is_item_hovered(const Context& context) const noexcept -> bool + { + std::ignore = context; + + return canvas_.last_item_hovered; + } + + auto Window::is_item_focused(const Context& context) const noexcept -> bool + { + std::ignore = context; + + return canvas_.last_item_focused; + } + + // auto Window::show() noexcept -> void + // { + // visible_ = true; + // + // std::ranges::for_each( + // children_this_frame_, + // &Window::show + // ); + // } + + auto Window::hide() noexcept -> void + { + visible_ = false; + accessed_ = false; + + std::ranges::for_each( + children_this_frame_, + &Window::hide + ); + } + + auto Window::find_hovered_window(const point_type& position, const bool excludes_children) noexcept -> Window* + { + if (not visible_) + { + return nullptr; + } + + if (not excludes_children) + { + for (auto* child: children_this_frame_) + { + if (auto* window = child->find_hovered_window(position, excludes_children); + window != nullptr) + { + return window; + } + } + } + + if (const auto rect = this->rect(); + rect.includes(position)) + { + return this; + } + + return nullptr; + } +} diff --git a/src/gui/internal/window.hpp b/src/gui/internal/window.hpp new file mode 100644 index 00000000..ad0d1f98 --- /dev/null +++ b/src/gui/internal/window.hpp @@ -0,0 +1,330 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include + +#include +#include + +namespace gal::prometheus::gui::internal +{ + class Window final + { + public: + using value_type = extent_type::value_type; + using alpha_type = Theme::alpha_type; + + // <= 0 + constexpr static auto auto_size = value_type{-.999999f}; + + private: + class IdMaker; + class Drawer; + class Anonymous; + + struct canvas_type + { + // Cursor start position of the canvas + point_type cursor_start_line; + // Cursor current position of the canvas + point_type cursor_current_line; + // Cursor previous (line) position of the canvas + point_type cursor_previous_line; + + // Current line height + value_type height_current_line; + // Previous line height + value_type height_previous_line; + + // The drawing area of the previous widget + rect_type last_item_rect; + // Whether the drawing area of the previous widget is hovered by the mouse + bool last_item_hovered; + // Whether the drawing area of the previous widget is focused by the mouse + bool last_item_focused; + + // next item width + std::vector item_width; + // <0(DrawList::text_wrap_width_not_set): disable + // =0: window.content_region_max().width + // >0: width + std::vector text_wrap_width; + }; + + // ================== + // CANVAS + + canvas_type canvas_; + + // ================== + // DRAW LIST + + DrawList draw_list_; + + // ================== + // WINDOW INFO + + std::string name_; + WindowFlag flag_; + + // The last non-child window in the Context::window_current_stack (this is different from the parent window, which can be a child of another window), you can find it in Context::window_root_list + Window* root_; + // All child windows that have been presented in this frame + std::vector children_this_frame_; + + // Position of the window + point_type point_; + // Size of the full (expanded) window + extent_type size_full_; + // Current window size (depends on whether it is expanded or not, if it is not expanded then it is the size of the titlebar) + extent_type size_; + + // Current size of contents, extents reach by the drawing cursor + extent_type size_of_content_; + + // Default width when creating items + value_type default_item_width_; + + // Current position of the scrollbar + value_type scroll_y_; + // Next position of the scrollbar + value_type scroll_next_y_; + // Whether the scrollbar is visible (present) + bool scroll_y_visible_; + + // Whether the window is visible (not closed) + bool visible_; + // Whether the window is collapsed or not (titlebar only) + bool collapsed_; + // == visible and not collapsed (utility, avoid pad) + bool skip_item_; + + // Whether any (window's) widget accesses the current window + bool accessed_; + + // Does auto-fit apply only when the size gets bigger + bool auto_fit_only_grows_; + // Frames requiring automatic resizing + // 2 boolean, avoid padding + std::int16_t auto_fit_frames_; + + // The number of frames the current window was last drawn, if the window is not visible it will stop counting + frame_count_type last_drawn_frame_; + + // Used as a seed when generating widget ids, the first element is the id of the window itself + std::vector id_stack_; + // Clip area of the window, there should be only two elements before/after drawing the widget, the window area and the canvas area + std::vector clip_rect_stack_; + + public: + // ----------------------------------- + // ctor & reset + + /** + * @brief Create a new window + * @param name window name + * @param flag window flag + * @param point window position + * @param size window size + * @param root the root of the window (topmost parent window), if the current window is not a child window, + * this parameter must be a null pointer (while the root of the window is itself) + */ + Window( + std::string_view name, + WindowFlag flag, + const point_type& point, + const extent_type& size, + Window* root + ) noexcept; + + /** + * @brief If a window has already been created, each "re-creation" only resets its flag (if necessary) + * @param flag window flag + * @param size If the current window is a child window, set the window size to @c size + */ + auto reset(WindowFlag flag, const extent_type& size) noexcept -> void; + + /** + * @brief Handles all input devices (mouse and keyboard) at the beginning of each frame (if necessary) + */ + auto handle_inputs(const Context& context) noexcept -> void; + + // ----------------------------------- + // DRAW BEGIN + + /** + * @brief Creating a canvas for the window + * @param context + * @param parent If the current window is a child window, then @c parent is its parent, otherwise this parameter must be a null pointer + * @param background_fill_alpha Alpha for window background color fill + * @return Whether the window is visible (not closed) + * @note Widgets can be drawn in the window if and only if its canvas has been created + */ + [[nodiscard]] auto begin_window( + Context& context, + Window* parent, + alpha_type background_fill_alpha + ) noexcept -> bool; + auto end_window(Context& context) noexcept -> void; + + auto begin_child_window( + Context& context, + std::string_view name, + alpha_type background_fill_alpha, + extent_type size, + bool border, + WindowFlag flag + ) noexcept -> void; + auto end_child_window(Context& context, Window& child) noexcept -> void; + + auto render(Context& context, draw_lists_type& draw_lists) const noexcept -> void; + + // ----------------------------------- + // WIDGETS + + auto draw_text(Context& context, std::string_view utf8_text) noexcept -> void; + + auto draw_button(Context& context, std::string_view utf8_text, extent_type size, bool repeat_when_held) noexcept -> bool; + + auto draw_small_button(Context& context, std::string_view utf8_text, bool repeat_when_held) noexcept -> bool; + + auto draw_radio_button(Context& context, std::string_view utf8_text, bool checked) noexcept -> bool; + + auto draw_checkbox(Context& context, std::string_view utf8_text, bool checked) noexcept -> bool; + + auto draw_slider( + Context& context, + std::string_view utf8_text, + float& reference, + float min, + float max, + std::uint32_t decimal_precision, + float power + ) noexcept -> bool; + + auto draw_slider_n( + Context& context, + std::string_view utf8_text, + std::span references, + float min, + float max, + std::uint32_t decimal_precision, + float power + ) noexcept -> bool; + + auto draw_combo( + Context& context, + std::string_view utf8_text, + std::span selections, + std::size_t& selected, + std::size_t show_selection_count + ) noexcept -> bool; + + auto draw_combo( + Context& context, + std::string_view utf8_text, + std::span selections, + std::size_t& selected, + std::size_t show_selection_count + ) noexcept -> bool; + + auto draw_combo( + Context& context, + std::string_view utf8_text, + std::span selections, + std::size_t& selected, + std::size_t show_selection_count + ) noexcept -> bool; + + // ----------------------------------- + // WIDGET LAYOUT + + auto same_line(Context& context, value_type column_width = layout_auto_size, value_type spacing_width = layout_auto_size) noexcept -> void; + + // ----------------------------------- + // CANVAS LAYOUT + + auto push_item_width(Context& context, Theme::value_type new_item_width) noexcept -> void; + auto pop_item_width(Context& context) noexcept -> void; + + auto push_text_wrap_width(Context& context, Theme::value_type new_wrap_width) noexcept -> void; + auto pop_text_wrap_width(Context& context) noexcept -> void; + + // ----------------------------------- + // ID + + [[nodiscard]] auto id_of_move(Context& context) const noexcept -> widget_id_type; + + [[nodiscard]] auto id_of_close(Context& context) const noexcept -> widget_id_type; + + [[nodiscard]] auto id_of_resize(Context& context) const noexcept -> widget_id_type; + + [[nodiscard]] auto id_of_scrollbar(Context& context) const noexcept -> widget_id_type; + + auto push_id(Context& context, std::string_view string) noexcept -> void; + + auto push_id(Context& context, const void* pointer) noexcept -> void; + + auto push_id(Context& context, widget_id_type value) noexcept -> void; + + auto pop_id(Context& context) noexcept -> void; + + // ----------------------------------- + // CLIP RECT + + auto push_clip_rect(Context& context, const rect_type& rect, bool clipped = true) noexcept -> void; + + auto pop_clip_rect(Context& context) noexcept -> void; + + // ----------------------------------- + // STATES + + [[nodiscard]] auto name() const noexcept -> std::string_view; + + [[nodiscard]] auto flag() const noexcept -> WindowFlag; + + [[nodiscard]] auto position() const noexcept -> point_type; + + [[nodiscard]] auto width() const noexcept -> value_type; + + [[nodiscard]] auto height() const noexcept -> value_type; + + [[nodiscard]] auto size() const noexcept -> extent_type; + + [[nodiscard]] auto rect() const noexcept -> rect_type; + + [[nodiscard]] auto visible() const noexcept -> bool; + + [[nodiscard]] auto collapsed() const noexcept -> bool; + + [[nodiscard]] auto root() const noexcept -> const Window&; + + [[nodiscard]] auto content_region_max(const Context& context) const noexcept -> extent_type; + [[nodiscard]] auto window_content_region_min(const Context& context) const noexcept -> extent_type; + [[nodiscard]] auto window_content_region_max(const Context& context) const noexcept -> extent_type; + + /** + * @brief Test if mouse cursor is hovering given rect + * @note @c rect is clipped by current clip rect setting + */ + [[nodiscard]] auto is_hovered(const Context& context, const rect_type& rect) const noexcept -> bool; + + [[nodiscard]] auto is_item_hovered(const Context& context) const noexcept -> bool; + [[nodiscard]] auto is_item_focused(const Context& context) const noexcept -> bool; + + // ----------------------------------- + // CONTEXT + + // auto show() noexcept -> void; + auto hide() noexcept -> void; + + /** + * @brief Find the first (more top-level) window that contains the location of the given point + */ + [[nodiscard]] auto find_hovered_window(const point_type& position, bool excludes_children) noexcept -> Window*; + }; +} diff --git a/src/io/device.ixx b/src/io/device.ixx deleted file mode 100644 index 0bca8796..00000000 --- a/src/io/device.ixx +++ /dev/null @@ -1,365 +0,0 @@ -// This file is part of prometheus -// Copyright (C) 2022-2024 Life4gal -// This file is subject to the license terms in the LICENSE file -// found in the top-level directory of this distribution. - -#if not GAL_PROMETHEUS_MODULE_FRAGMENT_DEFINED - -#include - -export module gal.prometheus:io.device; - -import std; - -#if GAL_PROMETHEUS_COMPILER_DEBUG -import :platform; -#endif - -#endif not GAL_PROMETHEUS_MODULE_FRAGMENT_DEFINED - -#if not GAL_PROMETHEUS_USE_MODULE - -#pragma once - -#include -#include -#include -#include - -#include - -#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE - -#endif - -#if GAL_PROMETHEUS_INTELLISENSE_WORKING -namespace GAL_PROMETHEUS_COMPILER_MODULE_NAMESPACE_PREFIX :: io -#else -GAL_PROMETHEUS_COMPILER_MODULE_NAMESPACE_EXPORT(io) -#endif -{ - enum class DeviceKey : std::uint16_t - { - NONE, - - // ======================================== - // keyboard <------------------------------------------------------- - // from left to right, top to bottom - // ======================================== - - KB_ESCAPE, - - // ================== - // function keys - - KB_F1, - KB_F2, - KB_F3, - KB_F4, - KB_F5, - KB_F6, - KB_F7, - KB_F8, - KB_F9, - KB_F10, - KB_F11, - FK_F12, - - // ================== - // main area - - // ` - KB_GRAVE_ACCENT, - KB_1, - KB_2, - KB_3, - KB_4, - KB_5, - KB_6, - KB_7, - KB_8, - KB_9, - KB_0, - // - OR _ - KB_MINUS, - // = OR + - KB_PLUS, - KB_BACKSPACE, - - KB_TAB, - KB_Q, - KB_W, - KB_E, - KB_R, - KB_T, - KB_Y, - KB_U, - KB_I, - KB_O, - KB_P, - // [ OR { - KB_LEFT_BRACKET, - // ] OR } - KB_RIGHT_BRACKET, - // \ OR | - KB_BACKSLASH, - - KB_CAPS_LOCK, - KB_A, - KB_S, - KB_D, - KB_F, - KB_G, - KB_H, - KB_J, - KB_K, - KB_L, - // ; OR : - KB_SEMICOLON, - // ' OR " - KB_QUOTATION, - KB_ENTER, - - KB_LEFT_SHIFT, - KB_Z, - KB_X, - KB_C, - KB_V, - KB_B, - KB_N, - KB_M, - // , OR < - KB_COMMA, - // . OR > - KB_PERIOD, - // / OR ? - KB_SLASH, - KB_RIGHT_SHIFT, - - KB_LEFT_CTRL, - KB_LEFT_SUPER, - KB_LEFT_ALT, - KB_SPACE, - KB_RIGHT_ALT, - KB_RIGHT_SUPER, - KB_MENU, - KB_RIGHT_CTRL, - - // ================== - // navigation keys - - KB_PAUSE, - KB_SCROLL_LOCK, - KB_PRINT_SCREEN, - - KB_INSERT, - KB_HOME, - KB_PAGE_UP, - KB_DELETE, - KB_END, - KB_PAGE_DOWN, - - KB_ARROW_UP, - KB_ARROW_LEFT, - KB_ARROW_DOWN, - KB_ARROW_RIGHT, - - // ================== - // Numeric Keypad - - KB_KEYPAD_NUM_LOCK, - // / - KB_KEYPAD_DIVIDE, - // * - KB_KEYPAD_MULTIPLY, - // - - KB_KEYPAD_MINUS, - - KB_KEYPAD_7, - KB_KEYPAD_8, - KB_KEYPAD_9, - KB_KEYPAD_PLUS, - - KB_KEYPAD_4, - KB_KEYPAD_5, - KB_KEYPAD_6, - - KB_KEYPAD_1, - KB_KEYPAD_2, - KB_KEYPAD_3, - KB_KEYPAD_ENTER, - - KB_KEYPAD_0, - KB_KEYPAD_DECIMAL, - - // ======================================== - // -------------------------------------------------------> keyboard - // ======================================== - - // incoming - }; - - enum class MouseButton : std::uint32_t - { - LEFT = 0, - MIDDLE = 1, - RIGHT = 2, - X1 = 3, - X2 = 4 - }; - - enum class MouseButtonStatus : std::uint32_t - { - PRESS, - RELEASE, - }; - - enum class DeviceActionType : std::uint8_t - { - MOUSE_MOV = 0, - MOUSE_BUTTON = 1, - MOUSE_WHEEL = 2, - - TEXT = 3, - KEY = 4, - }; - - class [[nodiscard]] DeviceEvent - { - public: - struct mouse_mov_type - { - float x; - float y; - }; - - struct mouse_button_type - { - MouseButton button; - MouseButtonStatus status; - }; - - struct mouse_wheel_type - { - float x; - float y; - }; - - struct text_type - { - std::uint32_t c; - std::uint32_t pad; - }; - - struct key_type - { - DeviceKey key; - bool down; - float analog_value; - - static_assert(sizeof(DeviceKey) == 2); - std::uint8_t pad; - }; - - using underlying_type = std::variant; - static_assert(std::is_same_v(DeviceActionType::MOUSE_MOV), underlying_type>, mouse_mov_type>); - static_assert(std::is_same_v(DeviceActionType::MOUSE_BUTTON), underlying_type>, mouse_button_type>); - static_assert(std::is_same_v(DeviceActionType::MOUSE_WHEEL), underlying_type>, mouse_wheel_type>); - static_assert(std::is_same_v(DeviceActionType::TEXT), underlying_type>, text_type>); - static_assert(std::is_same_v(DeviceActionType::KEY), underlying_type>, key_type>); - - private: - underlying_type underlying_; - - public: - template - requires std::is_constructible_v, Args...> - constexpr explicit DeviceEvent(std::in_place_type_t type, Args&&... args) noexcept - : underlying_{type, std::forward(args)...} {} - - template - requires std::is_constructible_v - constexpr explicit DeviceEvent(Args&&... args) noexcept - : underlying_{std::forward(args)...} {} - - [[nodiscard]] constexpr auto type() const noexcept -> DeviceActionType - { - return static_cast(underlying_.index()); - } - - // note: unchecked - template - [[nodiscard]] constexpr auto value() const noexcept -> const auto& - { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(underlying_.index() == static_cast(Type)); - return std::get(Type)>(underlying_); - } - - template - constexpr auto visit(Callable&& callable) const noexcept -> void - { - std::visit(std::forward(callable), underlying_); - } - }; - - class DeviceEventQueue - { - public: - template - using list_type = std::vector; - - using queue_type = list_type; - using size_type = queue_type::size_type; - - private: - queue_type queue_; - - public: - [[nodiscard]] constexpr auto size() const noexcept -> size_type - { - return queue_.size(); - } - - [[nodiscard]] constexpr auto consume() noexcept -> DeviceEvent - { - const auto e = queue_.back(); - queue_.pop_back(); - return e; - } - - constexpr auto mouse_move(const float x, const float y) noexcept -> void - { - // queue_.emplace_back(std::in_place_type, x, y); - queue_.emplace_back(DeviceEvent::mouse_mov_type{.x = x, .y = y}); - } - - constexpr auto mouse_button(const MouseButton button, const MouseButtonStatus status) noexcept -> void - { - // queue_.emplace_back(std::in_place_type, button, status); - queue_.emplace_back(DeviceEvent::mouse_button_type{.button = button, .status = status}); - } - - constexpr auto mouse_wheel(const float x, const float y) noexcept -> void - { - // queue_.emplace_back(std::in_place_type, x, y); - queue_.emplace_back(DeviceEvent::mouse_wheel_type{.x = x, .y = y}); - } - - constexpr auto text(const std::uint32_t c) noexcept -> void - { - // queue_.emplace_back(std::in_place_type, c); - queue_.emplace_back(DeviceEvent::text_type{.c = c, .pad = 0}); - } - - constexpr auto key(const DeviceKey key, const bool down, const float analog_value) noexcept -> void - { - // queue_.emplace_back(std::in_place_type, key, down, analog_value); - queue_.emplace_back(DeviceEvent::key_type{.key = key, .down = down, .analog_value = analog_value, .pad = 0}); - } - - constexpr auto key(const DeviceKey key, const bool down) noexcept -> void - { - return this->key(key, down, down ? 1.f : 0.f); - } - }; -} diff --git a/src/io/inputs.cpp b/src/io/inputs.cpp new file mode 100644 index 00000000..f93f16e2 --- /dev/null +++ b/src/io/inputs.cpp @@ -0,0 +1,583 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +#include +#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE + +namespace +{ + using namespace gal::prometheus; + using namespace io; + + [[nodiscard]] auto current_time() noexcept -> time_point_type + { + return std::chrono::time_point_cast(time_point_type::clock::now()); + } +} + +namespace gal::prometheus::io +{ + auto InputHandler::Mouse::reset(const time_point_type& time_point, const InputHandler& handler) noexcept -> void + { + std::ranges::for_each( + states, + [&](key_state_type& state) noexcept -> void + { + state.down_this_frame = 0; + + const auto view = state.press_records | std::views::reverse; + const auto it = std::ranges::find_if( + view, + [&](const press_record_type& record) noexcept -> bool + { + if (const auto interval = record.time_point - time_point; interval > handler.mouse_double_click_interval_threshold_) + { + return true; + } + + if (const auto distance = record.position.distance(position_current); distance > handler.mouse_double_click_distance_threshold_) + { + return true; + } + + return false; + } + ); + state.press_records.erase(state.press_records.begin(), it.base()); + } + ); + + position_delta = {0, 0}; + wheel = {0, 0}; + } + + auto InputHandler::Mouse::is_up(const InputHandler& self, const MouseButton button) const noexcept -> bool + { + std::ignore = self; + + const auto index = static_cast(button); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(index < states.size()); + + const auto& state = states[index]; + + return state.down_this_frame == 0; + } + + auto InputHandler::Mouse::is_down(const InputHandler& self, const MouseButton button) const noexcept -> bool + { + std::ignore = self; + + const auto index = static_cast(button); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(index < states.size()); + + const auto& state = states[index]; + + return state.down_this_frame != 0; + } + + auto InputHandler::Mouse::is_clicked(const InputHandler& self, const MouseButton button, const bool repeat) const noexcept -> bool + { + std::ignore = self; + + const auto index = static_cast(button); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(index < states.size()); + + const auto& state = states[index]; + + if (state.down_this_frame != 0) + { + return true; + } + + if (repeat and state.down_time != 0) + { + const auto now = current_time(); + + if (const auto interval = duration_type{now.time_since_epoch().count() - state.down_time}; + interval > self.mouse_repeat_click_delay_) + { + // todo: 1/60 second? + constexpr auto tick_time{std::chrono::milliseconds{16}}; + const auto half = self.mouse_repeat_click_rate_ / 2; + + const auto v1 = (interval - self.mouse_repeat_click_delay_) % self.mouse_repeat_click_rate_; + const auto v2 = (interval - tick_time) % self.mouse_repeat_click_rate_; + + return (v1 > half) != (v2 > half); + } + } + + return false; + } + + auto InputHandler::Mouse::is_double_clicked(const InputHandler& self, const MouseButton button) const noexcept -> bool + { + const auto index = static_cast(button); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(index < states.size()); + + const auto& state = states[index]; + + if (state.down_this_frame == 0) + { + return false; + } + + const auto records_count = state.press_records.size(); + if (records_count < 2) + { + return false; + } + + const auto& [c1_time_point, c1_position] = state.press_records[records_count - 2]; + const auto& [c2_time_point, c2_position] = state.press_records[records_count - 1]; + + if (const auto distance = c2_position.distance(c1_position); distance > self.mouse_double_click_distance_threshold_) + { + return false; + } + + if (const auto time = c2_time_point - c1_time_point; time > self.mouse_double_click_interval_threshold_) + { + return false; + } + + return true; + } + + auto InputHandler::Mouse::is_pressing(const InputHandler& self, const MouseButton button) const noexcept -> bool + { + std::ignore = self; + + const auto index = static_cast(button); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(index < states.size()); + + const auto& state = states[index]; + + return state.down_time != 0; + } + + auto InputHandler::Keyboard::reset(const time_point_type& time_point, const InputHandler& handler) noexcept -> void + { + std::ranges::for_each( + states, + [&](key_state_type& state) noexcept -> void + { + state.down_this_frame = 0; + + const auto view = state.press_records | std::views::reverse; + const auto it = std::ranges::find_if( + view, + [&](const press_record_type& record) noexcept -> bool + { + if (const auto interval = record.time_point - time_point; interval > handler.mouse_double_click_interval_threshold_) + { + return true; + } + + return false; + } + ); + state.press_records.erase(state.press_records.begin(), it.base()); + } + ); + } + + auto InputHandler::Keyboard::is_up(const InputHandler& self, const KeyboardKeyCode code) const noexcept -> bool + { + std::ignore = self; + + const auto index = static_cast(code); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(index < states.size()); + + const auto& state = states[index]; + + return state.down_this_frame == 0; + } + + auto InputHandler::Keyboard::is_down(const InputHandler& self, const KeyboardKeyCode code) const noexcept -> bool + { + std::ignore = self; + + const auto index = static_cast(code); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(index < states.size()); + + const auto& state = states[index]; + + return state.down_this_frame != 0; + } + + auto InputHandler::Keyboard::is_pressing(const InputHandler& self, const KeyboardKeyCode code) const noexcept -> bool + { + std::ignore = self; + + const auto index = static_cast(code); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(index < states.size()); + + const auto& state = states[index]; + + return state.down_time != 0; + } + + auto InputHandler::Keyboard::is_combination_pressing(const InputHandler& self, const std::span codes) const noexcept -> bool + { + const auto first = codes | std::views::take(codes.size() - 1); + const auto last = codes.back(); + + if (not std::ranges::all_of( + first, + [&](const KeyboardKeyCode code) noexcept -> bool + { + return is_pressing(self, code); + } + ) + ) + { + return false; + } + + return is_down(self, last); + } + + auto InputHandler::Keyboard::is_combination_pressing(const InputHandler& self, std::initializer_list codes) const noexcept -> bool + { + const std::span list{codes}; + + return is_combination_pressing(self, list); + } + + auto InputHandler::process_event(const input_event_type& event) noexcept -> void + { + // ============ + // MOUSE + // ============ + const auto handle_mouse_move = [this](const MouseMoveEventData& data) noexcept -> void + { + mouse_.position_previous = mouse_.position_current; + mouse_.position_current = data.position; + mouse_.position_delta = (mouse_.position_current - mouse_.position_previous).to(); + }; + const auto handle_mouse_button = [this](const MouseButtonEventData& data) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(data.action != DeviceKeyAction::NONE); + + const auto button = static_cast(data.button); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(button < mouse_.states.size()); + + if (auto& [down_this_frame, down_time, press_records] = mouse_.states[button]; + data.action == DeviceKeyAction::DOWN) + { + const auto now = current_time(); + + down_this_frame = 1; + down_time = now.time_since_epoch().count(); + press_records.emplace_back(Mouse::press_record_type{.time_point = now, .position = mouse_.position_current}); + } + else + { + down_this_frame = 0; + down_time = 0; + } + }; + const auto handle_mouse_wheel = [this](const MouseWheelEventData& data) noexcept -> void + { + mouse_.wheel = data.value; + }; + + // ============ + // KEYBOARD + // ============ + const auto handle_keyboard = [this](const KeyboardEventData& data) noexcept -> void + { + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(data.code != KeyboardKeyCode::NONE); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(data.action != DeviceKeyAction::NONE); + + const auto code = static_cast(data.code); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(code < keyboard_.states.size()); + + if (auto& [down_this_frame, down_time, press_records] = keyboard_.states[code]; + data.action == DeviceKeyAction::DOWN) + { + const auto now = current_time(); + + down_this_frame = 1; + down_time = now.time_since_epoch().count(); + press_records.emplace_back(Keyboard::press_record_type{.time_point = now}); + } + else + { + down_this_frame = 0; + down_time = 0; + } + }; + + // ============ + // DISPLAY + // ============ + const auto handle_display_move = [this](const DisplayMoveEventData& data) noexcept -> void + { + display_.position = data.position; + }; + const auto handle_display_resize = [this](const DisplayResizeEventData& data) noexcept -> void + { + display_.size = data.size; + }; + + const auto visitor = functional::overloaded{ + handle_mouse_move, + handle_mouse_button, + handle_mouse_wheel, + handle_keyboard, + handle_display_move, + handle_display_resize, + [](const auto& data) noexcept -> void + { + std::ignore = data; + GAL_PROMETHEUS_ERROR_DEBUG_UNREACHABLE(); + } + }; + + std::visit(visitor, event); + } + + InputHandler::InputHandler() noexcept + : + mouse_ + { + .position_current = {0, 0}, + .position_previous = {0, 0}, + .position_delta = {0, 0}, + .wheel = {0, 0}, + .states = {} + }, + keyboard_ + { + .states = {} + }, + display_ + { + .position = {0, 0}, + .size = {0, 0} + }, + mouse_double_click_interval_threshold_{std::chrono::milliseconds{300}}, + mouse_double_click_distance_threshold_{6}, + mouse_repeat_click_delay_{std::chrono::milliseconds{275}}, + mouse_repeat_click_rate_{std::chrono::milliseconds{50}} {} + + auto InputHandler::begin_frame() noexcept -> void + { + event_queue_type queue{}; + { + std::scoped_lock lock{mutex_}; + + event_queue_.swap(queue); + } + + std::ranges::for_each( + queue, + [this](const input_event_type& event) noexcept -> void + { + process_event(event); + } + ); + } + + auto InputHandler::end_frame() noexcept -> void + { + const auto now = current_time(); + + // ============ + // MOUSE + // ============ + mouse_.reset(now, *this); + + // ============ + // KEYBOARD + // ============ + keyboard_.reset(now, *this); + + // ============ + // DISPLAY + // ============ + // + } + + auto InputHandler::push_event(const input_event_type& event) noexcept -> void + { + std::scoped_lock lock{mutex_}; + event_queue_.push_back(event); + } + + auto InputHandler::mouse_proxy::position() const noexcept -> position_type + { + const auto& handler = self.get(); + const auto& mouse = handler.mouse_; + + return mouse.position_current; + } + + auto InputHandler::mouse_proxy::position_delta() const noexcept -> extent_type + { + const auto& handler = self.get(); + const auto& mouse = handler.mouse_; + + return mouse.position_delta; + } + + auto InputHandler::mouse_proxy::wheel() const noexcept -> extent_type + { + const auto& handler = self.get(); + const auto& mouse = handler.mouse_; + + return mouse.wheel; + } + + auto InputHandler::mouse_proxy::is_up(const MouseButton button) const noexcept -> bool + { + const auto& handler = self.get(); + const auto& mouse = handler.mouse_; + + return mouse.is_up(handler, button); + } + + auto InputHandler::mouse_proxy::is_down(const MouseButton button) const noexcept -> bool + { + const auto& handler = self.get(); + const auto& mouse = handler.mouse_; + + return mouse.is_down(handler, button); + } + + auto InputHandler::mouse_proxy::is_click(const MouseButton button, const bool repeat) const noexcept -> bool + { + const auto& handler = self.get(); + const auto& mouse = handler.mouse_; + + return mouse.is_clicked(handler, button, repeat); + } + + auto InputHandler::mouse_proxy::is_double_click(const MouseButton button) const noexcept -> bool + { + const auto& handler = self.get(); + const auto& mouse = handler.mouse_; + + return mouse.is_double_clicked(handler, button); + } + + auto InputHandler::mouse_proxy::is_pressing(const MouseButton button) const noexcept -> bool + { + const auto& handler = self.get(); + const auto& mouse = handler.mouse_; + + return mouse.is_pressing(handler, button); + } + + auto InputHandler::mouse() const noexcept -> mouse_proxy + { + return {.self = *this}; + } + + auto InputHandler::keyboard_proxy::is_up(const KeyboardKeyCode code) const noexcept -> bool + { + const auto& handler = self.get(); + const auto& keyboard = handler.keyboard_; + + return keyboard.is_up(handler, code); + } + + auto InputHandler::keyboard_proxy::is_down(const KeyboardKeyCode code) const noexcept -> bool + { + const auto& handler = self.get(); + const auto& keyboard = handler.keyboard_; + + return keyboard.is_down(handler, code); + } + + auto InputHandler::keyboard_proxy::is_pressing(const KeyboardKeyCode code) const noexcept -> bool + { + const auto& handler = self.get(); + const auto& keyboard = handler.keyboard_; + + return keyboard.is_pressing(handler, code); + } + + auto InputHandler::keyboard_proxy::is_combination_pressing(const std::span codes) const noexcept -> bool + { + const auto& handler = self.get(); + const auto& keyboard = handler.keyboard_; + + return keyboard.is_combination_pressing(handler, codes); + } + + auto InputHandler::keyboard_proxy::is_combination_pressing(const std::initializer_list codes) const noexcept -> bool + { + const auto& handler = self.get(); + const auto& keyboard = handler.keyboard_; + + return keyboard.is_combination_pressing(handler, codes); + } + + auto InputHandler::keyboard() const noexcept -> keyboard_proxy + { + return {.self = *this}; + } + + auto InputHandler::display_proxy::position() const noexcept -> position_type + { + const auto& handler = self.get(); + // ReSharper disable once CppUseStructuredBinding + const auto& display = handler.display_; + + return display.position; + } + + auto InputHandler::display_proxy::size() const noexcept -> extent_type + { + const auto& handler = self.get(); + // ReSharper disable once CppUseStructuredBinding + const auto& display = handler.display_; + + return display.size; + } + + auto InputHandler::display() const noexcept -> display_proxy + { + return {.self = *this}; + } + + auto InputHandler::config_proxy::set_mouse_double_click_interval_threshold(const duration_type interval) noexcept -> void + { + auto& handler = self.get(); + + handler.mouse_double_click_interval_threshold_ = interval; + } + + auto InputHandler::config_proxy::set_mouse_double_click_distance_threshold(const value_type distance) noexcept -> void + { + auto& handler = self.get(); + + handler.mouse_double_click_distance_threshold_ = distance; + } + + auto InputHandler::config_proxy::set_mouse_repeat_click_delay(const duration_type delay) noexcept -> void + { + auto& handler = self.get(); + + handler.mouse_repeat_click_delay_ = delay; + } + + auto InputHandler::config_proxy::set_mouse_repeat_click_rate(const duration_type rate) noexcept -> void + { + auto& handler = self.get(); + + handler.mouse_repeat_click_rate_ = rate; + } + + auto InputHandler::config() noexcept -> config_proxy + { + return {.self = *this}; + } +} diff --git a/src/io/inputs.hpp b/src/io/inputs.hpp new file mode 100644 index 00000000..e2ce6b32 --- /dev/null +++ b/src/io/inputs.hpp @@ -0,0 +1,540 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace gal::prometheus::io +{ + using position_type = primitive::basic_point_2d; + using extent_type = primitive::basic_extent_2d; + + using duration_type = std::chrono::milliseconds; + using time_point_type = std::chrono::time_point; + + // ====================================================================== + // MOUSE + // ====================================================================== + + enum class MouseButton : std::uint8_t + { + LEFT = 0, + MIDDLE, + RIGHT, + X1, + X2, + + INTERNAL_COUNT, + NONE, + }; + + constexpr auto total_mouse_button = static_cast(MouseButton::INTERNAL_COUNT); + + enum class MouseWheel : std::uint8_t + { + HORIZONTAL = 0, + VERTICAL, + + NONE, + }; + + // ====================================================================== + // KEYBOARD + // ====================================================================== + + // todo: keycode layout + enum class KeyboardKeyCode : std::uint8_t + { + // ======================================== + // from left to right, top to bottom + // ======================================== + + KB_ESCAPE = 0, + + // ================== + // function keys + + KB_F1, + KB_F2, + KB_F3, + KB_F4, + KB_F5, + KB_F6, + KB_F7, + KB_F8, + KB_F9, + KB_F10, + KB_F11, + FK_F12, + + // ================== + // main area + + // ` + KB_GRAVE_ACCENT, + KB_1, + KB_2, + KB_3, + KB_4, + KB_5, + KB_6, + KB_7, + KB_8, + KB_9, + KB_0, + // - OR _ + KB_MINUS, + // = OR + + KB_PLUS, + KB_BACKSPACE, + + KB_TAB, + KB_Q, + KB_W, + KB_E, + KB_R, + KB_T, + KB_Y, + KB_U, + KB_I, // NOLINT(misc-confusable-identifiers) + KB_O, // NOLINT(misc-confusable-identifiers) + KB_P, + // [ OR { + KB_LEFT_BRACKET, + // ] OR } + KB_RIGHT_BRACKET, + // \ OR | + KB_BACKSLASH, + + KB_CAPS_LOCK, + KB_A, + KB_S, + KB_D, + KB_F, + KB_G, + KB_H, + KB_J, + KB_K, + KB_L, + // ; OR : + KB_SEMICOLON, + // ' OR " + KB_QUOTATION, + KB_ENTER, + + KB_LEFT_SHIFT, + KB_Z, + KB_X, + KB_C, + KB_V, + KB_B, + KB_N, + KB_M, + // , OR < + KB_COMMA, + // . OR > + KB_PERIOD, + // / OR ? + KB_SLASH, + KB_RIGHT_SHIFT, + + KB_LEFT_CTRL, + KB_LEFT_SUPER, + KB_LEFT_ALT, + KB_SPACE, + KB_RIGHT_ALT, + KB_RIGHT_SUPER, + KB_MENU, + KB_RIGHT_CTRL, + + // ================== + // navigation keys + + KB_PAUSE, + KB_SCROLL_LOCK, + KB_PRINT_SCREEN, + + KB_INSERT, + KB_HOME, + KB_PAGE_UP, + KB_DELETE, + KB_END, + KB_PAGE_DOWN, + + KB_ARROW_UP, + KB_ARROW_LEFT, + KB_ARROW_DOWN, + KB_ARROW_RIGHT, + + // ================== + // numeric keypad + + KB_KEYPAD_NUM_LOCK, + // / + KB_KEYPAD_DIVIDE, + // * + KB_KEYPAD_MULTIPLY, + // - + KB_KEYPAD_MINUS, + + KB_KEYPAD_7, + KB_KEYPAD_8, + KB_KEYPAD_9, + KB_KEYPAD_PLUS, + + KB_KEYPAD_4, + KB_KEYPAD_5, + KB_KEYPAD_6, + + KB_KEYPAD_1, + KB_KEYPAD_2, + KB_KEYPAD_3, + KB_KEYPAD_ENTER, + + KB_KEYPAD_0, + KB_KEYPAD_DECIMAL, + + INTERNAL_COUNT, + NONE, + }; + + constexpr auto total_keyboard_code = static_cast(KeyboardKeyCode::INTERNAL_COUNT); + + // ====================================================================== + // ALL DEVICE + // ====================================================================== + + enum class DeviceKeyAction : std::uint8_t + { + NONE = 0, + + UP, + DOWN, + }; + + enum class DeviceType : std::uint8_t + { + NONE = 0, + + MOUSE, + KEYBOARD, + DISPLAY, + }; + + // ====================================================================== + // MOUSE + // ====================================================================== + + // 8 bytes + class MouseMoveEventData final + { + public: + position_type position{0, 0}; + }; + + // 2 bytes + class MouseButtonEventData final + { + public: + MouseButton button{MouseButton::NONE}; + DeviceKeyAction action{DeviceKeyAction::NONE}; + }; + + // 8 bytes + class MouseWheelEventData final + { + public: + extent_type value; + }; + + // ====================================================================== + // KEYBOARD + // ====================================================================== + + // 2 bytes + class KeyboardEventData final + { + public: + KeyboardKeyCode code{KeyboardKeyCode::NONE}; + DeviceKeyAction action{DeviceKeyAction::NONE}; + }; + + // ====================================================================== + // DISPLAY + // ====================================================================== + + // 8 bytes + class DisplayMoveEventData final + { + public: + position_type position{0, 0}; + }; + + // 8 bytes + class DisplayResizeEventData final + { + public: + extent_type size{0, 0}; + }; + + // ====================================================================== + // Handler + // ====================================================================== + + using input_event_type = std::variant< + MouseMoveEventData, + MouseButtonEventData, + MouseWheelEventData, + KeyboardEventData, + DisplayMoveEventData, + DisplayResizeEventData + >; + + class InputHandler + { + public: + using mutex_type = std::mutex; + using event_queue_type = std::vector; + + using value_type = extent_type::value_type; + + private: + mutex_type mutex_; + event_queue_type event_queue_; + + // ============ + // MOUSE + // ============ + + class Mouse final + { + public: + struct press_record_type + { + time_point_type time_point{}; + position_type position{0, 0}; + }; + + struct key_state_type + { + static_assert(sizeof(time_point_type) == sizeof(std::uint64_t)); + + // Whether this key is pressed or not in this frame (depends on DeviceKeyAction), reset to 0 every frame + std::uint64_t down_this_frame : 1 {0}; + // If pressed, the time of the moment of the press, this value is only updated the next time it is released, not reset every frame + // fixme: If the runtime is too long, the 63-bit will not fully represent the 64-bit time values + std::uint64_t down_time : 63 {0}; + + std::vector press_records{}; + }; + + using mouse_states_type = std::array; + + // Update every frame + // Current mouse position (this frame) + position_type position_current; + // Update every frame + // Previous mouse position (this frame) + position_type position_previous; + // Update every frame + // Mouse distance between two frames + extent_type position_delta; + // Update every frame + // Current mouse wheel value + extent_type wheel; + // Update every frame + // Current mouse button state (this frame) + mouse_states_type states; + + auto reset(const time_point_type& time_point, const InputHandler& handler) noexcept -> void; + + [[nodiscard]] auto is_up(const InputHandler& self, MouseButton button) const noexcept -> bool; + [[nodiscard]] auto is_down(const InputHandler& self, MouseButton button) const noexcept -> bool; + + [[nodiscard]] auto is_clicked(const InputHandler& self, MouseButton button, bool repeat) const noexcept -> bool; + [[nodiscard]] auto is_double_clicked(const InputHandler& self, MouseButton button) const noexcept -> bool; + + [[nodiscard]] auto is_pressing(const InputHandler& self, MouseButton button) const noexcept -> bool; + }; + + Mouse mouse_; + + // ============ + // KEYBOARD + // ============ + + class Keyboard final + { + public: + struct press_record_type + { + time_point_type time_point{}; + }; + + struct key_state_type + { + static_assert(sizeof(time_point_type) == sizeof(std::uint64_t)); + + // Whether this key is pressed or not in this frame (depends on DeviceKeyAction), reset to 0 every frame + std::uint64_t down_this_frame : 1 {0}; + // If pressed, the time of the moment of the press, this value is only updated the next time it is released, not reset every frame + // fixme: If the runtime is too long, the 63-bit will not fully represent the 64-bit time values + std::uint64_t down_time : 63 {0}; + + std::vector press_records{}; + }; + + using keyboard_states_type = std::array; + + // Update every frame + // Current keyboard button state (this frame) + keyboard_states_type states; + + auto reset(const time_point_type& time_point, const InputHandler& handler) noexcept -> void; + + [[nodiscard]] auto is_up(const InputHandler& self, KeyboardKeyCode code) const noexcept -> bool; + [[nodiscard]] auto is_down(const InputHandler& self, KeyboardKeyCode code) const noexcept -> bool; + + [[nodiscard]] auto is_pressing(const InputHandler& self, KeyboardKeyCode code) const noexcept -> bool; + + [[nodiscard]] auto is_combination_pressing(const InputHandler& self, std::span codes) const noexcept -> bool; + [[nodiscard]] auto is_combination_pressing(const InputHandler& self, std::initializer_list codes) const noexcept -> bool; + }; + + Keyboard keyboard_; + + // ============ + // DISPLAY + // ============ + + struct display_data_type + { + // Update once (it can also be updated every frame basis if desired) + // Current window (viewport) position + position_type position; + // Update once (it can also be updated every frame basis if desired) + // Current window (viewport) size + extent_type size; + }; + + display_data_type display_; + + // ============ + // CONFIG + // ============ + + // Update once (it can also be updated every frame basis if desired) + // The interval between two clicks is less than this threshold to be considered as a double click, in seconds + duration_type mouse_double_click_interval_threshold_; + // Update once (it can also be updated every frame basis if desired) + // The mouse position delta between two clicks is less than this threshold to be considered as a double click + value_type mouse_double_click_distance_threshold_; + // Update once (it can also be updated every frame basis if desired) + // When holding a button, time before it starts repeating, in seconds + duration_type mouse_repeat_click_delay_; + // Update once (it can also be updated every frame basis if desired) + // When holding a button, rate at which it repeats, in seconds + duration_type mouse_repeat_click_rate_; + + auto process_event(const input_event_type& event) noexcept -> void; + + public: + InputHandler(const InputHandler&) noexcept = delete; + InputHandler(InputHandler&&) noexcept = delete; + auto operator=(const InputHandler&) noexcept -> InputHandler& = delete; + auto operator=(InputHandler&&) noexcept -> InputHandler& = delete; + ~InputHandler() noexcept = default; + + InputHandler() noexcept; + + auto begin_frame() noexcept -> void; + + auto end_frame() noexcept -> void; + + auto push_event(const input_event_type& event) noexcept -> void; + + // ============ + // MOUSE + // ============ + + struct [[nodiscard]] mouse_proxy final + { + memory::RefWrapper self; + + [[nodiscard]] auto position() const noexcept -> position_type; + [[nodiscard]] auto position_delta() const noexcept -> extent_type; + [[nodiscard]] auto wheel() const noexcept -> extent_type; + + [[nodiscard]] auto is_up(MouseButton button) const noexcept -> bool; + [[nodiscard]] auto is_down(MouseButton button) const noexcept -> bool; + + [[nodiscard]] auto is_click(MouseButton button, bool repeat = false) const noexcept -> bool; + [[nodiscard]] auto is_double_click(MouseButton button) const noexcept -> bool; + + [[nodiscard]] auto is_pressing(MouseButton button) const noexcept -> bool; + }; + + [[nodiscard]] auto mouse() const noexcept -> mouse_proxy; + + // ============ + // KEYBOARD + // ============ + + struct [[nodiscard]] keyboard_proxy final + { + memory::RefWrapper self; + + [[nodiscard]] auto is_up(KeyboardKeyCode code) const noexcept -> bool; + [[nodiscard]] auto is_down(KeyboardKeyCode code) const noexcept -> bool; + + [[nodiscard]] auto is_pressing(KeyboardKeyCode code) const noexcept -> bool; + + [[nodiscard]] auto is_combination_pressing(std::span codes) const noexcept -> bool; + [[nodiscard]] auto is_combination_pressing(std::initializer_list codes) const noexcept -> bool; + }; + + [[nodiscard]] auto keyboard() const noexcept -> keyboard_proxy; + + // ============ + // DISPLAY + // ============ + + struct [[nodiscard]] display_proxy final + { + memory::RefWrapper self; + + [[nodiscard]] auto position() const noexcept -> position_type; + [[nodiscard]] auto size() const noexcept -> extent_type; + }; + + [[nodiscard]] auto display() const noexcept -> display_proxy; + + // ============ + // CONFIG + // ============ + + struct [[nodiscard]] config_proxy final + { + memory::RefWrapper self; + + auto set_mouse_double_click_interval_threshold(duration_type interval) noexcept -> void; + auto set_mouse_double_click_distance_threshold(value_type distance) noexcept -> void; + auto set_mouse_repeat_click_delay(duration_type delay) noexcept -> void; + auto set_mouse_repeat_click_rate(duration_type rate) noexcept -> void; + }; + + [[nodiscard]] auto config() noexcept -> config_proxy; + }; +} diff --git a/src/io/io.hpp b/src/io/io.hpp new file mode 100644 index 00000000..1d4d9b65 --- /dev/null +++ b/src/io/io.hpp @@ -0,0 +1,8 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include diff --git a/src/io/io.ixx b/src/io/io.ixx deleted file mode 100644 index 1468b62a..00000000 --- a/src/io/io.ixx +++ /dev/null @@ -1,22 +0,0 @@ -// This file is part of prometheus -// Copyright (C) 2022-2024 Life4gal -// This file is subject to the license terms in the LICENSE file -// found in the top-level directory of this distribution. - -#if not GAL_PROMETHEUS_MODULE_FRAGMENT_DEFINED - -#include - -export module gal.prometheus:io; - -export import :io.device; - -#endif not GAL_PROMETHEUS_MODULE_FRAGMENT_DEFINED - -#if not GAL_PROMETHEUS_USE_MODULE - -#pragma once - -#include - -#endif diff --git a/src/math/cmath.hpp b/src/math/cmath.hpp index f8a12ea4..27871ad3 100644 --- a/src/math/cmath.hpp +++ b/src/math/cmath.hpp @@ -12,7 +12,7 @@ #include -#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE +// #include GAL_PROMETHEUS_ERROR_DEBUG_MODULE #if not defined(__cpp_lib_constexpr_cmath) or __cpp_lib_constexpr_cmath < 202306L #define CMATH_WORKAROUND_REQUIRED @@ -120,7 +120,7 @@ namespace gal::prometheus::math // ReSharper disable once IdentifierTypo [[nodiscard]] constexpr auto tgamma(const T value) noexcept -> T { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(value >= T{0}); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(value >= T{0}); if constexpr (not std::is_floating_point_v) { @@ -149,7 +149,7 @@ namespace gal::prometheus::math requires std::is_arithmetic_v [[nodiscard]] constexpr auto pow(const T base, const int exp) noexcept -> T { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(exp >= 0); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(exp >= 0); #if defined(CMATH_WORKAROUND_REQUIRED) GAL_PROMETHEUS_SEMANTIC_IF_CONSTANT_EVALUATED @@ -170,7 +170,7 @@ namespace gal::prometheus::math requires std::is_arithmetic_v [[nodiscard]] constexpr auto sqrt(const T value) noexcept -> T { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(value >= 0); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(value >= 0); #if defined(CMATH_WORKAROUND_REQUIRED) GAL_PROMETHEUS_SEMANTIC_IF_CONSTANT_EVALUATED diff --git a/src/memory/reference_wrapper.hpp b/src/memory/reference_wrapper.hpp new file mode 100644 index 00000000..1bb03909 --- /dev/null +++ b/src/memory/reference_wrapper.hpp @@ -0,0 +1,193 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include + +namespace gal::prometheus::memory +{ + namespace reference_wrapper_detail + { + template + // ReSharper disable once CppFunctionIsNotImplemented + auto ctor(std::type_identity_t) noexcept -> void; + + template + auto ctor(std::type_identity_t) noexcept -> void = delete; + + template + struct ref_wrap_has_ctor_from : std::bool_constant< + requires + { + reference_wrapper_detail::ctor(std::declval()); + } + > {}; + } + + /** + * @brief A wrapper for std::reference_wrapper, but propagating const. + */ + template + class RefWrapper + { + public: + static_assert(std::is_object_v || std::is_function_v, "reference_wrapper requires T to be an object type or a function type."); + + using type = T; + + private: + type* pointer_; + + public: + template + requires std::conjunction_v< + std::negation, RefWrapper>>, + reference_wrapper_detail::ref_wrap_has_ctor_from + > + constexpr explicit(false) RefWrapper(U&& value) noexcept(noexcept(reference_wrapper_detail::ctor(std::declval()))) + : pointer_{nullptr} + { + auto& ref = std::forward(value); + pointer_ = std::addressof(ref); + } + + // RefWrapper --(implicit)--> T& + // ReSharper disable once CppNonExplicitConversionOperator + constexpr explicit(false) operator type&() noexcept + { + return *pointer_; + } + + // RefWrapper --(implicit)--> const T& + // ReSharper disable once CppNonExplicitConversionOperator + constexpr explicit(false) operator type&() const noexcept + requires(std::is_const_v) // avoid redefinition (RefWrapper) + { + return *pointer_; + } + + // const RefWrapper --(implicit)--> const T& + // ReSharper disable once CppNonExplicitConversionOperator + constexpr explicit (false) operator const type&() const noexcept // + requires (not std::is_const_v) // avoid redefinition (RefWrapper) + { + return *pointer_; + } + + [[nodiscard]] constexpr auto get() noexcept -> type& + { + return *pointer_; + } + + [[nodiscard]] constexpr auto get() const noexcept -> const type& + { + return *pointer_; + } + + template + constexpr auto operator()(Args&&... args) // + noexcept(noexcept(std::invoke(*pointer_, std::forward(args)...))) // + -> decltype(std::invoke(*pointer_, std::forward(args)...)) // + { + return std::invoke(*pointer_, std::forward(args)...); + } + }; + + template + // ReSharper disable once CppInconsistentNaming + RefWrapper(T&) -> RefWrapper; + + template + [[nodiscard]] constexpr auto ref(T& value) noexcept -> RefWrapper + { + return RefWrapper{value}; + } + + template + [[nodiscard]] constexpr auto ref(const T&&) noexcept -> RefWrapper = delete; + + template + [[nodiscard]] constexpr auto ref(RefWrapper value) noexcept -> RefWrapper + { + return value; + } + + template + [[nodiscard]] constexpr auto ref(Ref value) noexcept -> RefWrapper // + requires std::is_same_v> + { + return RefWrapper{value.get()}; + } + + template + [[nodiscard]] constexpr auto cref(const T& value) noexcept -> RefWrapper + { + return RefWrapper{value}; + } + + template + [[nodiscard]] constexpr auto cref(const T&&) noexcept -> RefWrapper = delete; + + template + [[nodiscard]] constexpr auto cref(RefWrapper value) noexcept -> RefWrapper + { + return value; + } + + template + [[nodiscard]] constexpr auto cref(Ref value) noexcept -> RefWrapper // + requires std::is_same_v> + { + return RefWrapper{value.get()}; + } + + namespace reference_wrapper_detail + { + template + concept ref_wrapper_t = std::is_same_v>; + + // for std::basic_common_reference + template + concept ref_wrap_common_reference_exists_with = + ref_wrapper_t and + requires + { + typename std::common_reference_t; + } and + std::convertible_to>; + } +} + +namespace std +{ + // fixme: std::invoke + + template + struct unwrap_reference> // NOLINT(cert-dcl58-cpp) + { + using type = T&; + }; + + template typename RefWrapQ, template typename TypeQ> + requires ( + gal::prometheus::memory::reference_wrapper_detail::ref_wrap_common_reference_exists_with, TypeQ> and + not gal::prometheus::memory::reference_wrapper_detail::ref_wrap_common_reference_exists_with, RefWrapQ> + ) + struct basic_common_reference // NOLINT(cert-dcl58-cpp) + { + using type = std::common_reference_t>; + }; + + template typename TypeQ, template typename RefWrapQ> + requires ( + gal::prometheus::memory::reference_wrapper_detail::ref_wrap_common_reference_exists_with, TypeQ> and + not gal::prometheus::memory::reference_wrapper_detail::ref_wrap_common_reference_exists_with, RefWrapQ> + ) + struct basic_common_reference // NOLINT(cert-dcl58-cpp) + { + using type = std::common_reference_t>; + }; +} diff --git a/src/memory/unique_ptr.hpp b/src/memory/unique_ptr.hpp new file mode 100644 index 00000000..d3389a6e --- /dev/null +++ b/src/memory/unique_ptr.hpp @@ -0,0 +1,590 @@ +// This file is part of prometheus +// Copyright (C) 2022-2025 Life4gal +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#pragma once + +#include +#include + +namespace gal::prometheus::memory +{ + namespace unique_ptr_detail + { + template + struct default_constructible_deleter : std::conjunction< + std::negation>, + std::is_constructible + > {}; + + template + constexpr auto default_constructible_deleter_v = default_constructible_deleter::value; + + template + concept default_constructible_deleter_t = default_constructible_deleter_v; + } + + template> + class UniquePointer; + + /** + * @brief A wrapper for std::unique_ptr, but propagating const. + */ + template + class UniquePointer final + { + template + friend class UniquePointer; + + using phantom_type = std::unique_ptr; + + public: + using pointer = typename phantom_type::pointer; + using element_type = typename phantom_type::element_type; + using deleter_type = typename phantom_type::deleter_type; + + private: + template + constexpr static auto enable_conversion = + std::conjunction_v< + std::negation>, + std::is_convertible::pointer, pointer>, + Trait + >; + + phantom_type phantom_; + + public: + // ===================================== + // UniquePointer p{}; + + constexpr UniquePointer() noexcept // + requires unique_ptr_detail::default_constructible_deleter_v + : phantom_{} {} + + // ===================================== + // UniquePointer p{nullptr}; + + constexpr explicit(false) UniquePointer(std::nullptr_t) noexcept // + requires unique_ptr_detail::default_constructible_deleter_v + : phantom_{} {} + + constexpr auto operator=(std::nullptr_t) noexcept -> UniquePointer& + { + phantom_ = nullptr; + return *this; + } + + // ===================================== + // UniquePointer p{new T{...}}; + + constexpr explicit UniquePointer(pointer ptr) noexcept // + requires unique_ptr_detail::default_constructible_deleter_v + : phantom_{ptr} {} + + constexpr UniquePointer(pointer ptr, const deleter_type& deleter) noexcept // + requires std::conjunction_v< + std::is_constructible + > + : phantom_{ptr, deleter} {} + + constexpr UniquePointer(pointer ptr, deleter_type&& deleter) noexcept // + requires std::conjunction_v< + std::negation>, + std::is_constructible + > + : phantom_{ptr, std::move(deleter)} {} + + constexpr UniquePointer(pointer, std::remove_reference_t&&) noexcept // + requires std::conjunction_v< + std::is_reference, + std::is_constructible> + > // + = delete; + + // ===================================== + // UniquePointer p1{...}; + // UniquePointer p2{p1}; + + constexpr UniquePointer(const UniquePointer&) noexcept = delete; + + constexpr auto operator=(const UniquePointer&) noexcept -> UniquePointer& = delete; + + // ===================================== + // UniquePointer p1{...}; + // UniquePointer p2{std::move(p1)}; + + constexpr UniquePointer(UniquePointer&& other) noexcept // + requires std::conjunction_v< + std::is_move_constructible + > + : phantom_{std::move(other.phantom_)} {} + + constexpr auto operator=(UniquePointer&& other) noexcept -> UniquePointer& // + requires std::conjunction_v< + std::is_move_assignable + > + { + phantom_ = std::move(other.phantom_); + return *this; + } + + template + requires enable_conversion, std::is_same, std::is_convertible>> + constexpr explicit(false) UniquePointer(UniquePointer&& other) noexcept + : phantom_{std::move(other)} {} + + template + requires enable_conversion> + constexpr auto operator=(UniquePointer&& other) noexcept -> UniquePointer& + { + phantom_ = std::move(other).phantom_; + return *this; + } + + constexpr ~UniquePointer() noexcept = default; + + constexpr auto swap(UniquePointer& other) noexcept -> void + { + phantom_.swap(other.phantom_); + } + + constexpr auto release() noexcept -> pointer + { + return phantom_.release(); + } + + constexpr auto reset(pointer ptr = nullptr) noexcept -> void + { + phantom_.reset(ptr); + } + + [[nodiscard]] constexpr auto get_deleter() noexcept -> deleter_type& + { + return phantom_.get_deleter(); + } + + [[nodiscard]] constexpr auto get_deleter() const noexcept -> const deleter_type& + { + return phantom_.get_deleter(); + } + + [[nodiscard]] constexpr auto operator*() noexcept(noexcept(*std::declval())) -> element_type& + { + return phantom_.operator*(); + } + + [[nodiscard]] constexpr auto operator*() const noexcept(noexcept(*std::declval())) -> const element_type& + { + return phantom_.operator*(); + } + + [[nodiscard]] constexpr auto operator->() noexcept -> pointer + { + return phantom_.operator->(); + } + + [[nodiscard]] constexpr auto operator->() const noexcept -> std::add_pointer_t> + { + return phantom_.operator->(); + } + + [[nodiscard]] constexpr auto get() noexcept -> pointer + { + return phantom_.get(); + } + + [[nodiscard]] constexpr auto get() const noexcept -> std::add_pointer_t> + { + return phantom_.get(); + } + + [[nodiscard]] constexpr explicit operator bool() const noexcept + { + return static_cast(phantom_); + } + }; + + /** + * @brief A wrapper for std::unique_ptr, but propagating const. + */ + template + class UniquePointer final + { + template + friend class UniquePointer; + + using phantom_type = std::unique_ptr; + + public: + using pointer = typename phantom_type::pointer; + using element_type = typename phantom_type::element_type; + using deleter_type = typename phantom_type::deleter_type; + + private: + template> + constexpr static auto enable_ctor_reset = + std::disjunction_v< + IsNullptr, + std::is_same, + std::conjunction< + std::is_pointer, + std::is_same, + std::is_convertible(*)[], element_type(*)[]> + > + >; + + template + constexpr static auto enable_conversion = + std::conjunction_v< + std::is_array, + std::is_same, + std::is_same::pointer, typename UniquePointer::element_type*>, + std::is_convertible::element_type(*)[], element_type(*)[]>, + Trait + >; + + phantom_type phantom_; + + public: + // ===================================== + // UniquePointer p{}; + + constexpr UniquePointer() noexcept // + requires unique_ptr_detail::default_constructible_deleter_v + : phantom_{} {} + + // ===================================== + // UniquePointer p{nullptr}; + + constexpr explicit(false) UniquePointer(std::nullptr_t) noexcept // + requires unique_ptr_detail::default_constructible_deleter_v + : phantom_{} {} + + constexpr auto operator=(std::nullptr_t) noexcept -> UniquePointer& + { + phantom_ = nullptr; + return *this; + } + + // ===================================== + // UniquePointer p{new T{...}}; + + template + requires enable_ctor_reset + constexpr explicit UniquePointer(U ptr) noexcept // + requires unique_ptr_detail::default_constructible_deleter_v + : phantom_{ptr} {} + + template + requires enable_ctor_reset + constexpr UniquePointer(U ptr, const deleter_type& deleter) noexcept // + requires std::conjunction_v< + std::is_constructible + > + : phantom_{ptr, deleter} {} + + template + requires enable_ctor_reset + constexpr UniquePointer(U ptr, deleter_type&& deleter) noexcept // + requires std::conjunction_v< + std::negation>, + std::is_constructible + > + : phantom_{ptr, std::move(deleter)} {} + + template + constexpr UniquePointer(U pointer, std::remove_reference_t&&) noexcept // + requires std::conjunction_v< + std::is_reference, + std::is_constructible> + > // + = delete; + + // ===================================== + // UniquePointer p1{...}; + // UniquePointer p2{p1}; + + constexpr UniquePointer(const UniquePointer&) noexcept = delete; + + constexpr auto operator=(const UniquePointer&) noexcept -> UniquePointer& = delete; + + // ===================================== + // UniquePointer p1{...}; + // UniquePointer p2{std::move(p1)}; + + constexpr UniquePointer(UniquePointer&& other) noexcept // + requires std::conjunction_v< + std::is_move_constructible + > + : phantom_{std::move(other.phantom_)} {} + + constexpr auto operator=(UniquePointer&& other) noexcept -> UniquePointer& // + requires std::conjunction_v< + std::is_move_assignable + > + { + phantom_ = std::move(other.phantom_); + return *this; + } + + template + requires enable_conversion, std::is_same, std::is_convertible>> + constexpr explicit(false) UniquePointer(UniquePointer&& other) noexcept + : phantom_{std::move(other)} {} + + template + requires enable_conversion> + constexpr auto operator=(UniquePointer&& other) noexcept -> UniquePointer& + { + phantom_ = std::move(other).phantom_; + return *this; + } + + constexpr ~UniquePointer() noexcept = default; + + constexpr auto swap(UniquePointer& other) noexcept -> void + { + phantom_.swap(other.phantom_); + } + + constexpr auto release() noexcept -> pointer + { + return phantom_.release(); + } + + template + requires enable_ctor_reset + constexpr auto reset(U ptr) noexcept -> void + { + phantom_.reset(ptr); + } + + [[nodiscard]] constexpr auto get_deleter() noexcept -> deleter_type& + { + return phantom_.get_deleter(); + } + + [[nodiscard]] constexpr auto get_deleter() const noexcept -> const deleter_type& + { + return phantom_.get_deleter(); + } + + [[nodiscard]] constexpr auto operator[](const std::size_t index) noexcept -> element_type& + { + return phantom_[index]; + } + + [[nodiscard]] constexpr auto operator[](const std::size_t index) const noexcept -> const element_type& + { + return phantom_[index]; + } + + [[nodiscard]] constexpr auto get() noexcept -> pointer + { + return phantom_.get(); + } + + [[nodiscard]] constexpr auto get() const noexcept -> std::add_pointer_t> + { + return phantom_.get(); + } + + [[nodiscard]] constexpr explicit operator bool() const noexcept + { + return static_cast(phantom_); + } + }; + + template + requires std::conjunction_v< + std::negation>, + std::is_constructible + > + [[nodiscard]] constexpr auto make_unique(Args&&... args) noexcept -> UniquePointer + { + // return UniquePointer{new T{std::forward(args)...}}; + return UniquePointer{new T(std::forward(args)...)}; + } + + template + requires std::conjunction_v< + std::is_array, + std::bool_constant == 0> + > + [[nodiscard]] constexpr auto make_unique(const std::size_t size) noexcept -> UniquePointer + { + using element_type = std::remove_extent_t; + // initialized + return UniquePointer{new element_type[size]{}}; + } + + template + requires std::conjunction_v< + std::bool_constant != 0> + > + [[nodiscard]] constexpr auto make_unique(Args&&...) noexcept -> UniquePointer = delete; + + template + requires std::conjunction_v< + std::negation>, + std::is_default_constructible + > + [[nodiscard]] constexpr auto make_unique_for_overwrite() noexcept -> UniquePointer + { + // unspecified value (for trivial type) + return UniquePointer{new T}; + } + + template + requires std::conjunction_v< + std::is_unbounded_array + > + [[nodiscard]] constexpr auto make_unique_for_overwrite(const std::size_t size) noexcept -> UniquePointer + { + using element_type = std::remove_extent_t; + // unspecified value (for trivial type) + return UniquePointer{new element_type[size]}; + } + + template + requires std::conjunction_v< + std::is_bounded_array + > + [[nodiscard]] constexpr auto make_unique_for_overwrite(Args&&...) noexcept = delete; + + template + requires std::conjunction_v< + std::is_swappable + > + constexpr auto swap(UniquePointer& lhs, UniquePointer& rhs) noexcept -> void + { + lhs.swap(rhs); + } + + template + [[nodiscard]] constexpr auto operator==(const UniquePointer& lhs, const UniquePointer& rhs) noexcept -> bool + { + return lhs.get() == rhs.get(); + } + + template + [[nodiscard]] constexpr auto operator<(const UniquePointer& lhs, const UniquePointer& rhs) noexcept -> bool + { + using p1 = typename UniquePointer::pointer; + using p2 = typename UniquePointer::pointer; + using common = std::common_type_t; + return std::less{}(lhs.get(), rhs.get()); + } + + template + [[nodiscard]] constexpr auto operator<=(const UniquePointer& lhs, const UniquePointer& rhs) noexcept -> bool + { + return not(rhs < lhs); + } + + template + [[nodiscard]] constexpr auto operator>(const UniquePointer& lhs, const UniquePointer& rhs) noexcept -> bool + { + return rhs < lhs; + } + + template + [[nodiscard]] constexpr auto operator>=(const UniquePointer& lhs, const UniquePointer& rhs) noexcept -> bool + { + return not(lhs < rhs); + } + + template + requires std::three_way_comparable_with< + typename UniquePointer::pointer, + typename UniquePointer::pointer + > + [[nodiscard]] constexpr auto operator<=>( + const UniquePointer& lhs, + const UniquePointer& rhs + ) noexcept -> std::compare_three_way_result_t::pointer, typename UniquePointer::pointer> + { + return lhs.get() <=> rhs.get(); + } + + template + [[nodiscard]] constexpr auto operator==(const UniquePointer& lhs, std::nullptr_t) noexcept -> bool + { + return not lhs; + } + + template + [[nodiscard]] constexpr auto operator<(const UniquePointer& lhs, std::nullptr_t) noexcept -> bool + { + using p = typename UniquePointer::pointer; + return std::less

{}(lhs.get(), nullptr); + } + + template + [[nodiscard]] constexpr auto operator<(std::nullptr_t, const UniquePointer& rhs) noexcept -> bool + { + using p = typename UniquePointer::pointer; + return std::less

{}(nullptr, rhs.get()); + } + + template + [[nodiscard]] constexpr auto operator<=(const UniquePointer& lhs, std::nullptr_t) noexcept -> bool + { + return not(nullptr < lhs); + } + + template + [[nodiscard]] constexpr auto operator<=(std::nullptr_t, const UniquePointer& rhs) noexcept -> bool + { + return not(rhs < nullptr); + } + + template + [[nodiscard]] constexpr auto operator>(const UniquePointer& lhs, std::nullptr_t) noexcept -> bool + { + return nullptr < lhs; + } + + template + constexpr auto operator>(std::nullptr_t, const UniquePointer& rhs) noexcept -> bool + { + return rhs < nullptr; + } + + template + [[nodiscard]] constexpr auto operator>=(const UniquePointer& lhs, std::nullptr_t) noexcept -> bool + { + return not(lhs < nullptr); + } + + template + [[nodiscard]] constexpr auto operator>=(std::nullptr_t, const UniquePointer& rhs) noexcept -> bool + { + return not(nullptr < rhs); + } + + template + requires std::three_way_comparable::pointer> + [[nodiscard]] constexpr auto operator<=>( + const UniquePointer& lhs, + std::nullptr_t // + ) noexcept -> std::compare_three_way_result_t::pointer> + { + return lhs.get() <=> static_cast::pointer>(nullptr); + } + + template + constexpr auto operator<<(std::basic_ostream& out, const UniquePointer& pointer) noexcept -> std::basic_ostream // + requires requires + { + out << pointer.get(); + } + { + out << pointer.get(); + return out; + } +} + +namespace std {} diff --git a/src/meta/member.hpp b/src/meta/member.hpp index 653ef79e..73ef40e1 100644 --- a/src/meta/member.hpp +++ b/src/meta/member.hpp @@ -211,7 +211,10 @@ namespace gal::prometheus::meta } template - requires (known_member_t> and N < member_size>()) + requires ( + known_member_t> and + N < member_size>() + ) [[nodiscard]] constexpr auto member_of_index(T&& object) noexcept -> decltype(auto) { using bare_type = std::remove_cvref_t; @@ -249,7 +252,10 @@ namespace gal::prometheus::meta namespace member_detail { template - requires (member_detail::known_member_t> and N < member_size>()) + requires ( + member_detail::known_member_t> and + N < member_size>() + ) struct member_type_of_index { using type = std::decay_t(std::declval()))>; @@ -260,9 +266,30 @@ namespace gal::prometheus::meta using member_type_of_index = typename member_detail::member_type_of_index::type; template - requires (known_member_t> and N < member_size>()) + requires ( + known_member_t> and + N < member_size>() + ) [[nodiscard]] constexpr auto name_of_member() noexcept -> std::string_view { + // note: + // If you encounter compile errors here, such as: + // "No overloaded function matching `get_full_function_name` was found" + // "The result of `member_detail::visit` was not a (compile time) constant" + // then you should check your class definition about structured bindings at this point: + // class YourClass + // { + // public: + // template + // [[nodiscard]] constexpr auto get() const noexcept -> const value_type& // <-- see this + // + // template + // requires(Index < 2) + // [[nodiscard]] constexpr auto get() noexcept -> value_type&; + // }; + // + // Even if your value_type is a POD type, or can return a value directly (perhaps at less cost than a reference), it must be a constant reference here! + // constexpr auto full_function_name = get_full_function_name< member_detail::visit( [](Ts&&... args) noexcept -> auto // @@ -343,7 +370,8 @@ namespace gal::prometheus::meta { const auto f = []() noexcept { - if constexpr (name_of_member() == Name) + if constexpr (constexpr auto member_name = meta::name_of_member(); + member_name == Name.template as()) { return I; } @@ -361,7 +389,7 @@ namespace gal::prometheus::meta GAL_PROMETHEUS_COMPILER_DISABLE_WARNING_PUSH GAL_PROMETHEUS_COMPILER_DISABLE_CLANG_WARNING(-Wunused-value) - (((index = f.template operator()()) == member_index_unknown) and ...); + std::ignore = (((index = f.template operator()()) == member_index_unknown) and ...); GAL_PROMETHEUS_COMPILER_DISABLE_WARNING_POP @@ -376,7 +404,8 @@ namespace gal::prometheus::meta { const auto f = [name]() noexcept { - if (name_of_member() == name) + if (constexpr auto member_name = meta::name_of_member(); + member_name == name) { return I; } @@ -392,7 +421,7 @@ namespace gal::prometheus::meta GAL_PROMETHEUS_COMPILER_DISABLE_WARNING_PUSH GAL_PROMETHEUS_COMPILER_DISABLE_CLANG_WARNING(-Wunused-value) - (((index = f.template operator()()) == member_index_unknown) and ...); + std::ignore = (((index = f.template operator()()) == member_index_unknown) and ...); GAL_PROMETHEUS_COMPILER_DISABLE_WARNING_POP @@ -429,6 +458,81 @@ namespace gal::prometheus::meta return member_index(name) != member_index_unknown; } + namespace member_detail + { + template + [[nodiscard]] constexpr auto member_of_name(T&& object) noexcept -> decltype(auto) + { + constexpr auto begin = std::ranges::begin(Name); + constexpr auto end = std::ranges::end(Name); + constexpr auto size = std::ranges::size(Name); + + if constexpr (begin == end) + { + GAL_PROMETHEUS_SEMANTIC_STATIC_UNREACHABLE(); + } + else + { + constexpr std::string_view sub{begin, end}; + + constexpr auto sub_ref_index = sub.find_first_of('.'); + constexpr auto sub_ptr_index = sub.find_first_of("->"); + + if constexpr (sub_ref_index == std::string_view::npos and sub_ptr_index == std::string_view::npos) + { + constexpr auto index = meta::member_index(); + + if constexpr (index == member_index_unknown) + { + GAL_PROMETHEUS_SEMANTIC_STATIC_UNREACHABLE("If you see this message, you've passed in the wrong member name."); + } + else + { + return meta::member_of_index(std::forward(object)); + } + } + else + { + const auto make_sub_name = [](const auto it) noexcept + { + basic_fixed_string r{it, it + N}; + r[N] = '\0'; + return r; + }; + + if constexpr (sub_ref_index != std::string_view::npos) + { + constexpr auto name = make_sub_name.template operator()(begin); + auto&& ref = member_detail::member_of_name(std::forward(object)); + + // "." + constexpr auto next = make_sub_name.template operator()(begin + sub_ref_index + 1); + return member_detail::member_of_name(std::forward(ref)); + } + else if constexpr (sub_ptr_index != std::string_view::npos) + { + constexpr auto name = make_sub_name.template operator()(begin); + auto&& ptr = member_detail::member_of_name(std::forward(object)); + + // "->" + constexpr auto next = make_sub_name.template operator()(begin + sub_ptr_index + 2); + return member_detail::member_of_name(*std::forward(ptr)); + } + else + { + GAL_PROMETHEUS_SEMANTIC_STATIC_UNREACHABLE(); + } + } + } + } + } + + template + [[nodiscard]] constexpr auto member_of_name(T&& object) noexcept -> decltype(auto) + { + return member_detail::member_of_name(std::forward(object)); + } + namespace member_detail { enum class FoldCategory : std::uint8_t diff --git a/src/meta/string.hpp b/src/meta/string.hpp index 40a3264a..3a3ed419 100644 --- a/src/meta/string.hpp +++ b/src/meta/string.hpp @@ -312,19 +312,19 @@ namespace gal::prometheus::meta // ================================================= template - requires(is_constructible_from_data_v) - [[nodiscard]] constexpr explicit operator StringType() const noexcept // + requires(is_constructible_from_view_v) + [[nodiscard]] constexpr explicit(not std::is_convertible_v, StringType>) operator StringType() const noexcept // requires(not lazy_is_static()) { - return StringType{rep_value(), rep_size()}; + return StringType{basic_string_view{rep_value(), rep_size()}}; } template - requires(is_constructible_from_view_v and not is_constructible_from_data_v) - [[nodiscard]] constexpr explicit(not std::is_convertible_v, StringType>) operator StringType() const noexcept // - requires(not lazy_is_static()) + requires(is_constructible_from_data_v) + [[nodiscard]] constexpr explicit operator StringType() const noexcept // + requires(not is_constructible_from_view_v and not lazy_is_static()) { - return StringType{basic_string_view{rep_value(), rep_size()}}; + return StringType{rep_value(), rep_size()}; } template @@ -687,13 +687,20 @@ namespace gal::prometheus::meta std::ranges::copy(std::ranges::begin(char_array), std::ranges::begin(char_array) + N, value); } - template + template requires std::is_same_v, value_type> constexpr explicit basic_fixed_string(const String& string) noexcept { std::ranges::copy(std::ranges::begin(string), std::ranges::begin(string) + N, value); } + template + requires std::is_same_v, value_type> + constexpr explicit basic_fixed_string(const Iterator begin, const Iterator end) noexcept + { + std::ranges::copy(begin, end, value); + } + [[nodiscard]] constexpr auto begin() noexcept -> pointer { return value; } [[nodiscard]] constexpr auto begin() const noexcept -> const_pointer { return value; } @@ -702,6 +709,10 @@ namespace gal::prometheus::meta [[nodiscard]] constexpr auto end() const noexcept -> const_pointer { return value + size; } + [[nodiscard]] constexpr auto operator[](const size_type index) noexcept -> value_type& { return value[index]; } + + [[nodiscard]] constexpr auto operator[](const size_type index) const noexcept -> value_type { return value[index]; } + // // basic_fixed_string <=> basic_fixed_string // template // [[nodiscard]] constexpr auto operator<=>(const basic_fixed_string& rhs) const noexcept -> auto diff --git a/src/primitive/circle.hpp b/src/primitive/circle.hpp index b09c6ee7..bfd5c92b 100644 --- a/src/primitive/circle.hpp +++ b/src/primitive/circle.hpp @@ -16,7 +16,7 @@ #include #include -#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE +// #include GAL_PROMETHEUS_ERROR_DEBUG_MODULE namespace gal::prometheus { diff --git a/src/primitive/ellipse.hpp b/src/primitive/ellipse.hpp index baaa698a..1d78d0c8 100644 --- a/src/primitive/ellipse.hpp +++ b/src/primitive/ellipse.hpp @@ -19,7 +19,7 @@ #include -#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE +// #include GAL_PROMETHEUS_ERROR_DEBUG_MODULE namespace gal::prometheus { diff --git a/src/primitive/extent.hpp b/src/primitive/extent.hpp index ee0b4108..081261b8 100644 --- a/src/primitive/extent.hpp +++ b/src/primitive/extent.hpp @@ -13,7 +13,7 @@ #include -#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE +// #include GAL_PROMETHEUS_ERROR_DEBUG_MODULE namespace gal::prometheus { @@ -49,7 +49,7 @@ namespace gal::prometheus template requires(Index < 2) - [[nodiscard]] constexpr auto get() const noexcept -> value_type + [[nodiscard]] constexpr auto get() const noexcept -> const value_type& { if constexpr (Index == 0) { return width; } else if constexpr (Index == 1) { return height; } @@ -69,6 +69,42 @@ namespace gal::prometheus { return {.width = width, .height = height, .depth = value_type{0}}; } + + template U = value_type> + [[nodiscard]] constexpr auto combine_max(const basic_extent<2, U>& other) const noexcept -> basic_extent + { + return + { + std::ranges::max(width, other.width), + std::ranges::max(height, other.height) + }; + } + + template U = value_type> + [[nodiscard]] constexpr auto combine_min(const basic_extent<2, U>& other) const noexcept -> basic_extent + { + return + { + std::ranges::min(width, other.width), + std::ranges::min(height, other.height) + }; + } + + template Low = value_type, std::convertible_to High = value_type> + [[nodiscard]] constexpr auto clamp( + const basic_extent<2, Low>& low, + const basic_extent<2, High>& high + ) const noexcept -> basic_extent + { + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(low.width < high.width); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(low.height < high.height); + + return + { + std::ranges::clamp(width, low.width, high.width), + std::ranges::clamp(height, low.height, high.height) + }; + } }; template @@ -102,7 +138,7 @@ namespace gal::prometheus template requires(Index < 3) - [[nodiscard]] constexpr auto get() const noexcept -> value_type + [[nodiscard]] constexpr auto get() const noexcept -> const value_type& { if constexpr (Index == 0) { return width; } else if constexpr (Index == 1) { return height; } @@ -124,6 +160,46 @@ namespace gal::prometheus { return {.width = width, .height = height}; } + + template U = value_type> + [[nodiscard]] constexpr auto combine_max(const basic_extent<3, U>& other) const noexcept -> basic_extent + { + return + { + std::ranges::max(width, other.width), + std::ranges::max(height, other.height), + std::ranges::max(depth, other.depth) + }; + } + + template U = value_type> + [[nodiscard]] constexpr auto combine_min(const basic_extent<3, U>& other) const noexcept -> basic_extent + { + return + { + std::ranges::min(width, other.width), + std::ranges::min(height, other.height), + std::ranges::min(depth, other.depth) + }; + } + + template Low = value_type, std::convertible_to High = value_type> + [[nodiscard]] constexpr auto clamp( + const basic_extent<3, Low>& low, + const basic_extent<3, High>& high + ) const noexcept -> basic_extent + { + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(low.width < high.width); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(low.height < high.height); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(low.depth < high.depth); + + return + { + std::ranges::clamp(width, low.width, high.width), + std::ranges::clamp(height, low.height, high.height), + std::ranges::clamp(depth, low.depth, high.depth) + }; + } }; template diff --git a/src/primitive/point.hpp b/src/primitive/point.hpp index b54daa76..f309e35c 100644 --- a/src/primitive/point.hpp +++ b/src/primitive/point.hpp @@ -14,7 +14,7 @@ #include #include -#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE +// #include GAL_PROMETHEUS_ERROR_DEBUG_MODULE namespace gal::prometheus { @@ -50,7 +50,7 @@ namespace gal::prometheus template requires(Index < 2) - [[nodiscard]] constexpr auto get() const noexcept -> value_type + [[nodiscard]] constexpr auto get() const noexcept -> const value_type& { if constexpr (Index == 0) { return x; } else if constexpr (Index == 1) { return y; } @@ -77,40 +77,40 @@ namespace gal::prometheus return math::hypot(x - static_cast(other.x), y - static_cast(other.y)); } - template Low = value_type, std::convertible_to High = value_type> - [[nodiscard]] constexpr auto clamp( - const basic_point<2, Low>& low, - const basic_point<2, High>& high - ) noexcept -> basic_point& + template U = value_type> + [[nodiscard]] constexpr auto combine_max(const basic_point<2, U>& other) const noexcept -> basic_point { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(low.x < high.x); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(low.y < high.y); - - GAL_PROMETHEUS_SEMANTIC_IF_CONSTANT_EVALUATED - { - x = std::ranges::min(std::ranges::max(x, static_cast(low.x)), static_cast(high.x)); - y = std::ranges::min(std::ranges::max(y, static_cast(low.y)), static_cast(high.y)); - } - else + return { - x = std::ranges::clamp(x, low.x, high.x); - y = std::ranges::clamp(y, low.y, high.y); - } + std::ranges::max(x, other.x), + std::ranges::max(y, other.y) + }; + } - return *this; + template U = value_type> + [[nodiscard]] constexpr auto combine_min(const basic_point<2, U>& other) const noexcept -> basic_point + { + return + { + std::ranges::min(x, other.x), + std::ranges::min(y, other.y) + }; } template Low = value_type, std::convertible_to High = value_type> - [[nodiscard]] friend constexpr auto clamp( - const basic_point& point, + [[nodiscard]] constexpr auto clamp( const basic_point<2, Low>& low, const basic_point<2, High>& high - ) noexcept -> basic_point + ) const noexcept -> basic_point { - auto result{point}; + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(low.x < high.x); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(low.y < high.y); - result.clamp(low, high); - return result; + return + { + std::ranges::clamp(x, low.x, high.x), + std::ranges::clamp(y, low.y, high.y) + }; } template T1 = value_type, std::convertible_to T2 = value_type> @@ -122,13 +122,13 @@ namespace gal::prometheus { if constexpr (Index == 0) { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(static_cast(p1.x) < static_cast(p2.x)); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(static_cast(p1.x) < static_cast(p2.x)); return x >= static_cast(p1.x) and x < static_cast(p2.x); } else if constexpr (Index == 1) { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(static_cast(p1.y) < static_cast(p2.y)); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(static_cast(p1.y) < static_cast(p2.y)); return y >= static_cast(p1.y) and y < static_cast(p2.y); } @@ -178,7 +178,7 @@ namespace gal::prometheus template requires(Index < 3) - [[nodiscard]] constexpr auto get() const noexcept -> value_type + [[nodiscard]] constexpr auto get() const noexcept -> const value_type& { if constexpr (Index == 0) { return x; } else if constexpr (Index == 1) { return y; } @@ -207,43 +207,44 @@ namespace gal::prometheus return math::hypot(x - static_cast(other.x), y - static_cast(other.y), z - static_cast(other.z)); } - template Low = value_type, std::convertible_to High = value_type> - [[nodiscard]] constexpr auto clamp( - const basic_point<3, Low>& low, - const basic_point<3, High>& high - ) noexcept -> basic_point& + template U = value_type> + [[nodiscard]] constexpr auto combine_max(const basic_point<3, U>& other) const noexcept -> basic_point { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(low.x < high.x); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(low.y < high.y); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(low.z < high.z); - - GAL_PROMETHEUS_SEMANTIC_IF_CONSTANT_EVALUATED - { - x = std::ranges::min(std::ranges::max(x, static_cast(low.x)), static_cast(high.x)); - y = std::ranges::min(std::ranges::max(y, static_cast(low.y)), static_cast(high.y)); - z = std::ranges::min(std::ranges::max(z, static_cast(low.z)), static_cast(high.z)); - } - else + return { - x = std::ranges::clamp(x, low.x, high.x); - y = std::ranges::clamp(y, low.y, high.y); - z = std::ranges::clamp(z, low.z, high.z); - } + std::ranges::max(x, other.x), + std::ranges::max(y, other.y), + std::ranges::max(z, other.z) + }; + } - return *this; + template U = value_type> + [[nodiscard]] constexpr auto combine_min(const basic_point<3, U>& other) const noexcept -> basic_point + { + return + { + std::ranges::min(x, other.x), + std::ranges::min(y, other.y), + std::ranges::min(z, other.z) + }; } template Low = value_type, std::convertible_to High = value_type> - [[nodiscard]] friend constexpr auto clamp( - const basic_point& point, + [[nodiscard]] constexpr auto clamp( const basic_point<3, Low>& low, const basic_point<3, High>& high - ) noexcept -> basic_point + ) const noexcept -> basic_point { - auto result{point}; + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(low.x < high.x); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(low.y < high.y); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(low.z < high.z); - result.clamp(low, high); - return result; + return + { + std::ranges::clamp(x, low.x, high.x), + std::ranges::clamp(y, low.y, high.y), + std::ranges::clamp(z, low.z, high.z) + }; } template T1 = value_type, std::convertible_to T2 = value_type> @@ -255,19 +256,19 @@ namespace gal::prometheus { if constexpr (Index == 0) { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(static_cast(p1.x) < static_cast(p2.x)); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(static_cast(p1.x) < static_cast(p2.x)); return x >= static_cast(p1.x) and x < static_cast(p2.x); } else if constexpr (Index == 1) { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(static_cast(p1.y) < static_cast(p2.y)); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(static_cast(p1.y) < static_cast(p2.y)); return y >= static_cast(p1.y) and y < static_cast(p2.y); } else if constexpr (Index == 2) { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(static_cast(p1.z) < static_cast(p2.z)); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(static_cast(p1.z) < static_cast(p2.z)); return z >= static_cast(p1.z) and z < static_cast(p2.z); } diff --git a/src/primitive/rect.hpp b/src/primitive/rect.hpp index 0bd3ce7c..35c3ab8f 100644 --- a/src/primitive/rect.hpp +++ b/src/primitive/rect.hpp @@ -15,7 +15,7 @@ #include #include -#include GAL_PROMETHEUS_ERROR_DEBUG_MODULE +// #include GAL_PROMETHEUS_ERROR_DEBUG_MODULE namespace gal::prometheus { @@ -162,16 +162,16 @@ namespace gal::prometheus [[nodiscard]] constexpr auto includes(const point_type& p) const noexcept -> bool { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not empty() and valid()); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not empty() and valid()); return p.between(left_top(), right_bottom()); } [[nodiscard]] constexpr auto includes(const basic_rect& rect) const noexcept -> bool { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not empty() and valid()); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not rect.empty() and rect.valid()); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(size().exact_greater_than(rect.size())); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not empty() and valid()); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not rect.empty() and rect.valid()); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(width() > rect.width() and height() > rect.height()); return rect.point.x >= point.x and @@ -182,8 +182,8 @@ namespace gal::prometheus [[nodiscard]] constexpr auto intersects(const basic_rect& rect) const noexcept -> bool { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not empty() and valid()); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not rect.empty() and rect.valid()); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not empty() and valid()); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not rect.empty() and rect.valid()); return not( rect.point.x >= point.x + width() or @@ -197,10 +197,8 @@ namespace gal::prometheus { return { - std::ranges::min(point.x, rect.point.x), - std::ranges::min(point.y, rect.point.y), - std::ranges::max(point.x + width(), rect.point.x + rect.width()), - std::ranges::max(point.y + height(), rect.point.y + rect.height()) + left_top().combine_min(rect.left_top()), + right_bottom().combine_max(rect.right_bottom()) }; } @@ -208,10 +206,8 @@ namespace gal::prometheus { return { - std::ranges::max(point.x, rect.point.x), - std::ranges::max(point.y, rect.point.y), - std::ranges::min(point.x + width(), rect.point.x + rect.width()), - std::ranges::min(point.y + height(), rect.point.y + rect.height()) + left_top().combine_max(rect.left_top()), + right_bottom().combine_min(rect.right_bottom()) }; } }; @@ -384,16 +380,16 @@ namespace gal::prometheus [[nodiscard]] constexpr auto includes(const point_type& p) const noexcept -> bool { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not empty() and valid()); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not empty() and valid()); return p.between(left_top_near(), right_bottom_near()); } [[nodiscard]] constexpr auto includes(const basic_rect& rect) const noexcept -> bool { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not empty() and valid()); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not rect.empty() and rect.valid()); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(size().exact_greater_than(rect.size())); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not empty() and valid()); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not rect.empty() and rect.valid()); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(width() > rect.width() and height() > rect.height() and depth() > rect.depth()); return rect.point.x >= point.x and @@ -406,8 +402,8 @@ namespace gal::prometheus [[nodiscard]] constexpr auto intersects(const basic_rect& rect) const noexcept -> bool { - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not empty() and valid()); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not rect.empty() and rect.valid()); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not empty() and valid()); + // GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(not rect.empty() and rect.valid()); return not( rect.point.x >= point.x + width() or @@ -423,12 +419,8 @@ namespace gal::prometheus { return { - std::ranges::min(point.x, rect.point.x), - std::ranges::min(point.y, rect.point.y), - std::ranges::min(point.z, rect.point.z), - std::ranges::max(point.x + width(), rect.point.x + rect.width()), - std::ranges::max(point.y + height(), rect.point.y + rect.height()), - std::ranges::max(point.z + depth(), rect.point.z + rect.depth()) + point.combine_min(rect.point), + extent.combine_max(rect.extent) }; } @@ -436,12 +428,8 @@ namespace gal::prometheus { return { - std::ranges::max(point.x, rect.point.x), - std::ranges::max(point.y, rect.point.y), - std::ranges::max(point.z, rect.point.z), - std::ranges::min(point.x + width(), rect.point.x + rect.width()), - std::ranges::min(point.y + height(), rect.point.y + rect.height()), - std::ranges::min(point.z + depth(), rect.point.z + rect.depth()) + point.combine_max(rect.point), + extent.combine_min(rect.extent) }; } }; diff --git a/src/prometheus/macro.hpp b/src/prometheus/macro.hpp index d4b8f5c4..c207b8b0 100644 --- a/src/prometheus/macro.hpp +++ b/src/prometheus/macro.hpp @@ -482,17 +482,3 @@ #define GAL_PROMETHEUS_ERROR_DEBUG_ASSUME GAL_PROMETHEUS_ERROR_ASSUME #endif - -// ========================================================= -// MODULE: gal.prometheus.draw -// ========================================================= - - - -#if defined(DEBUG) or defined(_DEBUG) - -#if not defined(GAL_PROMETHEUS_DRAW_CONTEXT_DEBUG) -#define GAL_PROMETHEUS_DRAW_CONTEXT_DEBUG -#endif - -#endif diff --git a/unit_test/CMakeLists.txt b/unit_test/CMakeLists.txt index df27a1e8..7b2e3f42 100644 --- a/unit_test/CMakeLists.txt +++ b/unit_test/CMakeLists.txt @@ -76,5 +76,5 @@ add_test( # assets path set(ASSETS_PATH_PIC ${CMAKE_SOURCE_DIR}/assets/pic.jpg) -# DRAW -add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/draw) +# GUI +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/gui) diff --git a/unit_test/src/draw/vulkan/main.cpp b/unit_test/src/draw/vulkan/main.cpp deleted file mode 100644 index 87ef6ed7..00000000 --- a/unit_test/src/draw/vulkan/main.cpp +++ /dev/null @@ -1,2222 +0,0 @@ -#if GAL_PROMETHEUS_USE_MODULE -import gal.prometheus; -#else -#include -#endif - -#include -#include -#include -#include - -#include "font.hpp" - -#include -#include - -namespace -{ - using namespace gal::prometheus; - - using point_type = primitive::basic_point; - using rect_type = primitive::basic_rect; - using vertex_type = primitive::basic_vertex; - using vertex_index_type = std::uint16_t; - - using vertex_list_type = primitive::basic_vertex_list; - // todo - using vertex_index_list_type = std::vector; - - struct draw_list_type - { - vertex_list_type vertex_list; - vertex_index_list_type index_list; - }; - - struct draw_data_type - { - rect_type display_rect; - - std::vector draw_lists; - - [[nodiscard]] constexpr auto total_vertex_size() const noexcept -> std::size_t - { - return std::ranges::fold_left( - draw_lists, - static_cast(0), - [](const auto s, const auto& c) noexcept -> std::size_t { return s + c.vertex_list.size(); } - ); - } - - [[nodiscard]] constexpr auto total_index_size() const noexcept -> std::size_t - { - return std::ranges::fold_left( - draw_lists, - static_cast(0), - [](const auto s, const auto& c) noexcept -> std::size_t { return s + c.index_list.size(); } - ); - } - }; - - draw_data_type g_draw_data; - - namespace detail - { - struct frame_type - { - VkCommandPool command_pool; - VkCommandBuffer command_buffer; - VkFence fence; - VkImage back_buffer; - VkImageView back_buffer_view; - VkFramebuffer frame_buffer; - }; - - struct frame_semaphore_type - { - VkSemaphore image_acquired_semaphore; - VkSemaphore render_complete_semaphore; - }; - - auto destroy_frame(const VkDevice device, frame_type& frame, const VkAllocationCallbacks* allocation_callbacks) -> void - { - vkDestroyFence(device, frame.fence, allocation_callbacks); - vkFreeCommandBuffers(device, frame.command_pool, 1, &frame.command_buffer); - vkDestroyCommandPool(device, frame.command_pool, allocation_callbacks); - frame.fence = VK_NULL_HANDLE; - frame.command_buffer = VK_NULL_HANDLE; - frame.command_pool = VK_NULL_HANDLE; - - // vkDestroyImage(device, frame.back_buffer, &allocation_callbacks); - vkDestroyImageView(device, frame.back_buffer_view, allocation_callbacks); - vkDestroyFramebuffer(device, frame.frame_buffer, allocation_callbacks); - frame.back_buffer = VK_NULL_HANDLE; - frame.back_buffer_view = VK_NULL_HANDLE; - frame.frame_buffer = VK_NULL_HANDLE; - } - - auto destroy_frame_semaphore( - const VkDevice device, - frame_semaphore_type& frame_semaphore, - const VkAllocationCallbacks* allocation_callbacks - ) -> void - { - vkDestroySemaphore(device, frame_semaphore.image_acquired_semaphore, allocation_callbacks); - vkDestroySemaphore(device, frame_semaphore.render_complete_semaphore, allocation_callbacks); - frame_semaphore.image_acquired_semaphore = VK_NULL_HANDLE; - frame_semaphore.render_complete_semaphore = VK_NULL_HANDLE; - } - - struct frame_render_buffer_type - { - VkDeviceMemory vertex_buffer_memory{VK_NULL_HANDLE}; - VkDeviceSize vertex_count{0}; - VkBuffer vertex_buffer{VK_NULL_HANDLE}; - - VkDeviceMemory index_buffer_memory{VK_NULL_HANDLE}; - VkDeviceSize index_count{0}; - VkBuffer index_buffer{VK_NULL_HANDLE}; - }; - } - - GLFWwindow* g_window; - int g_window_width = 1280; - int g_window_height = 720; - int g_window_frame_buffer_width; - int g_window_frame_buffer_height; - - VkAllocationCallbacks* g_allocation_callbacks; - - VkSurfaceKHR g_window_surface; - VkSurfaceFormatKHR g_window_surface_format; - VkPresentModeKHR g_window_present_mode; - // assert(g_window_min_image_count >= 2) - std::uint32_t g_window_min_image_count{2}; - VkSwapchainKHR g_window_swap_chain; - bool g_window_swap_chain_rebuild_required; - - bool g_window_clear_enable{true}; - VkClearValue g_window_clear_value{.45f, .55f, .65f, 1.f}; - - std::unique_ptr g_window_frames; - std::uint32_t g_window_frame_current_index; - std::uint32_t g_window_frame_total_count; - constexpr auto g_window_frames_maker = [] - { - g_window_frames = std::make_unique(g_window_frame_total_count); - }; - constexpr auto g_window_frames_destroyer = [](const VkDevice device, const VkAllocationCallbacks* allocation_callbacks) - { - std::ranges::for_each( - std::ranges::subrange{g_window_frames.get(), g_window_frames.get() + g_window_frame_total_count}, - [&](detail::frame_type& frame) { destroy_frame(device, frame, allocation_callbacks); } - ); - - g_window_frames.reset(); - g_window_frame_current_index = 0; - g_window_frame_total_count = 0; - }; - - std::unique_ptr g_window_frame_semaphores; - std::uint32_t g_window_frame_semaphore_current_index; - std::uint32_t g_window_frame_semaphore_total_count; - constexpr auto g_window_frame_semaphores_maker = [] - { - g_window_frame_semaphores = std::make_unique(g_window_frame_semaphore_total_count); - }; - constexpr auto g_window_frame_semaphores_destroyer = [](const VkDevice device, const VkAllocationCallbacks* allocation_callbacks) - { - std::ranges::for_each( - std::ranges::subrange{g_window_frame_semaphores.get(), g_window_frame_semaphores.get() + g_window_frame_semaphore_total_count}, - [&](detail::frame_semaphore_type& frame_semaphore) { destroy_frame_semaphore(device, frame_semaphore, allocation_callbacks); } - ); - - g_window_frame_semaphores.reset(); - g_window_frame_semaphore_current_index = 0; - g_window_frame_semaphore_total_count = 0; - }; - - std::unique_ptr g_window_render_buffer; - std::uint32_t g_window_render_buffer_current_index; - std::uint32_t g_window_render_buffer_total_count; - constexpr auto g_window_render_buffer_maker = [] - { - g_window_render_buffer = std::make_unique(g_window_render_buffer_total_count); - }; - constexpr auto g_window_render_buffer_destroyer = [](const VkDevice device, const VkAllocationCallbacks* allocation_callbacks) - { - std::ranges::for_each( - std::ranges::subrange{g_window_render_buffer.get(), g_window_render_buffer.get() + g_window_render_buffer_total_count}, - [&](const detail::frame_render_buffer_type& frame) -> void - { - if (frame.vertex_buffer) - { - vkDestroyBuffer(device, frame.vertex_buffer, allocation_callbacks); - } - if (frame.vertex_buffer_memory) - { - vkFreeMemory(device, frame.vertex_buffer_memory, allocation_callbacks); - } - - if (frame.index_buffer) - { - vkDestroyBuffer(device, frame.index_buffer, allocation_callbacks); - } - if (frame.index_buffer_memory) - { - vkFreeMemory(device, frame.index_buffer_memory, allocation_callbacks); - } - } - ); - g_window_render_buffer.reset(); - g_window_render_buffer_current_index = 0; - g_window_render_buffer_total_count = 0; - }; - - VkInstance g_instance; - VkDebugReportCallbackEXT g_debug_report_callback; - - VkPhysicalDevice g_physical_device; - VkDevice g_device; - - std::uint32_t g_queue_family; - VkQueue g_queue; - - VkDescriptorPool g_descriptor_pool; - - VkSampler g_font_sampler; - VkDeviceMemory g_font_memory; - VkImage g_font_image; - VkImageView g_font_image_view; - VkDescriptorSet g_font_descriptor_set; - VkCommandPool g_font_command_pool; - VkCommandBuffer g_font_command_buffer; - - VkDescriptorSetLayout g_pipeline_descriptor_set_layout; - VkPipelineLayout g_pipeline_layout; - VkShaderModule g_pipeline_shader_module_vertex; - VkShaderModule g_pipeline_shader_module_fragment; - VkSampleCountFlagBits g_pipeline_rasterization_msaa{VK_SAMPLE_COUNT_1_BIT}; - VkPipelineCreateFlags g_pipeline_create_flags; - VkRenderPass g_pipeline_render_pass; - VkPipeline g_pipeline; - // optional - VkPipelineCache g_pipeline_cache; - std::uint32_t g_pipeline_sub_pass; - bool g_pipeline_use_dynamic_rendering; - VkPipelineRenderingCreateInfoKHR g_pipeline_rendering_create_info; - - auto glfw_error_callback(const int error, const char* message) -> void // - { - std::println(std::cerr, "GLFW Error {}: {}", error, message); - } - - auto vulkan_check_error(const VkResult result, const std::source_location& location = std::source_location::current()) -> void - { - if (result == VK_SUCCESS) - { - return; - } - - std::println(std::cerr, "VULKAN Error: {} -- at {}:{}\n", meta::name_of(result), location.file_name(), location.line()); - std::abort(); - } - - auto VKAPI_CALL vulkan_debug_report( - const VkDebugReportFlagsEXT flags, - const VkDebugReportObjectTypeEXT object_type, - const std::uint64_t object, - const std::size_t location, - const std::int32_t message_code, - const char* layer_prefix, - const char* message, - [[maybe_unused]] void* user_data - ) -> VkBool32 - { - std::println( - std::cerr, - "Vulkan debug report: \n\t flags({}) \n\t object_type({}) \n\t object(0x{:x}) \n\t location({}) \n\t message_code({}) \n\t " - "layer_prefix({}) \n\t message({})\n", - flags, - meta::name_of(object_type), - object, - location, - message_code, - layer_prefix, - message - ); - return VK_FALSE; - } - - auto vulkan_setup(std::vector& extensions) -> void; - - auto vulkan_create_or_resize_window() -> void; - - auto vulkan_setup_window() -> bool; - - auto init() -> void; - - auto new_frame() -> void; - - auto frame_render() -> void; - - auto frame_present() -> void; - - auto shutdown() -> void; - - auto vulkan_cleanup_window() -> void; - - auto vulkan_cleanup() -> void; -} - -int main(int, char**) -{ - glfwSetErrorCallback(glfw_error_callback); - if (not glfwInit()) - { - std::println(std::cerr, "GLFW: glfwInit failed"); - return -1; - } - - // Create window with Vulkan context - glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); - g_window = glfwCreateWindow(g_window_width, g_window_height, "Vulkan+GLFW GUI Playground", nullptr, nullptr); - if (not glfwVulkanSupported()) - { - std::println(std::cerr, "GLFW: Vulkan Not Supported"); - return -1; - } - - std::uint32_t extensions_count; - const char** extensions_raw = glfwGetRequiredInstanceExtensions(&extensions_count); - std::vector extensions{extensions_raw, extensions_raw + extensions_count}; - vulkan_setup(extensions); - - // Create Window Surface - vulkan_check_error(glfwCreateWindowSurface(g_instance, g_window, g_allocation_callbacks, &g_window_surface)); - - // Create frame buffer - glfwGetFramebufferSize(g_window, &g_window_frame_buffer_width, &g_window_frame_buffer_height); - if (not vulkan_setup_window()) - { - std::println(std::cerr, "Vulkan: vulkan_setup_window failed"); - return -1; - } - - // Setup Platform/Renderer backends - init(); - - // user data here - { - g_draw_data.display_rect = {0, 0, static_cast(g_window_width), static_cast(g_window_height)}; - auto& draw_list = g_draw_data.draw_lists.emplace_back(); - - constexpr vertex_type v1{{100, 100}, {.1f, .1f}, primitive::colors::red}; - constexpr vertex_type v2{{150, 150}, {.1f, .1f}, primitive::colors::green}; - constexpr vertex_type v3{{200, 200}, {.1f, .1f}, primitive::colors::blue}; - - draw_list.vertex_list.triangle(v1, v2, v3); - draw_list.index_list.push_back(0); - draw_list.index_list.push_back(1); - draw_list.index_list.push_back(2); - } - - // Main loop - while (not glfwWindowShouldClose(g_window)) - { - glfwPollEvents(); - - if (g_window_swap_chain_rebuild_required) - { - glfwGetFramebufferSize(g_window, &g_window_frame_buffer_width, &g_window_frame_buffer_height); - if (g_window_frame_buffer_width > 0 and g_window_frame_buffer_height > 0) - { - vulkan_create_or_resize_window(); - g_window_frame_current_index = 0; - g_window_swap_chain_rebuild_required = false; - } - } - - new_frame(); - - if (g_window_width > 0 and g_window_height > 0) - { - frame_render(); - frame_present(); - } - } - - // Cleanup - vulkan_check_error(vkDeviceWaitIdle(g_device)); - shutdown(); - - vulkan_cleanup_window(); - vulkan_cleanup(); - - glfwDestroyWindow(g_window); - glfwTerminate(); - - return 0; -} - -namespace -{ - [[nodiscard]] auto memory_type(const VkMemoryPropertyFlags property_flag, const std::uint32_t type_bits) -> std::uint32_t - { - VkPhysicalDeviceMemoryProperties memory_properties; - vkGetPhysicalDeviceMemoryProperties(g_physical_device, &memory_properties); - - for (std::uint32_t i = 0; i < memory_properties.memoryTypeCount; ++i) - { - if ((memory_properties.memoryTypes[i].propertyFlags & property_flag) == property_flag and type_bits & (1 << i)) - { - return i; - } - } - return std::numeric_limits::max(); - } - - auto vulkan_setup(std::vector& extensions) -> void - { - // Create Vulkan Instance - { - VkInstanceCreateInfo instance_create_info{ - .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .pApplicationInfo = nullptr, - .enabledLayerCount = 0, - .ppEnabledLayerNames = nullptr, - .enabledExtensionCount = 0, - .ppEnabledExtensionNames = nullptr, - }; - - // Enumerate available extensions - std::uint32_t properties_count; - vkEnumerateInstanceExtensionProperties(nullptr, &properties_count, nullptr); - std::vector properties; - properties.resize(properties_count); - vulkan_check_error(vkEnumerateInstanceExtensionProperties(nullptr, &properties_count, properties.data())); - - // Enable required extensions - if (std::ranges::contains( - properties, - std::string_view{VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME}, - &VkExtensionProperties::extensionName - )) - { - extensions.push_back(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME); - } - if (std::ranges::contains( - properties, - std::string_view{VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME}, - &VkExtensionProperties::extensionName - )) - { - extensions.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); - instance_create_info.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; - } - - // Enabling validation layers - constexpr const char* layer_validation[]{"VK_LAYER_KHRONOS_validation"}; - instance_create_info.enabledLayerCount = 1; - instance_create_info.ppEnabledLayerNames = layer_validation; - extensions.push_back(VK_EXT_DEBUG_REPORT_EXTENSION_NAME); - - // Create Vulkan Instance - instance_create_info.enabledExtensionCount = static_cast(extensions.size()); - instance_create_info.ppEnabledExtensionNames = extensions.data(); - vulkan_check_error(vkCreateInstance(&instance_create_info, g_allocation_callbacks, &g_instance)); - - // Setup the debug report callback - const auto create_debug_report_callback = - reinterpret_cast( // NOLINT(clang-diagnostic-cast-function-type-strict) - vkGetInstanceProcAddr(g_instance, "vkCreateDebugReportCallbackEXT") - ); - assert(create_debug_report_callback); - VkDebugReportCallbackCreateInfoEXT debug_report_callback_create_info{ - .sType = VK_STRUCTURE_TYPE_DEBUG_REPORT_CALLBACK_CREATE_INFO_EXT, - .pNext = nullptr, - .flags = VK_DEBUG_REPORT_ERROR_BIT_EXT | VK_DEBUG_REPORT_WARNING_BIT_EXT | VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT, - .pfnCallback = vulkan_debug_report, - .pUserData = nullptr - }; - vulkan_check_error( - create_debug_report_callback(g_instance, &debug_report_callback_create_info, g_allocation_callbacks, &g_debug_report_callback) - ); - } - - // Select Physical Device (GPU) - { - std::uint32_t gpu_count; - vulkan_check_error(vkEnumeratePhysicalDevices(g_instance, &gpu_count, nullptr)); - assert(gpu_count != 0); - std::vector gpus; - gpus.resize(gpu_count); - vulkan_check_error(vkEnumeratePhysicalDevices(g_instance, &gpu_count, gpus.data())); - - for (const VkPhysicalDevice& device: gpus) - { - VkPhysicalDeviceProperties properties; - vkGetPhysicalDeviceProperties(device, &properties); - - if (properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) - { - g_physical_device = device; - return; - } - } - - // Use first GPU (Integrated) is a Discrete one is not available. - g_physical_device = gpus.front(); - } - - // Select graphics queue family - { - std::uint32_t queue_family_properties_count; - vkGetPhysicalDeviceQueueFamilyProperties(g_physical_device, &queue_family_properties_count, nullptr); - std::vector queue_family_properties; - queue_family_properties.resize(queue_family_properties_count); - vkGetPhysicalDeviceQueueFamilyProperties(g_physical_device, &queue_family_properties_count, queue_family_properties.data()); - - for (std::uint32_t i = 0; i < queue_family_properties_count; ++i) - { - if (queue_family_properties[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) - { - g_queue_family = i; - break; - } - } - } - - // Create Logical Device (with 1 queue) - { - std::vector device_extensions{VK_KHR_SWAPCHAIN_EXTENSION_NAME}; - - // Enumerate physical device extension - std::uint32_t extension_properties_count; - vkEnumerateDeviceExtensionProperties(g_physical_device, nullptr, &extension_properties_count, nullptr); - std::vector extension_properties; - extension_properties.resize(extension_properties_count); - vkEnumerateDeviceExtensionProperties(g_physical_device, nullptr, &extension_properties_count, extension_properties.data()); - #if defined(VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME) - if (std::ranges::contains(extension_properties, VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME, &VkExtensionProperties::extensionName)) - { - extension_properties.push_back(VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME); - } - #endif - - constexpr float queue_priority[]{ - 1.f, - }; - const VkDeviceQueueCreateInfo device_queue_create_info{ - .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .queueFamilyIndex = g_queue_family, - .queueCount = 1, - .pQueuePriorities = queue_priority - }; - const VkDeviceCreateInfo device_create_info{ - .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .queueCreateInfoCount = 1, - .pQueueCreateInfos = &device_queue_create_info, - .enabledLayerCount = 0, - .ppEnabledLayerNames = nullptr, - .enabledExtensionCount = static_cast(device_extensions.size()), - .ppEnabledExtensionNames = device_extensions.data(), - .pEnabledFeatures = nullptr - }; - vulkan_check_error(vkCreateDevice(g_physical_device, &device_create_info, g_allocation_callbacks, &g_device)); - vkGetDeviceQueue(g_device, g_queue_family, 0, &g_queue); - } - - // Create Descriptor Pool - { - // a single combined image sampler descriptor for the font image and only uses one descriptor set (for that) - - constexpr VkDescriptorPoolSize descriptor_pool_size[]{ - {.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .descriptorCount = 1}, - }; - // ReSharper disable once CppVariableCanBeMadeConstexpr - const VkDescriptorPoolCreateInfo descriptor_pool_create_info{ - .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, - .pNext = nullptr, - .flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT, - .maxSets = 1, - .poolSizeCount = std::ranges::size(descriptor_pool_size), - .pPoolSizes = descriptor_pool_size - }; - - vulkan_check_error(vkCreateDescriptorPool(g_device, &descriptor_pool_create_info, g_allocation_callbacks, &g_descriptor_pool)); - } - } - - auto vulkan_create_or_resize_window() -> void - { - // swap-chain - { - auto old_swap_chain = std::exchange(g_window_swap_chain, VK_NULL_HANDLE); - vulkan_check_error(vkDeviceWaitIdle(g_device)); - - g_window_frames_destroyer(g_device, g_allocation_callbacks); - g_window_frame_semaphores_destroyer(g_device, g_allocation_callbacks); - - if (g_pipeline_render_pass != VK_NULL_HANDLE) - { - vkDestroyRenderPass(g_device, g_pipeline_render_pass, g_allocation_callbacks); - g_pipeline_render_pass = VK_NULL_HANDLE; - } - if (g_pipeline != VK_NULL_HANDLE) - { - vkDestroyPipeline(g_device, g_pipeline, g_allocation_callbacks); - g_pipeline = VK_NULL_HANDLE; - } - - // Create SwapChain - { - VkSurfaceCapabilitiesKHR surface_capabilities; - vulkan_check_error(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(g_physical_device, g_window_surface, &surface_capabilities)); - - // If min image count was not specified, request different count of images dependent on selected present mode - auto image_count = g_window_min_image_count; - if (image_count < surface_capabilities.minImageCount) - { - image_count = surface_capabilities.minImageCount; - } - if (surface_capabilities.maxImageCount != 0 and image_count > surface_capabilities.maxImageCount) - { - image_count = surface_capabilities.maxImageCount; - } - - if (surface_capabilities.currentExtent.width != std::numeric_limits::max()) - { - g_window_width = static_cast(surface_capabilities.currentExtent.width); - g_window_height = static_cast(surface_capabilities.currentExtent.height); - } - - VkSwapchainCreateInfoKHR swap_chain_create_info{ - .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, - .pNext = nullptr, - .flags = 0, - .surface = g_window_surface, - .minImageCount = image_count, - .imageFormat = g_window_surface_format.format, - .imageColorSpace = g_window_surface_format.colorSpace, - .imageExtent = {.width = static_cast(g_window_width), .height = static_cast(g_window_height)}, - .imageArrayLayers = 1, - .imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, - .imageSharingMode = VK_SHARING_MODE_EXCLUSIVE, - .queueFamilyIndexCount = 0, - .pQueueFamilyIndices = nullptr, - .preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR, - .compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, - .presentMode = g_window_present_mode, - .clipped = VK_TRUE, - .oldSwapchain = old_swap_chain - }; - vulkan_check_error(vkCreateSwapchainKHR(g_device, &swap_chain_create_info, g_allocation_callbacks, &g_window_swap_chain)); - - vulkan_check_error(vkGetSwapchainImagesKHR(g_device, g_window_swap_chain, &g_window_frame_total_count, nullptr)); - g_window_frame_semaphore_total_count = g_window_frame_total_count + 1; - - g_window_frames_maker(); - g_window_frame_semaphores_maker(); - - VkImage back_buffer[16]{}; - vulkan_check_error(vkGetSwapchainImagesKHR(g_device, g_window_swap_chain, &g_window_frame_total_count, back_buffer)); - for (std::uint32_t i = 0; i < g_window_frame_total_count; ++i) - { - g_window_frames[i].back_buffer = back_buffer[i]; - } - } - - if (old_swap_chain) - { - vkDestroySwapchainKHR(g_device, old_swap_chain, g_allocation_callbacks); - } - - // Create Render Pass - if (not g_pipeline_use_dynamic_rendering) - { - const VkAttachmentDescription attachment_description{ - .flags = 0, - .format = g_window_surface_format.format, - .samples = VK_SAMPLE_COUNT_1_BIT, - .loadOp = g_window_clear_enable ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_DONT_CARE, - .storeOp = VK_ATTACHMENT_STORE_OP_STORE, - .stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE, - .stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE, - .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, - .finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR - }; - - constexpr VkAttachmentReference attachment_reference{.attachment = 0, .layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; - - // ReSharper disable once CppVariableCanBeMadeConstexpr - const VkSubpassDescription sub_pass_description{ - .flags = 0, - .pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, - .inputAttachmentCount = 0, - .pInputAttachments = nullptr, - .colorAttachmentCount = 1, - .pColorAttachments = &attachment_reference, - .pResolveAttachments = nullptr, - .pDepthStencilAttachment = nullptr, - .preserveAttachmentCount = 0, - .pPreserveAttachments = nullptr - }; - - constexpr VkSubpassDependency sub_pass_dependency{ - .srcSubpass = VK_SUBPASS_EXTERNAL, - .dstSubpass = 0, - .srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - .dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - .srcAccessMask = 0, - .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, - .dependencyFlags = 0 - }; - - const VkRenderPassCreateInfo render_pass_create_info{ - .sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .attachmentCount = 1, - .pAttachments = &attachment_description, - .subpassCount = 1, - .pSubpasses = &sub_pass_description, - .dependencyCount = 1, - .pDependencies = &sub_pass_dependency - }; - - vulkan_check_error(vkCreateRenderPass(g_device, &render_pass_create_info, g_allocation_callbacks, &g_pipeline_render_pass)); - } - - // Create Image Views - { - VkImageViewCreateInfo image_view_create_info{ - .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .image = VK_NULL_HANDLE, - .viewType = VK_IMAGE_VIEW_TYPE_2D, - .format = g_window_surface_format.format, - .components = - {.r = VK_COMPONENT_SWIZZLE_R, .g = VK_COMPONENT_SWIZZLE_G, .b = VK_COMPONENT_SWIZZLE_B, .a = VK_COMPONENT_SWIZZLE_A}, - .subresourceRange = - {.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1} - }; - - std::ranges::for_each( - std::ranges::subrange{g_window_frames.get(), g_window_frames.get() + g_window_frame_total_count}, - [&](detail::frame_type& frame) - { - image_view_create_info.image = frame.back_buffer; - vulkan_check_error(vkCreateImageView(g_device, &image_view_create_info, g_allocation_callbacks, &frame.back_buffer_view)); - } - ); - } - - // Create Frame Buffer - if (not g_pipeline_use_dynamic_rendering) - { - VkFramebufferCreateInfo frame_buffer_create_info{ - .sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .renderPass = g_pipeline_render_pass, - .attachmentCount = 1, - .pAttachments = VK_NULL_HANDLE, - .width = static_cast(g_window_width), - .height = static_cast(g_window_height), - .layers = 1 - }; - - std::ranges::for_each( - std::ranges::subrange{g_window_frames.get(), g_window_frames.get() + g_window_frame_total_count}, - [&](detail::frame_type& frame) - { - frame_buffer_create_info.pAttachments = &frame.back_buffer_view; - vulkan_check_error(vkCreateFramebuffer(g_device, &frame_buffer_create_info, g_allocation_callbacks, &frame.frame_buffer)); - } - ); - } - } - - // command buffer - { - std::ranges::for_each( - std::ranges::subrange{g_window_frames.get(), g_window_frames.get() + g_window_frame_total_count}, - [&](detail::frame_type& frame) - { - { - const VkCommandPoolCreateInfo command_pool_create_info{ - .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .queueFamilyIndex = g_queue_family - }; - vulkan_check_error(vkCreateCommandPool(g_device, &command_pool_create_info, g_allocation_callbacks, &frame.command_pool)); - } - { - const VkCommandBufferAllocateInfo command_buffer_allocate_info{ - .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, - .pNext = nullptr, - .commandPool = frame.command_pool, - .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, - .commandBufferCount = 1 - }; - vulkan_check_error( - vkAllocateCommandBuffers(g_device, &command_buffer_allocate_info, &frame.command_buffer) - ); - } - { - constexpr VkFenceCreateInfo fence_create_info{ - .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, .pNext = nullptr, .flags = VK_FENCE_CREATE_SIGNALED_BIT - }; - vulkan_check_error(vkCreateFence(g_device, &fence_create_info, g_allocation_callbacks, &frame.fence)); - } - } - ); - - std::ranges::for_each( - std::ranges::subrange{g_window_frame_semaphores.get(), g_window_frame_semaphores.get() + g_window_frame_semaphore_total_count}, - [&](detail::frame_semaphore_type& frame_semaphore) - { - constexpr VkSemaphoreCreateInfo semaphore_create_info{ - .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, .pNext = nullptr, .flags = 0 - }; - vulkan_check_error(vkCreateSemaphore( - g_device, - &semaphore_create_info, - g_allocation_callbacks, - &frame_semaphore.image_acquired_semaphore - )); - vulkan_check_error( - vkCreateSemaphore( - g_device, - &semaphore_create_info, - g_allocation_callbacks, - &frame_semaphore.render_complete_semaphore - )); - } - ); - } - } - - auto vulkan_setup_window() -> bool - { - // Check for WSI support - { - VkBool32 surface_support_result; - vkGetPhysicalDeviceSurfaceSupportKHR(g_physical_device, g_queue_family, g_window_surface, &surface_support_result); - if (surface_support_result != VK_TRUE) - { - std::println(std::cerr, "Vulkan Error: no WSI support on physical device"); - return false; - } - } - - // Select Surface Format - { - constexpr VkFormat request_surface_image_format[]{ - VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_B8G8R8_UNORM, VK_FORMAT_R8G8B8_UNORM - }; - constexpr VkColorSpaceKHR request_surface_color_space{VK_COLORSPACE_SRGB_NONLINEAR_KHR}; - - std::uint32_t surface_format_count; - vkGetPhysicalDeviceSurfaceFormatsKHR(g_physical_device, g_window_surface, &surface_format_count, nullptr); - std::vector surface_format; - surface_format.resize(surface_format_count); - vkGetPhysicalDeviceSurfaceFormatsKHR(g_physical_device, g_window_surface, &surface_format_count, surface_format.data()); - - // First check if only one format, VK_FORMAT_UNDEFINED, is available, which would imply that any format is available - if (surface_format_count == 1) - { - if (surface_format.front().format == VK_FORMAT_UNDEFINED) - { - g_window_surface_format.format = request_surface_image_format[0]; - g_window_surface_format.colorSpace = request_surface_color_space; - } - else - { - g_window_surface_format = surface_format.front(); - } - } - else - { - // Request several formats, the first found will be used - if (const auto result = [&]() -> std::optional - { - for (const VkFormat& request_format: request_surface_image_format) - { - if (const auto it = std::ranges::find_if( - surface_format, - [&](const VkSurfaceFormatKHR& format) noexcept -> bool - { - return format.format == request_format and format.colorSpace == request_surface_color_space; - } - ); - it != surface_format.end()) - { - return std::make_optional(*it); - } - } - - return std::nullopt; - }(); - result.has_value()) - { - g_window_surface_format = *result; - } - else - { - // If none of the requested image formats could be found, use the first available - g_window_surface_format = surface_format.front(); - } - } - } - - // Select Present Mode - { - // fixme: unlimited frame rate? - constexpr VkPresentModeKHR request_present_mode[] - { - #if 1 // NOLINT(readability-avoid-unconditional-preprocessor-if) - VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_IMMEDIATE_KHR, - #endif - VK_PRESENT_MODE_FIFO_KHR - }; - - // Request a certain mode and confirm that it is available. If not use VK_PRESENT_MODE_FIFO_KHR which is mandatory - std::uint32_t surface_present_mode_count; - vkGetPhysicalDeviceSurfacePresentModesKHR(g_physical_device, g_window_surface, &surface_present_mode_count, nullptr); - std::vector surface_present_mode; - surface_present_mode.resize(surface_present_mode_count); - vkGetPhysicalDeviceSurfacePresentModesKHR(g_physical_device, g_window_surface, &surface_present_mode_count, surface_present_mode.data()); - - if (const auto mode_it = std::ranges::find_if( - request_present_mode, - [&](const VkPresentModeKHR& request_mode) noexcept -> bool { return std::ranges::contains(surface_present_mode, request_mode); } - ); - mode_it != std::ranges::end(request_present_mode)) - { - g_window_present_mode = *mode_it; - } - else - { - g_window_present_mode = VK_PRESENT_MODE_FIFO_KHR; - } - } - - vulkan_create_or_resize_window(); - return true; - } - - GLFWwindowfocusfun g_glfw_callback_window_focus; - GLFWcursorenterfun g_glfw_callback_window_cursor_enter; - GLFWcursorposfun g_glfw_callback_window_cursor_position; - GLFWmousebuttonfun g_glfw_callback_window_mouse_button; - GLFWscrollfun g_glfw_callback_window_scroll; - GLFWkeyfun g_glfw_callback_window_key; - GLFWcharfun g_glfw_callback_window_char; - GLFWmonitorfun g_glfw_callback_window_monitor; - - auto glfw_init() -> void - { - static auto callback_window_focus = [](GLFWwindow* window, const int focused) - { - std::println(std::cout, "callback_window_focus: window: 0x{:x}, focused: {}", reinterpret_cast(window), focused != 0); - - if (g_glfw_callback_window_focus) - { - g_glfw_callback_window_focus(window, focused); - } - }; - static auto callback_window_cursor_enter = [](GLFWwindow* window, const int entered) - { - std::println(std::cout, - "callback_window_cursor_enter: window: 0x{:x}, entered: {}", - reinterpret_cast(window), - entered != 0); - - if (g_glfw_callback_window_cursor_enter) - { - g_glfw_callback_window_cursor_enter(window, entered); - } - }; - static auto callback_window_cursor_position = [](GLFWwindow* window, const double x, const double y) - { - std::println(std::cout, "callback_window_cursor_position: window: 0x{:x}, x: {}, y: {}", reinterpret_cast(window), x, y); - - if (g_glfw_callback_window_cursor_position) - { - g_glfw_callback_window_cursor_position(window, x, y); - } - }; - static auto callback_window_mouse_button = [](GLFWwindow* window, const int button, const int action, const int mods) - { - std::println( - std::cout, - "callback_window_mouse_button: window: 0x{:x}, button: {}, action: {}, mods: {}", - reinterpret_cast(window), - button, - action, - mods - ); - - if (g_glfw_callback_window_mouse_button) - { - g_glfw_callback_window_mouse_button(window, button, action, mods); - } - }; - static auto callback_window_scroll = [](GLFWwindow* window, const double x, const double y) - { - std::println(std::cout, "callback_window_scroll: window: 0x{:x}, x: {}, y: {}", reinterpret_cast(window), x, y); - - if (g_glfw_callback_window_scroll) - { - g_glfw_callback_window_scroll(window, x, y); - } - }; - static auto callback_window_key = [](GLFWwindow* window, const int key_code, const int scan_code, const int action, const int mods) - { - std::println( - std::cout, - "callback_window_scroll: window: 0x{:x}, key_code: {}, scan_code: {}, action: {}, mods: {}", - reinterpret_cast(window), - key_code, - scan_code, - action, - mods - ); - - if (g_glfw_callback_window_key) - { - g_glfw_callback_window_key(window, key_code, scan_code, action, mods); - } - }; - static auto callback_window_char = [](GLFWwindow* window, const unsigned int codepoint) - { - std::println(std::cout, "callback_window_char: window: 0x{:x}, codepoint: 0x{:x}", reinterpret_cast(window), codepoint); - - if (g_glfw_callback_window_char) - { - g_glfw_callback_window_char(window, codepoint); - } - }; - static auto callback_window_monitor = [](GLFWmonitor* monitor, const int event) - { - std::println(std::cout, "callback_window_monitor: monitor: 0x{:x}, event: {}", reinterpret_cast(monitor), event); - - if (g_glfw_callback_window_monitor) - { - g_glfw_callback_window_monitor(monitor, event); - } - }; - - g_glfw_callback_window_focus = glfwSetWindowFocusCallback(g_window, callback_window_focus); - g_glfw_callback_window_cursor_enter = glfwSetCursorEnterCallback(g_window, callback_window_cursor_enter); - g_glfw_callback_window_cursor_position = glfwSetCursorPosCallback(g_window, callback_window_cursor_position); - g_glfw_callback_window_mouse_button = glfwSetMouseButtonCallback(g_window, callback_window_mouse_button); - g_glfw_callback_window_scroll = glfwSetScrollCallback(g_window, callback_window_scroll); - g_glfw_callback_window_key = glfwSetKeyCallback(g_window, callback_window_key); - g_glfw_callback_window_char = glfwSetCharCallback(g_window, callback_window_char); - g_glfw_callback_window_monitor = glfwSetMonitorCallback(callback_window_monitor); - } - - auto vulkan_init() -> void - { - if (g_font_sampler == VK_NULL_HANDLE) - { - constexpr VkSamplerCreateInfo sampler_create_info{ - .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .magFilter = VK_FILTER_LINEAR, - .minFilter = VK_FILTER_LINEAR, - .mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR, - .addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT, - .addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT, - .addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT, - .mipLodBias = .0f, - .anisotropyEnable = 0, - .maxAnisotropy = 1.f, - .compareEnable = VK_FALSE, - .compareOp = VK_COMPARE_OP_NEVER, - .minLod = -1000, - .maxLod = 1000, - .borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK, - .unnormalizedCoordinates = VK_FALSE - }; - vulkan_check_error(vkCreateSampler(g_device, &sampler_create_info, g_allocation_callbacks, &g_font_sampler)); - } - - if (g_pipeline_descriptor_set_layout == VK_NULL_HANDLE) - { - constexpr VkDescriptorSetLayoutBinding descriptor_set_layout_binding{ - .binding = 0, - .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, - .descriptorCount = 1, - .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT, - .pImmutableSamplers = nullptr - }; - // ReSharper disable once CppVariableCanBeMadeConstexpr - const VkDescriptorSetLayoutCreateInfo descriptor_set_layout_create_info{ - .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .bindingCount = 1, - .pBindings = &descriptor_set_layout_binding - }; - vulkan_check_error(vkCreateDescriptorSetLayout( - g_device, - &descriptor_set_layout_create_info, - g_allocation_callbacks, - &g_pipeline_descriptor_set_layout - )); - } - - if (g_pipeline_layout == VK_NULL_HANDLE) - { - constexpr VkPushConstantRange push_constant_range{ - .stageFlags = VK_SHADER_STAGE_VERTEX_BIT, - .offset = sizeof(float) * 0, - .size = sizeof(float) * 4}; - // ReSharper disable once CppVariableCanBeMadeConstexpr - const VkPipelineLayoutCreateInfo pipeline_layout_create_info{ - .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .setLayoutCount = 1, - .pSetLayouts = &g_pipeline_descriptor_set_layout, - .pushConstantRangeCount = 1, - .pPushConstantRanges = &push_constant_range - }; - vulkan_check_error(vkCreatePipelineLayout(g_device, &pipeline_layout_create_info, g_allocation_callbacks, &g_pipeline_layout)); - } - - if (g_pipeline_shader_module_vertex == VK_NULL_HANDLE) - { - // #version 450 core - // layout(location = 0) in vec2 aPos; - // layout(location = 1) in vec2 aUV; - // layout(location = 2) in vec4 aColor; - // layout(push_constant) uniform uPushConstant { vec2 uScale; vec2 uTranslate; } pc; - // - // out gl_PerVertex { vec4 gl_Position; }; - // layout(location = 0) out struct { vec4 Color; vec2 UV; } Out; - // - // void main() - // { - // Out.Color = aColor; - // Out.UV = aUV; - // gl_Position = vec4(aPos * pc.uScale + pc.uTranslate, 0, 1); - // } - - constexpr static uint32_t shader_data[] = { - 0x07230203, 0x00010000, 0x00080001, 0x0000002e, 0x00000000, 0x00020011, 0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, - 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x000a000f, 0x00000000, 0x00000004, 0x6e69616d, - 0x00000000, 0x0000000b, 0x0000000f, 0x00000015, 0x0000001b, 0x0000001c, 0x00030003, 0x00000002, 0x000001c2, 0x00040005, - 0x00000004, 0x6e69616d, 0x00000000, 0x00030005, 0x00000009, 0x00000000, 0x00050006, 0x00000009, 0x00000000, 0x6f6c6f43, - 0x00000072, 0x00040006, 0x00000009, 0x00000001, 0x00005655, 0x00030005, 0x0000000b, 0x0074754f, 0x00040005, 0x0000000f, - 0x6c6f4361, 0x0000726f, 0x00030005, 0x00000015, 0x00565561, 0x00060005, 0x00000019, 0x505f6c67, 0x65567265, 0x78657472, - 0x00000000, 0x00060006, 0x00000019, 0x00000000, 0x505f6c67, 0x7469736f, 0x006e6f69, 0x00030005, 0x0000001b, 0x00000000, - 0x00040005, 0x0000001c, 0x736f5061, 0x00000000, 0x00060005, 0x0000001e, 0x73755075, 0x6e6f4368, 0x6e617473, 0x00000074, - 0x00050006, 0x0000001e, 0x00000000, 0x61635375, 0x0000656c, 0x00060006, 0x0000001e, 0x00000001, 0x61725475, 0x616c736e, - 0x00006574, 0x00030005, 0x00000020, 0x00006370, 0x00040047, 0x0000000b, 0x0000001e, 0x00000000, 0x00040047, 0x0000000f, - 0x0000001e, 0x00000002, 0x00040047, 0x00000015, 0x0000001e, 0x00000001, 0x00050048, 0x00000019, 0x00000000, 0x0000000b, - 0x00000000, 0x00030047, 0x00000019, 0x00000002, 0x00040047, 0x0000001c, 0x0000001e, 0x00000000, 0x00050048, 0x0000001e, - 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x0000001e, 0x00000001, 0x00000023, 0x00000008, 0x00030047, 0x0000001e, - 0x00000002, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, - 0x00000007, 0x00000006, 0x00000004, 0x00040017, 0x00000008, 0x00000006, 0x00000002, 0x0004001e, 0x00000009, 0x00000007, - 0x00000008, 0x00040020, 0x0000000a, 0x00000003, 0x00000009, 0x0004003b, 0x0000000a, 0x0000000b, 0x00000003, 0x00040015, - 0x0000000c, 0x00000020, 0x00000001, 0x0004002b, 0x0000000c, 0x0000000d, 0x00000000, 0x00040020, 0x0000000e, 0x00000001, - 0x00000007, 0x0004003b, 0x0000000e, 0x0000000f, 0x00000001, 0x00040020, 0x00000011, 0x00000003, 0x00000007, 0x0004002b, - 0x0000000c, 0x00000013, 0x00000001, 0x00040020, 0x00000014, 0x00000001, 0x00000008, 0x0004003b, 0x00000014, 0x00000015, - 0x00000001, 0x00040020, 0x00000017, 0x00000003, 0x00000008, 0x0003001e, 0x00000019, 0x00000007, 0x00040020, 0x0000001a, - 0x00000003, 0x00000019, 0x0004003b, 0x0000001a, 0x0000001b, 0x00000003, 0x0004003b, 0x00000014, 0x0000001c, 0x00000001, - 0x0004001e, 0x0000001e, 0x00000008, 0x00000008, 0x00040020, 0x0000001f, 0x00000009, 0x0000001e, 0x0004003b, 0x0000001f, - 0x00000020, 0x00000009, 0x00040020, 0x00000021, 0x00000009, 0x00000008, 0x0004002b, 0x00000006, 0x00000028, 0x00000000, - 0x0004002b, 0x00000006, 0x00000029, 0x3f800000, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, - 0x00000005, 0x0004003d, 0x00000007, 0x00000010, 0x0000000f, 0x00050041, 0x00000011, 0x00000012, 0x0000000b, 0x0000000d, - 0x0003003e, 0x00000012, 0x00000010, 0x0004003d, 0x00000008, 0x00000016, 0x00000015, 0x00050041, 0x00000017, 0x00000018, - 0x0000000b, 0x00000013, 0x0003003e, 0x00000018, 0x00000016, 0x0004003d, 0x00000008, 0x0000001d, 0x0000001c, 0x00050041, - 0x00000021, 0x00000022, 0x00000020, 0x0000000d, 0x0004003d, 0x00000008, 0x00000023, 0x00000022, 0x00050085, 0x00000008, - 0x00000024, 0x0000001d, 0x00000023, 0x00050041, 0x00000021, 0x00000025, 0x00000020, 0x00000013, 0x0004003d, 0x00000008, - 0x00000026, 0x00000025, 0x00050081, 0x00000008, 0x00000027, 0x00000024, 0x00000026, 0x00050051, 0x00000006, 0x0000002a, - 0x00000027, 0x00000000, 0x00050051, 0x00000006, 0x0000002b, 0x00000027, 0x00000001, 0x00070050, 0x00000007, 0x0000002c, - 0x0000002a, 0x0000002b, 0x00000028, 0x00000029, 0x00050041, 0x00000011, 0x0000002d, 0x0000001b, 0x0000000d, 0x0003003e, - 0x0000002d, 0x0000002c, 0x000100fd, 0x00010038 - }; - - const VkShaderModuleCreateInfo shader_module_create_info{ - .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .codeSize = sizeof(shader_data), - .pCode = shader_data - }; - vulkan_check_error(vkCreateShaderModule( - g_device, - &shader_module_create_info, - g_allocation_callbacks, - &g_pipeline_shader_module_vertex - ) - ); - } - if (g_pipeline_shader_module_fragment == VK_NULL_HANDLE) - { - // #version 450 core - // layout(location = 0) out vec4 fColor; - // layout(set=0, binding=0) uniform sampler2D sTexture; - // layout(location = 0) in struct { vec4 Color; vec2 UV; } In; - // void main() - // { - // fColor = In.Color * texture(sTexture, In.UV.st); - // } - constexpr static uint32_t shader_data[] = { - 0x07230203, 0x00010000, 0x00080001, 0x0000001e, 0x00000000, 0x00020011, 0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, - 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0007000f, 0x00000004, 0x00000004, 0x6e69616d, - 0x00000000, 0x00000009, 0x0000000d, 0x00030010, 0x00000004, 0x00000007, 0x00030003, 0x00000002, 0x000001c2, 0x00040005, - 0x00000004, 0x6e69616d, 0x00000000, 0x00040005, 0x00000009, 0x6c6f4366, 0x0000726f, 0x00030005, 0x0000000b, 0x00000000, - 0x00050006, 0x0000000b, 0x00000000, 0x6f6c6f43, 0x00000072, 0x00040006, 0x0000000b, 0x00000001, 0x00005655, 0x00030005, - 0x0000000d, 0x00006e49, 0x00050005, 0x00000016, 0x78655473, 0x65727574, 0x00000000, 0x00040047, 0x00000009, 0x0000001e, - 0x00000000, 0x00040047, 0x0000000d, 0x0000001e, 0x00000000, 0x00040047, 0x00000016, 0x00000022, 0x00000000, 0x00040047, - 0x00000016, 0x00000021, 0x00000000, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006, - 0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000004, 0x00040020, 0x00000008, 0x00000003, 0x00000007, 0x0004003b, - 0x00000008, 0x00000009, 0x00000003, 0x00040017, 0x0000000a, 0x00000006, 0x00000002, 0x0004001e, 0x0000000b, 0x00000007, - 0x0000000a, 0x00040020, 0x0000000c, 0x00000001, 0x0000000b, 0x0004003b, 0x0000000c, 0x0000000d, 0x00000001, 0x00040015, - 0x0000000e, 0x00000020, 0x00000001, 0x0004002b, 0x0000000e, 0x0000000f, 0x00000000, 0x00040020, 0x00000010, 0x00000001, - 0x00000007, 0x00090019, 0x00000013, 0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00000000, - 0x0003001b, 0x00000014, 0x00000013, 0x00040020, 0x00000015, 0x00000000, 0x00000014, 0x0004003b, 0x00000015, 0x00000016, - 0x00000000, 0x0004002b, 0x0000000e, 0x00000018, 0x00000001, 0x00040020, 0x00000019, 0x00000001, 0x0000000a, 0x00050036, - 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x00050041, 0x00000010, 0x00000011, 0x0000000d, - 0x0000000f, 0x0004003d, 0x00000007, 0x00000012, 0x00000011, 0x0004003d, 0x00000014, 0x00000017, 0x00000016, 0x00050041, - 0x00000019, 0x0000001a, 0x0000000d, 0x00000018, 0x0004003d, 0x0000000a, 0x0000001b, 0x0000001a, 0x00050057, 0x00000007, - 0x0000001c, 0x00000017, 0x0000001b, 0x00050085, 0x00000007, 0x0000001d, 0x00000012, 0x0000001c, 0x0003003e, 0x00000009, - 0x0000001d, 0x000100fd, 0x00010038 - }; - - const VkShaderModuleCreateInfo shader_module_create_info{ - .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .codeSize = sizeof(shader_data), - .pCode = shader_data - }; - vulkan_check_error(vkCreateShaderModule( - g_device, - &shader_module_create_info, - g_allocation_callbacks, - &g_pipeline_shader_module_fragment - ) - ); - } - - if (g_pipeline == VK_NULL_HANDLE) - { - const VkPipelineShaderStageCreateInfo pipeline_shader_stage_create_info_vertex{ - .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .stage = VK_SHADER_STAGE_VERTEX_BIT, - .module = g_pipeline_shader_module_vertex, - .pName = "main", - .pSpecializationInfo = nullptr - }; - const VkPipelineShaderStageCreateInfo pipeline_shader_stage_create_info_fragment{ - .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .stage = VK_SHADER_STAGE_FRAGMENT_BIT, - .module = g_pipeline_shader_module_fragment, - .pName = "main", - .pSpecializationInfo = nullptr - }; - const VkPipelineShaderStageCreateInfo pipeline_shader_stage_create_info[2] - { - pipeline_shader_stage_create_info_vertex, - pipeline_shader_stage_create_info_fragment - }; - - constexpr VkVertexInputBindingDescription vertex_input_binding_description{ - .binding = 0, .stride = sizeof(vertex_type), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX - }; - constexpr VkVertexInputAttributeDescription vertex_input_attribute_description_position{ - .location = 0, - .binding = vertex_input_binding_description.binding, - .format = VK_FORMAT_R32G32_SFLOAT, - .offset = offsetof(vertex_type, position) - }; - constexpr VkVertexInputAttributeDescription vertex_input_attribute_description_uv{ - .location = 1, - .binding = vertex_input_binding_description.binding, - .format = VK_FORMAT_R32G32_SFLOAT, - .offset = offsetof(vertex_type, uv) - }; - constexpr VkVertexInputAttributeDescription vertex_input_attribute_description_color{ - .location = 2, - .binding = vertex_input_binding_description.binding, - .format = VK_FORMAT_R8G8B8A8_UNORM, - .offset = offsetof(vertex_type, color) - }; - constexpr VkVertexInputAttributeDescription vertex_input_attribute_descriptions[3]{ - vertex_input_attribute_description_position, - vertex_input_attribute_description_uv, - vertex_input_attribute_description_color - }; - const VkPipelineVertexInputStateCreateInfo pipeline_vertex_input_state_create_info{ - .sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .vertexBindingDescriptionCount = 1, - .pVertexBindingDescriptions = &vertex_input_binding_description, - .vertexAttributeDescriptionCount = 3, - .pVertexAttributeDescriptions = vertex_input_attribute_descriptions - }; - - constexpr VkPipelineInputAssemblyStateCreateInfo pipeline_input_assembly_state_create_info{ - .sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, - .primitiveRestartEnable = VK_FALSE - }; - - constexpr VkPipelineViewportStateCreateInfo pipeline_viewport_state_create_info{ - .sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .viewportCount = 1, - .pViewports = nullptr, - .scissorCount = 1, - .pScissors = nullptr - }; - - constexpr VkPipelineRasterizationStateCreateInfo pipeline_rasterization_state_create_info{ - .sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .depthClampEnable = VK_FALSE, - .rasterizerDiscardEnable = VK_FALSE, - .polygonMode = VK_POLYGON_MODE_FILL, - .cullMode = VK_CULL_MODE_NONE, - .frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE, - .depthBiasEnable = VK_FALSE, - .depthBiasConstantFactor = .0f, - .depthBiasClamp = .0f, - .depthBiasSlopeFactor = .0f, - .lineWidth = 1.f - }; - - const VkPipelineMultisampleStateCreateInfo pipeline_multi_sample_state_create_info{ - .sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .rasterizationSamples = g_pipeline_rasterization_msaa, - .sampleShadingEnable = false, - .minSampleShading = .0f, - .pSampleMask = nullptr, - .alphaToCoverageEnable = VK_FALSE, - .alphaToOneEnable = VK_FALSE - }; - - constexpr VkPipelineDepthStencilStateCreateInfo pipeline_depth_stencil_state_create_info{ - .sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .depthTestEnable = VK_FALSE, - .depthWriteEnable = VK_FALSE, - .depthCompareOp = VK_COMPARE_OP_NEVER, - .depthBoundsTestEnable = VK_FALSE, - .stencilTestEnable = VK_FALSE, - .front = {}, - .back = {}, - .minDepthBounds = .0f, - .maxDepthBounds = .0f - }; - - constexpr VkPipelineColorBlendAttachmentState pipeline_color_blend_attachment_state{ - .blendEnable = VK_TRUE, - .srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA, - .dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, - .colorBlendOp = VK_BLEND_OP_ADD, - .srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE, - .dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, - .alphaBlendOp = VK_BLEND_OP_ADD, - .colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT - }; - // ReSharper disable once CppVariableCanBeMadeConstexpr - const VkPipelineColorBlendStateCreateInfo pipeline_color_blend_state_create_info{ - .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .logicOpEnable = VK_FALSE, - .logicOp = VK_LOGIC_OP_CLEAR, - .attachmentCount = 1, - .pAttachments = &pipeline_color_blend_attachment_state, - .blendConstants = {} - }; - - constexpr VkDynamicState dynamic_state[2]{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; - // ReSharper disable once CppVariableCanBeMadeConstexpr - const VkPipelineDynamicStateCreateInfo pipeline_dynamic_state_create_info{ - .sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .dynamicStateCount = std::ranges::size(dynamic_state), - .pDynamicStates = dynamic_state - }; - - VkGraphicsPipelineCreateInfo pipeline_create_info{ - .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, - .pNext = nullptr, - .flags = g_pipeline_create_flags, - .stageCount = 2, - .pStages = pipeline_shader_stage_create_info, - .pVertexInputState = &pipeline_vertex_input_state_create_info, - .pInputAssemblyState = &pipeline_input_assembly_state_create_info, - .pTessellationState = nullptr, - .pViewportState = &pipeline_viewport_state_create_info, - .pRasterizationState = &pipeline_rasterization_state_create_info, - .pMultisampleState = &pipeline_multi_sample_state_create_info, - .pDepthStencilState = &pipeline_depth_stencil_state_create_info, - .pColorBlendState = &pipeline_color_blend_state_create_info, - .pDynamicState = &pipeline_dynamic_state_create_info, - .layout = g_pipeline_layout, - .renderPass = g_pipeline_render_pass, - .subpass = g_pipeline_sub_pass, - .basePipelineHandle = VK_NULL_HANDLE, - .basePipelineIndex = 0 - }; - if (g_pipeline_use_dynamic_rendering) - { - assert(g_pipeline_rendering_create_info.sType == VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR && - "pipeline_rendering_create_info sType must be VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR"); - assert(g_pipeline_rendering_create_info.pNext == nullptr && "pipeline_rendering_create_info.pNext must be NULL"); - - pipeline_create_info.pNext = &g_pipeline_rendering_create_info; - pipeline_create_info.renderPass = VK_NULL_HANDLE; - } - - vulkan_check_error(vkCreateGraphicsPipelines(g_device, g_pipeline_cache, 1, &pipeline_create_info, g_allocation_callbacks, &g_pipeline)); - } - } - - auto init() -> void - { - glfw_init(); - vulkan_init(); - } - - auto destroy_font_texture() -> void - { - if (g_font_descriptor_set != VK_NULL_HANDLE) - { - vkFreeDescriptorSets(g_device, g_descriptor_pool, 1, &g_font_descriptor_set); - g_font_descriptor_set = VK_NULL_HANDLE; - } - if (g_font_image_view != VK_NULL_HANDLE) - { - vkDestroyImageView(g_device, g_font_image_view, g_allocation_callbacks); - g_font_image_view = VK_NULL_HANDLE; - } - if (g_font_image != VK_NULL_HANDLE) - { - vkDestroyImage(g_device, g_font_image, g_allocation_callbacks); - g_font_image = VK_NULL_HANDLE; - } - if (g_font_memory != VK_NULL_HANDLE) - { - vkFreeMemory(g_device, g_font_memory, g_allocation_callbacks); - g_font_memory = VK_NULL_HANDLE; - } - } - - auto create_font_texture() -> void - { - if (g_font_descriptor_set != VK_NULL_HANDLE) - { - return; - } - - // Destroy existing texture (if any) - if (g_font_memory or g_font_image or g_font_image_view) - { - vkQueueWaitIdle(g_queue); - destroy_font_texture(); - } - - // Create command pool/buffer - if (g_font_command_pool == VK_NULL_HANDLE) - { - const VkCommandPoolCreateInfo command_pool_create_info{ - .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, .pNext = nullptr, .flags = 0, - .queueFamilyIndex = g_queue_family - }; - vulkan_check_error(vkCreateCommandPool( - g_device, - &command_pool_create_info, - g_allocation_callbacks, - &g_font_command_pool - ) - ); - } - if (g_font_command_buffer == VK_NULL_HANDLE) - { - const VkCommandBufferAllocateInfo command_buffer_allocate_info{ - .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, - .pNext = nullptr, - .commandPool = g_font_command_pool, - .commandBufferCount = 1 - }; - vulkan_check_error(vkAllocateCommandBuffers(g_device, &command_buffer_allocate_info, &g_font_command_buffer)); - } - - // Start command buffer - { - vulkan_check_error(vkResetCommandPool(g_device, g_font_command_pool, 0)); - - constexpr VkCommandBufferBeginInfo command_buffer_begin_info{ - .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, - .pNext = nullptr, - .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, - .pInheritanceInfo = nullptr - }; - vulkan_check_error(vkBeginCommandBuffer(g_font_command_buffer, &command_buffer_begin_info)); - } - - // todo: RGBA[8+8+8+8] - const auto [pixels, width, height] = load_font(); - const auto upload_size = static_cast(width) * height * 4 * sizeof(unsigned char); - - // Create Image - { - const VkImageCreateInfo image_create_info{ - .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .imageType = VK_IMAGE_TYPE_2D, - .format = VK_FORMAT_R8G8B8A8_UNORM, - .extent = {.width = static_cast(width), .height = static_cast(height), .depth = 1}, - .mipLevels = 1, - .arrayLayers = 1, - .samples = VK_SAMPLE_COUNT_1_BIT, - .tiling = VK_IMAGE_TILING_OPTIMAL, - .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, - .sharingMode = VK_SHARING_MODE_EXCLUSIVE, - .queueFamilyIndexCount = 0, - .pQueueFamilyIndices = nullptr, - .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED - }; - vulkan_check_error(vkCreateImage(g_device, &image_create_info, g_allocation_callbacks, &g_font_image)); - - VkMemoryRequirements memory_requirements; - vkGetImageMemoryRequirements(g_device, g_font_image, &memory_requirements); - - const VkMemoryAllocateInfo memory_allocate_info{ - .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, - .pNext = nullptr, - .allocationSize = memory_requirements.size, - .memoryTypeIndex = memory_type(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, memory_requirements.memoryTypeBits) - }; - vulkan_check_error(vkAllocateMemory(g_device, &memory_allocate_info, g_allocation_callbacks, &g_font_memory)); - vulkan_check_error(vkBindImageMemory(g_device, g_font_image, g_font_memory, 0)); - } - - // Create Image View - { - const VkImageViewCreateInfo image_view_create_info{ - .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .image = g_font_image, - .viewType = VK_IMAGE_VIEW_TYPE_2D, - .format = VK_FORMAT_R8G8B8A8_UNORM, - .components = {}, - .subresourceRange = - { - .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, - .baseMipLevel = 0, - .levelCount = 1, - .baseArrayLayer = 0, - .layerCount = 1, - } - }; - vulkan_check_error(vkCreateImageView(g_device, &image_view_create_info, g_allocation_callbacks, &g_font_image_view)); - } - - // Create Descriptor Set - { - const VkDescriptorSetAllocateInfo descriptor_set_allocate_info{ - .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, - .pNext = nullptr, - .descriptorPool = g_descriptor_pool, - .descriptorSetCount = 1, - .pSetLayouts = &g_pipeline_descriptor_set_layout - }; - vulkan_check_error(vkAllocateDescriptorSets(g_device, &descriptor_set_allocate_info, &g_font_descriptor_set)); - - const VkDescriptorImageInfo descriptor_image_info{ - .sampler = g_font_sampler, .imageView = g_font_image_view, .imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL - }; - const VkWriteDescriptorSet write_descriptor_set{ - .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, - .pNext = nullptr, - .dstSet = g_font_descriptor_set, - .dstBinding = 0, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, - .pImageInfo = &descriptor_image_info, - .pBufferInfo = nullptr, - .pTexelBufferView = nullptr - }; - vkUpdateDescriptorSets(g_device, 1, &write_descriptor_set, 0, nullptr); - } - - // Create Upload Buffer - { - const VkBufferCreateInfo buffer_create_info{ - .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .size = upload_size, - .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, - .sharingMode = VK_SHARING_MODE_EXCLUSIVE, - .queueFamilyIndexCount = 0, - .pQueueFamilyIndices = nullptr - }; - - VkBuffer upload_buffer; - vulkan_check_error(vkCreateBuffer(g_device, &buffer_create_info, g_allocation_callbacks, &upload_buffer)); - - VkMemoryRequirements memory_requirements; - vkGetBufferMemoryRequirements(g_device, upload_buffer, &memory_requirements); - - const VkMemoryAllocateInfo memory_allocate_info{ - .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, - .pNext = nullptr, - .allocationSize = memory_requirements.size, - .memoryTypeIndex = memory_type(VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, memory_requirements.memoryTypeBits) - }; - - VkDeviceMemory upload_buffer_memory; - vulkan_check_error(vkAllocateMemory(g_device, &memory_allocate_info, g_allocation_callbacks, &upload_buffer_memory) - ); - vulkan_check_error(vkBindBufferMemory(g_device, upload_buffer, upload_buffer_memory, 0)); - - // Upload to Buffer - unsigned char* mapped_memory; - vulkan_check_error(vkMapMemory(g_device, upload_buffer_memory, 0, upload_size, 0, reinterpret_cast(&mapped_memory)) - ); - std::memcpy(mapped_memory, pixels, upload_size); - const VkMappedMemoryRange mapped_memory_range{ - .sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE, - .pNext = nullptr, - .memory = upload_buffer_memory, - .offset = 0, - .size = upload_size - }; - vulkan_check_error(vkFlushMappedMemoryRanges(g_device, 1, &mapped_memory_range)); - vkUnmapMemory(g_device, upload_buffer_memory); - - // Copy to Image - const VkImageMemoryBarrier copy_barrier{ - .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, - .pNext = nullptr, - .srcAccessMask = 0, - .dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, - .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, - .newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = g_font_image, - .subresourceRange = - {.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1 - } - }; - vkCmdPipelineBarrier( - g_font_command_buffer, - VK_PIPELINE_STAGE_HOST_BIT, - VK_PIPELINE_STAGE_TRANSFER_BIT, - 0, - 0, - nullptr, - 0, - nullptr, - 1, - ©_barrier - ); - - const VkBufferImageCopy region{ - .bufferOffset = 0, - .bufferRowLength = 0, - .bufferImageHeight = 0, - .imageSubresource = {.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .mipLevel = 0, .baseArrayLayer = 0, .layerCount = 1}, - .imageOffset = {.x = 0, .y = 0, .z = 0}, - .imageExtent = {.width = static_cast(width), .height = static_cast(height), .depth = 1} - }; - vkCmdCopyBufferToImage( - g_font_command_buffer, - upload_buffer, - g_font_image, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - 1, - ®ion - ); - - const VkImageMemoryBarrier use_barrier{ - .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, - .pNext = nullptr, - .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, - .dstAccessMask = VK_ACCESS_SHADER_READ_BIT, - .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - .newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = g_font_image, - .subresourceRange = - {.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1 - } - }; - vkCmdPipelineBarrier( - g_font_command_buffer, - VK_PIPELINE_STAGE_TRANSFER_BIT, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, - 0, - 0, - nullptr, - 0, - nullptr, - 1, - &use_barrier - ); - - // End command buffer - // ReSharper disable once CppVariableCanBeMadeConstexpr - const VkSubmitInfo submit_info{ - .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, - .pNext = nullptr, - .waitSemaphoreCount = 0, - .pWaitSemaphores = nullptr, - .pWaitDstStageMask = nullptr, - .commandBufferCount = 1, - .pCommandBuffers = &g_font_command_buffer, - .signalSemaphoreCount = 0, - .pSignalSemaphores = nullptr - }; - vulkan_check_error(vkEndCommandBuffer(g_font_command_buffer)); - vulkan_check_error(vkQueueSubmit(g_queue, 1, &submit_info, VK_NULL_HANDLE)); - vulkan_check_error(vkQueueWaitIdle(g_queue)); - - vkDestroyBuffer(g_device, upload_buffer, g_allocation_callbacks); - vkFreeMemory(g_device, upload_buffer_memory, g_allocation_callbacks); - } - } - - auto new_frame() -> void - { - // checked by callee - create_font_texture(); - - glfwGetWindowSize(g_window, &g_window_width, &g_window_height); - glfwGetFramebufferSize(g_window, &g_window_frame_buffer_width, &g_window_frame_buffer_height); - } - - auto frame_render() -> void - { - const auto image_acquired_semaphore = g_window_frame_semaphores[g_window_frame_semaphore_current_index].image_acquired_semaphore; - const auto render_complete_semaphore = g_window_frame_semaphores[g_window_frame_semaphore_current_index].render_complete_semaphore; - - if (const auto result = vkAcquireNextImageKHR( - g_device, - g_window_swap_chain, - std::numeric_limits::max(), - image_acquired_semaphore, - VK_NULL_HANDLE, - &g_window_frame_current_index - ); - result == VK_ERROR_OUT_OF_DATE_KHR or result == VK_SUBOPTIMAL_KHR) - { - g_window_swap_chain_rebuild_required = true; - return; - } - else - { - vulkan_check_error(result); - } - - const auto& this_frame = g_window_frames[g_window_frame_current_index]; - { - vulkan_check_error(vkWaitForFences(g_device, 1, &this_frame.fence, VK_TRUE, std::numeric_limits::max())); - vulkan_check_error(vkResetFences(g_device, 1, &this_frame.fence)); - } - { - vulkan_check_error(vkResetCommandPool(g_device, this_frame.command_pool, 0)); - - constexpr VkCommandBufferBeginInfo command_buffer_begin_info{ - .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, - .pNext = nullptr, - .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, - .pInheritanceInfo = nullptr - }; - vulkan_check_error(vkBeginCommandBuffer(this_frame.command_buffer, &command_buffer_begin_info)); - } - { - const VkRenderPassBeginInfo render_pass_begin_info{ - .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, - .pNext = nullptr, - .renderPass = g_pipeline_render_pass, - .framebuffer = this_frame.frame_buffer, - .renderArea = - {.offset = {.x = 0, .y = 0}, - .extent = {.width = static_cast(g_window_width), .height = static_cast(g_window_height)}}, - .clearValueCount = 1, - .pClearValues = &g_window_clear_value - }; - vkCmdBeginRenderPass(this_frame.command_buffer, &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE); - } - - if (g_window_render_buffer == nullptr) - { - g_window_render_buffer_total_count = g_window_frame_total_count; - g_window_render_buffer_maker(); - } - g_window_render_buffer_current_index = (g_window_render_buffer_current_index + 1) % g_window_render_buffer_total_count; - - auto& this_render_buffer = g_window_render_buffer[g_window_render_buffer_current_index]; - { - if (g_draw_data.total_vertex_size() > 0) - { - static auto resize_buffer = - [](VkBuffer& buffer, VkDeviceMemory& memory, const VkDeviceSize new_size, const VkBufferUsageFlagBits usage) - { - if (buffer != VK_NULL_HANDLE) - { - vkDestroyBuffer(g_device, buffer, g_allocation_callbacks); - } - if (memory != VK_NULL_HANDLE) - { - vkFreeMemory(g_device, memory, g_allocation_callbacks); - } - - const VkBufferCreateInfo buffer_create_info{ - .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .size = new_size, - .usage = static_cast(usage), - .sharingMode = VK_SHARING_MODE_EXCLUSIVE, - .queueFamilyIndexCount = 0, - .pQueueFamilyIndices = nullptr - }; - vulkan_check_error(vkCreateBuffer(g_device, &buffer_create_info, g_allocation_callbacks, &buffer)); - - VkMemoryRequirements memory_requirements; - vkGetBufferMemoryRequirements(g_device, buffer, &memory_requirements); - - const VkMemoryAllocateInfo memory_allocate_info{ - .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, - .pNext = nullptr, - .allocationSize = memory_requirements.size, - .memoryTypeIndex = memory_type(VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, memory_requirements.memoryTypeBits) - }; - vulkan_check_error(vkAllocateMemory(g_device, &memory_allocate_info, g_allocation_callbacks, &memory)); - - vulkan_check_error(vkBindBufferMemory(g_device, buffer, memory, 0)); - }; - - // Create or resize the vertex/index buffers - const auto vertex_size = g_draw_data.total_vertex_size() * sizeof(vertex_type); - const auto index_size = g_draw_data.total_index_size() * sizeof(vertex_index_type); - - if (this_render_buffer.vertex_buffer == VK_NULL_HANDLE or this_render_buffer.vertex_count < vertex_size) - { - resize_buffer( - this_render_buffer.vertex_buffer, - this_render_buffer.vertex_buffer_memory, - vertex_size, - VK_BUFFER_USAGE_VERTEX_BUFFER_BIT - ); - } - if (this_render_buffer.index_buffer == VK_NULL_HANDLE or this_render_buffer.index_count < index_size) - { - resize_buffer( - this_render_buffer.index_buffer, - this_render_buffer.index_buffer_memory, - index_size, - VK_BUFFER_USAGE_INDEX_BUFFER_BIT - ); - } - - // Upload vertex/index data into a single contiguous GPU buffer - { - struct target_vertex_type - { - float position[2]; - float uv[2]; - std::uint32_t color; - }; - static_assert(sizeof(target_vertex_type) == sizeof(vertex_type)); - - target_vertex_type* mapped_vertex = nullptr; - vertex_index_type* mapped_index = nullptr; - vulkan_check_error(vkMapMemory( - g_device, - this_render_buffer.vertex_buffer_memory, - 0, - vertex_size, - 0, - reinterpret_cast(&mapped_vertex) - )); - vulkan_check_error( - vkMapMemory(g_device, this_render_buffer.index_buffer_memory, 0, index_size, 0, reinterpret_cast(&mapped_index)) - ); - - for (const auto& [vertex_list, index_list]: g_draw_data.draw_lists) - { - std::ranges::transform( - vertex_list.vertices(), - mapped_vertex, - [](const auto& vertex) -> target_vertex_type - { - return { - .position = {vertex.position.x, vertex.position.y}, - .uv = {vertex.uv.x, vertex.uv.y}, - // todo - // .color = vertex.color.to(primitive::color_format) - .color = vertex.color.to(primitive::color_format) - }; - } - ); - std::ranges::copy(index_list, mapped_index); - - mapped_vertex += vertex_list.size(); - mapped_index += index_list.size(); - } - - const VkMappedMemoryRange mapped_memory_range_vertex{ - .sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE, - .pNext = nullptr, - .memory = this_render_buffer.vertex_buffer_memory, - .offset = 0, - .size = VK_WHOLE_SIZE - }; - const VkMappedMemoryRange mapped_memory_range_index{ - .sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE, - .pNext = nullptr, - .memory = this_render_buffer.index_buffer_memory, - .offset = 0, - .size = VK_WHOLE_SIZE - }; - const VkMappedMemoryRange mapped_memory_range[2]{mapped_memory_range_vertex, mapped_memory_range_index}; - vulkan_check_error(vkFlushMappedMemoryRanges(g_device, 2, mapped_memory_range)); - - vkUnmapMemory(g_device, this_render_buffer.vertex_buffer_memory); - vkUnmapMemory(g_device, this_render_buffer.index_buffer_memory); - } - } - - // Setup desired Vulkan state - { - // Bind pipeline - vkCmdBindPipeline(this_frame.command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, g_pipeline); - - // Bind Vertex And Index Buffer - if (g_draw_data.total_vertex_size() > 0) - { - constexpr VkDeviceSize offset[1]{}; - vkCmdBindVertexBuffers(this_frame.command_buffer, 0, 1, &this_render_buffer.vertex_buffer, offset); - vkCmdBindIndexBuffer( - this_frame.command_buffer, - this_render_buffer.index_buffer, - 0, - // ReSharper disable once CppUnreachableCode - sizeof(vertex_index_type) == 2 ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT32 - ); - } - - // Setup viewport - { - const VkViewport viewport{ - .x = 0, - .y = 0, - .width = static_cast(g_window_frame_buffer_width), - .height = static_cast(g_window_frame_buffer_height), - .minDepth = .0f, - .maxDepth = 1.f - }; - vkCmdSetViewport(this_frame.command_buffer, 0, 1, &viewport); - } - - // Setup scale and translation - { - const float scale[2]{2.f / g_draw_data.display_rect.width(), 2.f / g_draw_data.display_rect.height()}; - const float translate[2]{ - -1.f - g_draw_data.display_rect.left_top().x * scale[0], -1.f - g_draw_data.display_rect.left_top().y * scale[1] - }; - - vkCmdPushConstants( - this_frame.command_buffer, - g_pipeline_layout, - VK_SHADER_STAGE_VERTEX_BIT, - sizeof(float) * 0, - sizeof(float) * 2, - scale - ); - vkCmdPushConstants( - this_frame.command_buffer, - g_pipeline_layout, - VK_SHADER_STAGE_VERTEX_BIT, - sizeof(float) * 2, - sizeof(float) * 2, - translate - ); - } - } - - // Render command lists - { - std::ranges::for_each( - g_draw_data.draw_lists, - [&this_frame, - display_rect = g_draw_data.display_rect, - offset_vertex = static_cast(0), - offset_index = static_cast(0) - ](const draw_list_type& draw_list) mutable - { - // todo: clip rect - const VkRect2D scissor{ - .offset = - {.x = static_cast(display_rect.left_top().x), - .y = static_cast(display_rect.left_top().y)}, - .extent = - {.width = static_cast(display_rect.width()), - .height = static_cast(display_rect.height())} - }; - vkCmdSetScissor(this_frame.command_buffer, 0, 1, &scissor); - - // Bind DescriptorSet with font - vkCmdBindDescriptorSets( - this_frame.command_buffer, - VK_PIPELINE_BIND_POINT_GRAPHICS, - g_pipeline_layout, - 0, - 1, - &g_font_descriptor_set, - 0, - nullptr - ); - - // Draw - vkCmdDrawIndexed(this_frame.command_buffer, - static_cast(draw_list.index_list.size()), - 1, - offset_index, - offset_vertex, - 0); - - offset_vertex += draw_list.vertex_list.size(); - offset_index += draw_list.index_list.size(); - } - ); - } - - const VkRect2D scissor = {.offset = {.x = 0, .y = 0}, - .extent = {.width = static_cast(g_window_frame_buffer_width), - .height = static_cast(g_window_frame_buffer_height)}}; - vkCmdSetScissor(this_frame.command_buffer, 0, 1, &scissor); - } - - // Submit command buffer - vkCmdEndRenderPass(this_frame.command_buffer); - { - constexpr VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; - const VkSubmitInfo submit_info{ - .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, - .pNext = nullptr, - .waitSemaphoreCount = 1, - .pWaitSemaphores = &image_acquired_semaphore, - .pWaitDstStageMask = &wait_stage, - .commandBufferCount = 1, - .pCommandBuffers = &this_frame.command_buffer, - .signalSemaphoreCount = 1, - .pSignalSemaphores = &render_complete_semaphore - }; - - vulkan_check_error(vkEndCommandBuffer(this_frame.command_buffer)); - vulkan_check_error(vkQueueSubmit(g_queue, 1, &submit_info, this_frame.fence)); - } - } - - auto frame_present() -> void - { - if (g_window_swap_chain_rebuild_required) - { - return; - } - - const auto render_complete_semaphore = g_window_frame_semaphores[g_window_frame_semaphore_current_index].render_complete_semaphore; - const VkPresentInfoKHR present_info{ - .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, - .pNext = nullptr, - .waitSemaphoreCount = 1, - .pWaitSemaphores = &render_complete_semaphore, - .swapchainCount = 1, - .pSwapchains = &g_window_swap_chain, - .pImageIndices = &g_window_frame_current_index, - .pResults = nullptr - }; - - if (const auto result = vkQueuePresentKHR(g_queue, &present_info); result == VK_ERROR_OUT_OF_DATE_KHR or result == VK_SUBOPTIMAL_KHR) - { - g_window_swap_chain_rebuild_required = true; - return; - } - else - { - vulkan_check_error(result); - } - - g_window_frame_semaphore_current_index = (g_window_frame_semaphore_current_index + 1) % g_window_frame_semaphore_total_count; - } - - auto destroy_device_objects() -> void - { - // ============== - // render buffer - g_window_render_buffer_destroyer(g_device, g_allocation_callbacks); - - // ============== - // font - destroy_font_texture(); - if (g_font_sampler != VK_NULL_HANDLE) - { - vkDestroySampler(g_device, g_font_sampler, g_allocation_callbacks); - g_font_sampler = VK_NULL_HANDLE; - } - if (g_font_command_buffer != VK_NULL_HANDLE) - { - vkFreeCommandBuffers(g_device, g_font_command_pool, 1, &g_font_command_buffer); - g_font_command_buffer = VK_NULL_HANDLE; - } - if (g_font_command_pool != VK_NULL_HANDLE) - { - vkDestroyCommandPool(g_device, g_font_command_pool, g_allocation_callbacks); - g_font_command_pool = VK_NULL_HANDLE; - } - - // ============== - // pipeline - if (g_pipeline_shader_module_vertex != VK_NULL_HANDLE) - { - vkDestroyShaderModule(g_device, g_pipeline_shader_module_vertex, g_allocation_callbacks); - g_pipeline_shader_module_vertex = VK_NULL_HANDLE; - } - if (g_pipeline_shader_module_fragment != VK_NULL_HANDLE) - { - vkDestroyShaderModule(g_device, g_pipeline_shader_module_fragment, g_allocation_callbacks); - g_pipeline_shader_module_fragment = VK_NULL_HANDLE; - } - if (g_pipeline_layout != VK_NULL_HANDLE) - { - vkDestroyPipelineLayout(g_device, g_pipeline_layout, g_allocation_callbacks); - g_pipeline_layout = VK_NULL_HANDLE; - } - if (g_pipeline_descriptor_set_layout != VK_NULL_HANDLE) - { - vkDestroyDescriptorSetLayout(g_device, g_pipeline_descriptor_set_layout, g_allocation_callbacks); - g_pipeline_descriptor_set_layout = VK_NULL_HANDLE; - } - if (g_pipeline) - { - vkDestroyPipeline(g_device, g_pipeline, g_allocation_callbacks); - g_pipeline = VK_NULL_HANDLE; - } - } - - auto shutdown() -> void - { - destroy_device_objects(); - - glfwSetWindowFocusCallback(g_window, g_glfw_callback_window_focus); - glfwSetCursorEnterCallback(g_window, g_glfw_callback_window_cursor_enter); - glfwSetCursorPosCallback(g_window, g_glfw_callback_window_cursor_position); - glfwSetMouseButtonCallback(g_window, g_glfw_callback_window_mouse_button); - glfwSetScrollCallback(g_window, g_glfw_callback_window_scroll); - glfwSetKeyCallback(g_window, g_glfw_callback_window_key); - glfwSetCharCallback(g_window, g_glfw_callback_window_char); - glfwSetMonitorCallback(g_glfw_callback_window_monitor); - } - - auto vulkan_cleanup_window() -> void - { - vulkan_check_error(vkDeviceWaitIdle(g_device)); - - g_window_frames_destroyer(g_device, g_allocation_callbacks); - g_window_frame_semaphores_destroyer(g_device, g_allocation_callbacks); - - vkDestroyPipeline(g_device, g_pipeline, g_allocation_callbacks); - vkDestroyRenderPass(g_device, g_pipeline_render_pass, g_allocation_callbacks); - vkDestroySwapchainKHR(g_device, g_window_swap_chain, g_allocation_callbacks); - vkDestroySurfaceKHR(g_instance, g_window_surface, g_allocation_callbacks); - } - - auto vulkan_cleanup() -> void - { - vkDestroyDescriptorPool(g_device, g_descriptor_pool, g_allocation_callbacks); - - const auto destroy_debug_report_callback = - reinterpret_cast( // NOLINT(clang-diagnostic-cast-function-type-strict) - vkGetInstanceProcAddr(g_instance, "vkDestroyDebugReportCallbackEXT") - ); - destroy_debug_report_callback(g_instance, g_debug_report_callback, g_allocation_callbacks); - - vkDestroyDevice(g_device, g_allocation_callbacks); - vkDestroyInstance(g_instance, g_allocation_callbacks); - } -} diff --git a/unit_test/src/draw/CMakeLists.txt b/unit_test/src/gui/CMakeLists.txt similarity index 100% rename from unit_test/src/draw/CMakeLists.txt rename to unit_test/src/gui/CMakeLists.txt diff --git a/unit_test/src/draw/common/glfw_callback_handler.cpp b/unit_test/src/gui/common/glfw_callback_handler.cpp similarity index 55% rename from unit_test/src/draw/common/glfw_callback_handler.cpp rename to unit_test/src/gui/common/glfw_callback_handler.cpp index 1b162b05..33c7ef69 100644 --- a/unit_test/src/draw/common/glfw_callback_handler.cpp +++ b/unit_test/src/gui/common/glfw_callback_handler.cpp @@ -4,6 +4,7 @@ #include #include +#include namespace { @@ -18,22 +19,22 @@ namespace GLFWcharfun g_glfw_callback_window_char; GLFWmonitorfun g_glfw_callback_window_monitor; - enum class MouseButton + enum class MouseButton : std::uint8_t { LEFT = GLFW_MOUSE_BUTTON_LEFT, RIGHT = GLFW_MOUSE_BUTTON_RIGHT, MIDDLE = GLFW_MOUSE_BUTTON_MIDDLE, - X1, - X2, + X1 = GLFW_MOUSE_BUTTON_4, + X2 = GLFW_MOUSE_BUTTON_5, }; - enum class MouseAction + enum class MouseAction : std::uint8_t { RELEASE = GLFW_RELEASE, PRESS = GLFW_PRESS, }; - enum class MouseMod + enum class MouseMod : std::uint8_t { NONE = 0, SHIFT = GLFW_MOD_SHIFT, @@ -44,7 +45,7 @@ namespace NUM_LOCK = GLFW_MOD_NUM_LOCK, }; - enum class KeyboardKeyCode + enum class KeyboardKeyCode : std::uint8_t { NUM_0 = GLFW_KEY_0, NUM_1 = GLFW_KEY_1, @@ -84,14 +85,14 @@ namespace Z = GLFW_KEY_Z, }; - enum class KeyboardAction + enum class KeyboardAction : std::uint8_t { RELEASE = GLFW_RELEASE, PRESS = GLFW_PRESS, REPEAT = GLFW_REPEAT, }; - enum class KeyboardMod + enum class KeyboardMod : std::uint8_t { NONE = 0, SHIFT = GLFW_MOD_SHIFT, @@ -139,135 +140,177 @@ struct meta::user_defined::enum_name_policy constexpr static auto value = EnumNamePolicy::WITH_SCOPED_NAME; }; -// io::DeviceEventQueue g_device_event_queue; +bool g_print_glfw_message = true; +bool g_print_glfw_message_focus = false; +bool g_print_glfw_message_cursor_enter = false; +bool g_print_glfw_message_cursor_position = false; +bool g_print_glfw_message_mouse_button = false; +bool g_print_glfw_message_scroll = false; +bool g_print_glfw_message_key = false; +bool g_print_glfw_message_char = false; +bool g_print_glfw_message_monitor = false; auto glfw_callback_setup(GLFWwindow& w) -> void { - static auto callback_window_focus = [](GLFWwindow* window, const int focused) + static auto callback_window_focus = [](GLFWwindow* window, const int focused) noexcept -> void { - std::println(stdout, "[FOCUS]: window: 0x{:x}, focused: {}", reinterpret_cast(window), focused != 0); + if (g_print_glfw_message and g_print_glfw_message_focus) + { + std::println(stdout, "[FOCUS]: window: 0x{:x}, focused: {}", reinterpret_cast(window), focused != 0); + } if (g_glfw_callback_window_focus) { g_glfw_callback_window_focus(window, focused); } }; - static auto callback_window_cursor_enter = [](GLFWwindow* window, const int entered) + static auto callback_window_cursor_enter = [](GLFWwindow* window, const int entered) noexcept -> void { - std::println( - stdout, - "[CURSOR]: window: 0x{:x}, entered: {}", - reinterpret_cast(window), - entered != 0 - ); + if (g_print_glfw_message and g_print_glfw_message_cursor_enter) + { + std::println( + stdout, + "[CURSOR]: window: 0x{:x}, entered: {}", + reinterpret_cast(window), + entered != 0 + ); + } if (g_glfw_callback_window_cursor_enter) { g_glfw_callback_window_cursor_enter(window, entered); } }; - static auto callback_window_cursor_position = [](GLFWwindow* window, const double x, const double y) + static auto callback_window_cursor_position = [](GLFWwindow* window, const double x, const double y) noexcept -> void { - std::println(stdout, "[CURSOR]: window: 0x{:x}, x: {}, y: {}", reinterpret_cast(window), x, y); + if (g_print_glfw_message and g_print_glfw_message_cursor_position) + { + std::println(stdout, "[CURSOR]: window: 0x{:x}, x: {}, y: {}", reinterpret_cast(window), x, y); + } + + auto& io = gui::get_io(); - // g_device_event_queue.mouse_move(static_cast(x), static_cast(y)); + io.mouse_position = {static_cast(x), static_cast(y)}; if (g_glfw_callback_window_cursor_position) { g_glfw_callback_window_cursor_position(window, x, y); } }; - static auto callback_window_mouse_button = [](GLFWwindow* window, const int button, const int action, const int mods) + static auto callback_window_mouse_button = [](GLFWwindow* window, const int button, const int action, const int mods) noexcept -> void { - std::println( - stdout, - "[MOUSE]: window: 0x{:x}, button: [{}], action: [{}], mods: [{}]", - reinterpret_cast(window), - meta::to_string(static_cast(button)), - meta::to_string(static_cast(action)), - meta::to_string(static_cast(mods)) - ); - - // const auto status = static_cast(action) == MouseAction::PRESS ? io::MouseButtonStatus::PRESS : io::MouseButtonStatus::RELEASE; - // switch (static_cast(button)) - // { - // case MouseButton::LEFT: - // { - // g_device_event_queue.mouse_button(io::MouseButton::LEFT, status); - // break; - // } - // case MouseButton::MIDDLE: - // { - // g_device_event_queue.mouse_button(io::MouseButton::MIDDLE, status); - // break; - // } - // case MouseButton::RIGHT: - // { - // g_device_event_queue.mouse_button(io::MouseButton::RIGHT, status); - // break; - // } - // case MouseButton::X1: - // { - // g_device_event_queue.mouse_button(io::MouseButton::X1, status); - // break; - // } - // case MouseButton::X2: - // { - // g_device_event_queue.mouse_button(io::MouseButton::X2, status); - // break; - // } - // default: - // { - // break; - // } - // } + if (g_print_glfw_message and g_print_glfw_message_mouse_button) + { + std::println( + stdout, + "[MOUSE]: window: 0x{:x}, button: [{}], action: [{}], mods: [{}]", + reinterpret_cast(window), + meta::to_string(static_cast(button)), + meta::to_string(static_cast(action)), + meta::to_string(static_cast(mods)) + ); + } + + auto& io = gui::get_io(); + const auto pressed = static_cast(action) == MouseAction::PRESS; + + switch (static_cast(button)) + { + case MouseButton::LEFT: + { + io.mouse_button_state[gui::MouseKey::LEFT] = pressed; + + break; + } + case MouseButton::MIDDLE: + { + io.mouse_button_state[gui::MouseKey::MIDDLE] = pressed; + + break; + } + case MouseButton::RIGHT: + { + io.mouse_button_state[gui::MouseKey::RIGHT] = pressed; + + break; + } + case MouseButton::X1: + { + io.mouse_button_state[gui::MouseKey::X1] = pressed; + + break; + } + case MouseButton::X2: + { + io.mouse_button_state[gui::MouseKey::X2] = pressed; + + break; + } + default: + { + break; + } + } if (g_glfw_callback_window_mouse_button) { g_glfw_callback_window_mouse_button(window, button, action, mods); } }; - static auto callback_window_scroll = [](GLFWwindow* window, const double x, const double y) + static auto callback_window_scroll = [](GLFWwindow* window, const double x, const double y) noexcept -> void { - std::println(stdout, "[MOUSE SCROLL]: window: 0x{:x}, x: {}, y: {}", reinterpret_cast(window), x, y); + if (g_print_glfw_message and g_print_glfw_message_scroll) + { + std::println(stdout, "[MOUSE SCROLL]: window: 0x{:x}, x: {}, y: {}", reinterpret_cast(window), x, y); + } - // g_device_event_queue.mouse_wheel(static_cast(x), static_cast(y)); + auto& io = gui::get_io(); + io.mouse_wheel = static_cast(y); if (g_glfw_callback_window_scroll) { g_glfw_callback_window_scroll(window, x, y); } }; - static auto callback_window_key = [](GLFWwindow* window, const int key_code, const int scan_code, const int action, const int mods) + static auto callback_window_key = [](GLFWwindow* window, const int key_code, const int scan_code, const int action, const int mods) noexcept -> void { - std::println( - stdout, - "[KEYBOARD]: window: 0x{:x}, key_code: [{}]({}), scan_code: {}, action: {}, mods: {}", - reinterpret_cast(window), - meta::to_string(static_cast(key_code)), - key_code, - scan_code, - meta::to_string(static_cast(action)), - meta::to_string(static_cast(mods)) - ); + if (g_print_glfw_message and g_print_glfw_message_key) + { + std::println( + stdout, + "[KEYBOARD]: window: 0x{:x}, key_code: [{}]({}), scan_code: {}, action: {}, mods: {}", + reinterpret_cast(window), + meta::to_string(static_cast(key_code)), + key_code, + scan_code, + meta::to_string(static_cast(action)), + meta::to_string(static_cast(mods)) + ); + } if (g_glfw_callback_window_key) { g_glfw_callback_window_key(window, key_code, scan_code, action, mods); } }; - static auto callback_window_char = [](GLFWwindow* window, const unsigned int codepoint) + static auto callback_window_char = [](GLFWwindow* window, const unsigned int codepoint) noexcept -> void { - std::println(stdout, "[KEYBOARD]: window: 0x{:x}, codepoint: 0x{:x}", reinterpret_cast(window), codepoint); + if (g_print_glfw_message and g_print_glfw_message_char) + { + std::println(stdout, "[KEYBOARD]: window: 0x{:x}, codepoint: 0x{:x}", reinterpret_cast(window), codepoint); + } if (g_glfw_callback_window_char) { g_glfw_callback_window_char(window, codepoint); } }; - static auto callback_window_monitor = [](GLFWmonitor* monitor, const int event) + static auto callback_window_monitor = [](GLFWmonitor* monitor, const int event) noexcept -> void { - std::println(stdout, "[MONITOR]: monitor: 0x{:x}, event: {}", reinterpret_cast(monitor), event); + if (g_print_glfw_message and g_print_glfw_message_monitor) + { + std::println(stdout, "[MONITOR]: monitor: 0x{:x}, event: {}", reinterpret_cast(monitor), event); + } if (g_glfw_callback_window_monitor) { diff --git a/unit_test/src/draw/common/print_time.hpp b/unit_test/src/gui/common/print_time.hpp similarity index 100% rename from unit_test/src/draw/common/print_time.hpp rename to unit_test/src/gui/common/print_time.hpp diff --git a/unit_test/src/draw/dx11/CMakeLists.txt b/unit_test/src/gui/dx11/CMakeLists.txt similarity index 96% rename from unit_test/src/draw/dx11/CMakeLists.txt rename to unit_test/src/gui/dx11/CMakeLists.txt index 6594cbea..66d6c2dc 100644 --- a/unit_test/src/draw/dx11/CMakeLists.txt +++ b/unit_test/src/gui/dx11/CMakeLists.txt @@ -1,5 +1,5 @@ project( - prometheus-draw-dx11 + prometheus-gui-dx11 LANGUAGES CXX ) diff --git a/unit_test/src/draw/dx11/backend.cpp b/unit_test/src/gui/dx11/backend.cpp similarity index 64% rename from unit_test/src/draw/dx11/backend.cpp rename to unit_test/src/gui/dx11/backend.cpp index 26258877..91dbdbae 100644 --- a/unit_test/src/draw/dx11/backend.cpp +++ b/unit_test/src/gui/dx11/backend.cpp @@ -1,6 +1,5 @@ #include "../win/def.hpp" #include "../common/print_time.hpp" - #include #include #include @@ -24,8 +23,6 @@ extern double g_last_time; extern std::uint64_t g_frame_count; extern float g_fps; -// extern io::DeviceEventQueue g_device_event_queue; - namespace { struct render_buffer_type @@ -53,8 +50,6 @@ namespace ComPtr g_additional_picture_texture = nullptr; - draw::Window g_window{"Window 1", {100, 100, 640, 480}}; - [[nodiscard]] auto load_texture( const std::uint8_t* texture_data, const std::uint32_t texture_width, @@ -109,16 +104,6 @@ auto prometheus_init() -> void // { print_time(); - const auto glyph_range = i18n::RangeBuilder{}.simplified_chinese_common().range(); - const auto font_option = - draw::Font::option() - .path(R"(C:\Windows\Fonts\msyh.ttc)") - .glyph_ranges(i18n::RangeBuilder{}.simplified_chinese_common().range()) - .pixel_height(18); - - auto font = std::make_shared(); - auto font_texture = font->load(font_option); - // Create the blending setup { constexpr D3D11_RENDER_TARGET_BLEND_DESC render_target{ @@ -376,17 +361,29 @@ auto prometheus_init() -> void // check_hr_error(g_device->CreateSamplerState(&sampler_desc, g_font_sampler.ReleaseAndGetAddressOf())); } + gui::create_current_context(); + + set_default_theme(gui::test_theme()); + set_default_draw_list_flag(gui::DrawListFlag::ANTI_ALIASED_LINE | gui::DrawListFlag::ANTI_ALIASED_LINE_USE_TEXTURE | gui::DrawListFlag::ANTI_ALIASED_FILL); + + gui::FontOption font_option{}; + font_option.font_path = R"(C:\Windows\Fonts\msyh.ttc)"; + font_option.pixel_height = 18; + font_option.glyph_ranges = i18n::RangeBuilder{}.simplified_chinese_common().range(); + auto font_texture = set_default_font(font_option); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(font_texture.valid()); + // Load default font texture { [[maybe_unused]] const auto load_font_texture_result = load_texture( - reinterpret_cast(font_texture.data().get()), - font_texture.width(), - font_texture.height(), + reinterpret_cast(font_texture.data.get()), + font_texture.width, + font_texture.height, g_font_texture ); assert(load_font_texture_result); - font_texture.bind(reinterpret_cast(g_font_texture.Get())); + font_texture.bind(reinterpret_cast(g_font_texture.Get())); } // Load additional picture texture @@ -407,157 +404,222 @@ auto prometheus_init() -> void // stbi_image_free(data); } - - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(font->loaded()); - - auto& context = draw::Context::instance(); - auto& draw_list = g_window.test_draw_list(); - - context.set_default_font(font); - context.test_set_window(g_window); - - draw_list.draw_list_flag(draw::DrawListFlag::ANTI_ALIASED_LINE | draw::DrawListFlag::ANTI_ALIASED_LINE_USE_TEXTURE | draw::DrawListFlag::ANTI_ALIASED_FILL); - draw_list.reset(); - - g_window.test_init(); } auto prometheus_new_frame() -> void // { - auto& draw_list = g_window.test_draw_list(); + auto& io = gui::get_io(); + + io.display_size = {static_cast(g_window_width), static_cast(g_window_height)}; + io.delta_time = 1.f / g_fps; - draw_list.reset(); - draw_list.push_clip_rect({0, 0}, {static_cast(g_window_width), static_cast(g_window_height)}, false); + gui::new_frame(); } auto prometheus_render() -> void { - g_window.test_init(); - if (g_window.draw_button("Button")) + static auto test_string = [] { - std::println(stdout, "Button!"); - } + std::string string{}; + string.resize(200); + for (int i = 0; i < 200; ++i) + { + if (i != 0 and i % 25 == 0) + { + string[i] = '\n'; + } + else + { + const auto c = 'a' + i % ('z' - 'a'); + string[i] = static_cast(c); + } + } + return string; + }(); - g_window.draw_text("Hello1"); - g_window.draw_text("World1"); + if (static bool window_closed = false; + not window_closed) + { + window_closed = gui::begin_window("Window 1", {640, 480}); - g_window.draw_text("Hello2"); - g_window.layout_same_line(); - g_window.draw_text("World2"); + gui::draw_text(std::format("FPS: {:.3}", g_fps)); - { - static bool checked = false; - if (const auto new_status = g_window.draw_checkbox("Checkbox1", checked); - new_status != checked) + static bool theme_window_closed = true; + theme_window_closed ^= gui::draw_button("OpenThemeEditor"); + + if (not theme_window_closed) { - checked = new_status; - std::println(stdout, "Checkbox1!"); + gui::set_next_window_point({100, 100}); + theme_window_closed = gui::show_theme_editor(); } - } - { - static bool checked = true; - if (const auto new_status = g_window.draw_checkbox("Checkbox2", checked); - new_status != checked) + + gui::draw_text_colored("Text:", primitive::colors::red); + { + gui::draw_text("Hello"); + gui::draw_text("World"); + + gui::draw_text("你好,"); + gui::layout_same_line(); + gui::draw_text("世界"); + + gui::push_text_wrap_width(150); + gui::draw_text(test_string); + gui::pop_text_wrap_width(); + + gui::push_text_wrap_width(300); + gui::draw_text(test_string); + gui::pop_text_wrap_width(); + } + + gui::draw_text_colored("Button:", primitive::colors::red); + { + if (gui::draw_button("Button")) + { + std::println(stdout, "Press Button!"); + } + if (gui::draw_small_button("SmallButton")) + { + std::println(stdout, "Press SmallButton!"); + } + } + + gui::draw_text_colored("RadioButton:", primitive::colors::red); + { + struct radio_button + { + int value; + + [[nodiscard]] constexpr auto operator==(const radio_button& other) const noexcept -> bool + { + return value == other.value; + } + }; + static radio_button value{.value = 0}; + + gui::draw_radio_button("RadioButton1", value, radio_button{.value = 0}); + gui::layout_same_line(); + gui::draw_radio_button("RadioButton2", value, radio_button{.value = 1}); + gui::layout_same_line(); + gui::draw_radio_button("RadioButton3", value, radio_button{.value = 2}); + gui::layout_same_line(); + gui::draw_text(std::format("> Select: {}", value.value)); + } + + gui::draw_text_colored("Checkbox:", primitive::colors::red); + { + struct checkbox + { + int value; + + [[nodiscard]] constexpr auto operator==(const checkbox& other) const noexcept -> bool + { + return value == other.value; + } + }; + static checkbox value{.value = 0}; + + gui::draw_checkbox("Checkbox", value, checkbox{.value = 1}, checkbox{.value = 0}); + gui::layout_same_line(); + gui::draw_text(std::format("> Select: {}", value.value)); + } + + gui::draw_text_colored("Slider", primitive::colors::red); { - checked = new_status; - std::println(stdout, "Checkbox2!"); + static bool open_slider = false; + open_slider ^= gui::draw_button("OpenSlider"); + + if (open_slider) + { + gui::begin_window("SliderWindow"); + + gui::draw_text(test_string); + + static bool border = false; + border ^= gui::draw_button("border"); + + gui::begin_child_window("SliderWindowChild", {}, border); + + static std::array v{0, 0, 0, 0, 0}; + + gui::draw_slider("Slider1", v[0], -1, 1); + gui::draw_slider_n("Slider1(n)", {v.begin(), v.begin() + 1}, -1, 1); + gui::draw_slider_n("Slider2", {v.begin(), v.begin() + 2}, -1, 1); + gui::draw_slider_n("Slider3", {v.begin(), v.begin() + 3}, -1, 1); + gui::draw_slider_n("Slider4", {v.begin(), v.begin() + 4}, -1, 1); + gui::draw_slider_n("Slider5", {v.begin(), v.begin() + 5}, -1, 1); + + gui::end_child_window(); + + gui::end_window(); + } } + + gui::draw_text_colored("Tooltip", primitive::colors::red); + { + gui::draw_text("Hover me"); + if (gui::is_item_hovered()) + { + gui::begin_tooltip_window(); + + gui::draw_text("You found me!"); + + gui::end_tooltip_window(); + } + } + + gui::draw_text_colored("Combo", primitive::colors::red); + { + static std::array selections{"selection1", "selection2", "selection3", "selection4", "selection5", "selection6", "selection7", "selection8", "selection9"}; + static std::size_t selected = 8; + + gui::draw_combo("Combo", selections, selected); + } + + gui::end_window(); } - // auto& draw_list = g_window.test_draw_list(); - - // draw_list.text(24.f, {10, 10}, primitive::colors::blue, std::format("FPS: {:.3f}", g_fps)); - // - // draw_list.text(18.f, {50, 50}, primitive::colors::red, "The quick brown fox jumps over the lazy dog.\nHello world!\n你好世界!\n"); - - // draw_list.line({200, 100}, {200, 300}, primitive::colors::red); - // draw_list.line({100, 200}, {300, 200}, primitive::colors::red); - // - // draw_list.rect({100, 100}, {300, 300}, primitive::colors::blue); - // draw_list.rect({150, 150}, {250, 250}, primitive::colors::blue, 30); - // - // draw_list.triangle({120, 120}, {120, 150}, {150, 120}, primitive::colors::green); - // draw_list.triangle_filled({130, 130}, {130, 150}, {150, 130}, primitive::colors::red); - // - // draw_list.rect_filled({300, 100}, {400, 200}, primitive::colors::pink); - // draw_list.rect_filled({300, 200}, {400, 300}, primitive::colors::pink, 20); - // draw_list.rect_filled({300, 300}, {400, 400}, primitive::colors::pink, primitive::colors::gold, primitive::colors::azure, primitive::colors::lavender); - // - // draw_list.quadrilateral({100, 500}, {200, 500}, {250, 550}, {50, 550}, primitive::colors::red); - // draw_list.quadrilateral_filled({100, 500}, {200, 500}, {250, 450}, {50, 450}, primitive::colors::red); - // - // draw_list.circle({100, 600}, 50, primitive::colors::green); - // draw_list.circle({200, 600}, 50, primitive::colors::red, 8); - // draw_list.circle_filled({100, 700}, 50, primitive::colors::green); - // draw_list.circle_filled({200, 700}, 50, primitive::colors::red, 8); - // - // draw_list.ellipse({500, 100}, {50, 70}, std::numbers::pi_v * .35f, primitive::colors::red, 8); - // draw_list.ellipse_filled({500, 200}, {50, 70}, std::numbers::pi_v * -.35f, primitive::colors::red, 8); - // draw_list.ellipse({600, 100}, {50, 70}, std::numbers::pi_v * .35f, primitive::colors::red, 16); - // draw_list.ellipse_filled({600, 200}, {50, 70}, std::numbers::pi_v * -.35f, primitive::colors::red, 16); - // draw_list.ellipse({700, 100}, {50, 70}, std::numbers::pi_v * .35f, primitive::colors::red, 24); - // draw_list.ellipse_filled({700, 200}, {50, 70}, std::numbers::pi_v * -.35f, primitive::colors::red, 24); - // draw_list.ellipse({800, 100}, {50, 70}, std::numbers::pi_v * .35f, primitive::colors::red); - // draw_list.ellipse_filled({800, 200}, {50, 70}, std::numbers::pi_v * -.35f, primitive::colors::red); - // - // draw_list.circle_filled({500, 300}, 5, primitive::colors::red); - // draw_list.circle_filled({600, 350}, 5, primitive::colors::red); - // draw_list.circle_filled({450, 500}, 5, primitive::colors::red); - // draw_list.circle_filled({550, 550}, 5, primitive::colors::red); - // draw_list.bezier_cubic({500, 300}, {600, 350}, {450, 500}, {550, 550}, primitive::colors::green); - // - // draw_list.circle_filled({600, 300}, 5, primitive::colors::red); - // draw_list.circle_filled({700, 350}, 5, primitive::colors::red); - // draw_list.circle_filled({550, 500}, 5, primitive::colors::red); - // draw_list.circle_filled({650, 550}, 5, primitive::colors::red); - // draw_list.bezier_cubic({600, 300}, {700, 350}, {550, 500}, {650, 550}, primitive::colors::green, 5); - // - // draw_list.circle_filled({500, 600}, 5, primitive::colors::red); - // draw_list.circle_filled({600, 650}, 5, primitive::colors::red); - // draw_list.circle_filled({450, 800}, 5, primitive::colors::red); - // draw_list.bezier_quadratic({500, 600}, {600, 650}, {450, 800}, primitive::colors::green); - // - // draw_list.circle_filled({600, 600}, 5, primitive::colors::red); - // draw_list.circle_filled({700, 650}, 5, primitive::colors::red); - // draw_list.circle_filled({550, 800}, 5, primitive::colors::red); - // draw_list.bezier_quadratic({600, 600}, {700, 650}, {550, 800}, primitive::colors::green, 5); - // - // // push bound - // // [800,350] => [1000, 550] (200 x 200) - // draw_list.push_clip_rect({800, 350}, {1000, 550}, true); - // draw_list.rect({800, 350}, {1000, 550}, primitive::colors::red); - // // out-of-bound - // draw_list.triangle_filled({700, 250}, {900, 400}, {850, 450}, primitive::colors::green); - // // in-bound - // draw_list.triangle_filled({900, 450}, {1000, 450}, {950, 550}, primitive::colors::blue); - // // pop bound - // draw_list.pop_clip_rect(); - // - // draw_list.triangle_filled({800, 450}, {700, 750}, {850, 800}, primitive::colors::gold); - // - // // font - // draw_list.image(draw::Context::instance().font().texture_id(), {900, 20, 300, 300}); - // // image - // draw_list.image_rounded(reinterpret_cast(g_additional_picture_texture.Get()), {900, 350, 300, 300}, 10); + gui::render(); } auto prometheus_draw() -> void { - auto& draw_list = g_window.test_draw_list(); + const auto& draw_datas = gui::get_draw_data(); auto& [this_frame_index_buffer, this_frame_index_count, this_frame_vertex_buffer, this_frame_vertex_count] = render_buffer; - const auto command_list = draw_list.command_list(); - const auto vertex_list = draw_list.vertex_list(); - const auto index_list = draw_list.index_list(); + const auto [total_vertex_count, total_index_count] = [&]() noexcept + { + struct sum + { + UINT vertex; + UINT index; + }; + + return std::ranges::fold_left( + draw_datas, + sum{.vertex = 0, .index = 0}, + [](const sum s, const gui::DrawData& draw_data) noexcept -> sum + { + const auto vertex_list = draw_data.vertex_list.get(); + const auto index_list = draw_data.index_list.get(); - if (not this_frame_vertex_buffer or vertex_list.size() > this_frame_vertex_count) + return + { + .vertex = s.vertex + static_cast(vertex_list.size()), + .index = s.index + static_cast(index_list.size()) + }; + } + ); + }(); + + if (not this_frame_vertex_buffer or total_vertex_count > this_frame_vertex_count) { // todo: grow factor - this_frame_vertex_count = static_cast(vertex_list.size()) + 5000; + this_frame_vertex_count = total_vertex_count + 5000; - const D3D11_BUFFER_DESC buffer_desc{ - .ByteWidth = static_cast(this_frame_vertex_count * sizeof(draw::DrawList::vertex_type)), + const D3D11_BUFFER_DESC buffer_desc + { + .ByteWidth = static_cast(this_frame_vertex_count * sizeof(gui::vertex_type)), .Usage = D3D11_USAGE_DYNAMIC, .BindFlags = D3D11_BIND_VERTEX_BUFFER, .CPUAccessFlags = D3D11_CPU_ACCESS_WRITE, @@ -566,13 +628,14 @@ auto prometheus_draw() -> void }; check_hr_error(g_device->CreateBuffer(&buffer_desc, nullptr, this_frame_vertex_buffer.ReleaseAndGetAddressOf())); } - if (not this_frame_index_buffer or index_list.size() > this_frame_index_count) + if (not this_frame_index_buffer or total_index_count > this_frame_index_count) { // todo: grow factor - this_frame_index_count = static_cast(index_list.size()) + 10000; + this_frame_index_count = total_index_count + 10000; - const D3D11_BUFFER_DESC buffer_desc{ - .ByteWidth = static_cast(this_frame_index_count * sizeof(draw::DrawList::index_type)), + const D3D11_BUFFER_DESC buffer_desc + { + .ByteWidth = static_cast(this_frame_index_count * sizeof(gui::index_type)), .Usage = D3D11_USAGE_DYNAMIC, .BindFlags = D3D11_BIND_INDEX_BUFFER, .CPUAccessFlags = D3D11_CPU_ACCESS_WRITE, @@ -592,20 +655,42 @@ auto prometheus_draw() -> void auto* mapped_vertex = static_cast(mapped_vertex_resource.pData); auto* mapped_index = static_cast(mapped_index_resource.pData); - std::ranges::transform( - vertex_list, - mapped_vertex, - [](const draw::DrawList::vertex_type& vertex) -> d3d_vertex_type + UINT vertex_offset = 0; + UINT index_offset = 0; + + std::ranges::for_each( + draw_datas, + [&](const gui::DrawData& draw_data) noexcept -> void { - // return { - // .position = {vertex.position.x, vertex.position.y}, - // .uv = {vertex.uv.x, vertex.uv.y}, - // .color = vertex.color.to(primitive::color_format) - // }; - return std::bit_cast(vertex); + const auto vertex_list = draw_data.vertex_list.get(); + const auto index_list = draw_data.index_list.get(); + + std::ranges::transform( + vertex_list, + mapped_vertex + vertex_offset, + [](const gui::vertex_type& vertex) noexcept -> d3d_vertex_type + { + // return { + // .position = {vertex.position.x, vertex.position.y}, + // .uv = {vertex.uv.x, vertex.uv.y}, + // .color = vertex.color.to(primitive::color_format) + // }; + return std::bit_cast(vertex); + } + ); + std::ranges::transform( + index_list, + mapped_index + index_offset, + [vertex_offset](const gui::index_type index) noexcept -> d3d_index_type + { + return static_cast(index + vertex_offset); + } + ); + + vertex_offset += static_cast(vertex_list.size()); + index_offset += static_cast(index_list.size()); } ); - std::ranges::copy(index_list, mapped_index); g_device_immediate_context->Unmap(this_frame_vertex_buffer.Get(), 0); g_device_immediate_context->Unmap(this_frame_index_buffer.Get(), 0); @@ -674,22 +759,44 @@ auto prometheus_draw() -> void g_device_immediate_context->OMSetDepthStencilState(g_depth_stencil_state.Get(), 0); g_device_immediate_context->RSSetState(g_rasterizer_state.Get()); - for (const auto& [clip_rect, texture, index_offset, element_count]: command_list) - { - const auto [point, extent] = clip_rect; - const D3D11_RECT rect{static_cast(point.x), static_cast(point.y), static_cast(point.x + extent.width), static_cast(point.y + extent.height)}; - g_device_immediate_context->RSSetScissorRects(1, &rect); + UINT total_index_offset = 0; + std::ranges::for_each( + draw_datas, + [&total_index_offset](const gui::DrawData& draw_data) noexcept -> void + { + const auto vertex_list = draw_data.vertex_list.get(); + const auto index_list = draw_data.index_list.get(); - assert(texture != 0 and "push_texture_id when create texture view"); - ID3D11ShaderResourceView* textures[]{reinterpret_cast(texture)}; // NOLINT(performance-no-int-to-ptr) - g_device_immediate_context->PSSetShaderResources(0, 1, textures); + for (const auto& command_list = draw_data.command_list.get(); + const auto& [clip_rect, texture, index_offset, element_count]: command_list) + { + const auto [point, extent] = clip_rect; + const D3D11_RECT rect + { + static_cast(point.x), + static_cast(point.y), + static_cast(point.x + extent.width), + static_cast(point.y + extent.height) + }; + g_device_immediate_context->RSSetScissorRects(1, &rect); + + assert(texture != 0 and "push_texture_id when create texture view"); + ID3D11ShaderResourceView* textures[]{reinterpret_cast(texture)}; // NOLINT(performance-no-int-to-ptr) + g_device_immediate_context->PSSetShaderResources(0, 1, textures); + + const auto this_index_offset = static_cast(total_index_offset + index_offset); + // g_device_immediate_context->DrawIndexed(static_cast(element_count), this_index_offset, 0); + g_device_immediate_context->DrawIndexedInstanced(static_cast(element_count), 1, this_index_offset, 0, 0); + } - // g_device_immediate_context->DrawIndexed(static_cast(element_count), static_cast(index_offset), 0); - g_device_immediate_context->DrawIndexedInstanced(static_cast(element_count), 1, static_cast(index_offset), 0, 0); - } + total_index_offset += static_cast(index_list.size()); + } + ); } auto prometheus_shutdown() -> void { print_time(); + + gui::destroy_current_context(); } diff --git a/unit_test/src/draw/dx11/main.cpp b/unit_test/src/gui/dx11/main.cpp similarity index 99% rename from unit_test/src/draw/dx11/main.cpp rename to unit_test/src/gui/dx11/main.cpp index c68ea904..668c6641 100644 --- a/unit_test/src/draw/dx11/main.cpp +++ b/unit_test/src/gui/dx11/main.cpp @@ -25,7 +25,7 @@ int g_window_height = 960; double g_last_time = 0; std::uint64_t g_frame_count = 0; -float g_fps = 0; +float g_fps = 60; extern auto glfw_callback_setup(GLFWwindow& w) -> void; diff --git a/unit_test/src/draw/dx12/CMakeLists.txt b/unit_test/src/gui/dx12/CMakeLists.txt similarity index 96% rename from unit_test/src/draw/dx12/CMakeLists.txt rename to unit_test/src/gui/dx12/CMakeLists.txt index 4741bfcf..1603684c 100644 --- a/unit_test/src/draw/dx12/CMakeLists.txt +++ b/unit_test/src/gui/dx12/CMakeLists.txt @@ -1,5 +1,5 @@ project( - prometheus-draw-dx12 + prometheus-gui-dx12 LANGUAGES CXX ) diff --git a/unit_test/src/draw/dx12/backend.cpp b/unit_test/src/gui/dx12/backend.cpp similarity index 76% rename from unit_test/src/draw/dx12/backend.cpp rename to unit_test/src/gui/dx12/backend.cpp index 928c5d34..40f1cc7f 100644 --- a/unit_test/src/draw/dx12/backend.cpp +++ b/unit_test/src/gui/dx12/backend.cpp @@ -26,8 +26,6 @@ extern double g_last_time; extern std::uint64_t g_frame_count; extern float g_fps; -// extern io::DeviceEventQueue g_device_event_queue; - namespace { struct render_buffer_type @@ -57,8 +55,6 @@ namespace ComPtr g_additional_picture_resource = nullptr; D3D12_GPU_DESCRIPTOR_HANDLE g_additional_picture_handle = {.ptr = 0}; - draw::DrawList g_draw_list; - [[nodiscard]] auto load_texture( const std::uint8_t* texture_data, const std::uint32_t texture_width, @@ -277,20 +273,6 @@ auto prometheus_init() -> void { print_time(); - const auto glyph_range = i18n::RangeBuilder{}.simplified_chinese_common().range(); - const auto font_option = - draw::Font::option() - .path(R"(C:\Windows\Fonts\msyh.ttc)") - .glyph_ranges(i18n::RangeBuilder{}.simplified_chinese_common().range()) - .pixel_height(18); - - auto font = std::make_shared(); - auto font_texture = font->load(font_option); - - draw::Context::instance().set_default_font(font); - - g_draw_list.draw_list_flag(draw::DrawListFlag::ANTI_ALIASED_LINE | draw::DrawListFlag::ANTI_ALIASED_LINE_USE_TEXTURE | draw::DrawListFlag::ANTI_ALIASED_FILL); - // Create the root signature { // [0] projection_matrix @@ -566,12 +548,24 @@ auto prometheus_init() -> void check_hr_error(g_device->CreateDescriptorHeap(&desc, IID_PPV_ARGS(g_shader_resource_view_descriptor_heap.GetAddressOf()))); } + gui::create_current_context(); + + set_default_theme(gui::test_theme()); + set_default_draw_list_flag(gui::DrawListFlag::ANTI_ALIASED_LINE | gui::DrawListFlag::ANTI_ALIASED_LINE_USE_TEXTURE | gui::DrawListFlag::ANTI_ALIASED_FILL); + + gui::FontOption font_option{}; + font_option.font_path = R"(C:\Windows\Fonts\msyh.ttc)"; + font_option.pixel_height = 18; + font_option.glyph_ranges = i18n::RangeBuilder{}.simplified_chinese_common().range(); + auto font_texture = set_default_font(font_option); + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(font_texture.valid()); + // Load default font texture { [[maybe_unused]] const auto load_font_texture_result = load_texture( - reinterpret_cast(font_texture.data().get()), - font_texture.width(), - font_texture.height(), + reinterpret_cast(font_texture.data.get()), + font_texture.width, + font_texture.height, g_shader_resource_view_descriptor_heap, 0, g_font_handle, @@ -579,7 +573,7 @@ auto prometheus_init() -> void ); assert(load_font_texture_result); - font_texture.bind(static_cast(g_font_handle.ptr)); + font_texture.bind(static_cast(g_font_handle.ptr)); } // Load additional picture texture @@ -607,85 +601,150 @@ auto prometheus_init() -> void auto prometheus_new_frame() -> void // { - g_draw_list.reset(); - g_draw_list.push_clip_rect({0, 0}, {static_cast(g_window_width), static_cast(g_window_height)}, false); + auto& io = gui::get_io(); + + io.display_size = {static_cast(g_window_width), static_cast(g_window_height)}; + io.delta_time = 1.f / g_fps; + + gui::new_frame(); } auto prometheus_render() -> void { - g_draw_list.text(24.f, {10, 10}, primitive::colors::blue, std::format("FPS: {:.3f}", g_fps)); - - g_draw_list.text(18.f, {50, 50}, primitive::colors::red, "The quick brown fox jumps over the lazy dog.\nHello world!\n你好世界!\n"); - - g_draw_list.line({200, 100}, {200, 300}, primitive::colors::red); - g_draw_list.line({100, 200}, {300, 200}, primitive::colors::red); - - g_draw_list.rect({100, 100}, {300, 300}, primitive::colors::blue); - g_draw_list.rect({150, 150}, {250, 250}, primitive::colors::blue, 30); - - g_draw_list.triangle({120, 120}, {120, 150}, {150, 120}, primitive::colors::green); - g_draw_list.triangle_filled({130, 130}, {130, 150}, {150, 130}, primitive::colors::red); - - g_draw_list.rect_filled({300, 100}, {400, 200}, primitive::colors::pink); - g_draw_list.rect_filled({300, 200}, {400, 300}, primitive::colors::pink, 20); - g_draw_list.rect_filled({300, 300}, {400, 400}, primitive::colors::pink, primitive::colors::gold, primitive::colors::azure, primitive::colors::lavender); - - g_draw_list.quadrilateral({100, 500}, {200, 500}, {250, 550}, {50, 550}, primitive::colors::red); - g_draw_list.quadrilateral_filled({100, 500}, {200, 500}, {250, 450}, {50, 450}, primitive::colors::red); - - g_draw_list.circle({100, 600}, 50, primitive::colors::green); - g_draw_list.circle({200, 600}, 50, primitive::colors::red, 8); - g_draw_list.circle_filled({100, 700}, 50, primitive::colors::green); - g_draw_list.circle_filled({200, 700}, 50, primitive::colors::red, 8); - - g_draw_list.ellipse({500, 100}, {50, 70}, std::numbers::pi_v * .35f, primitive::colors::red, 8); - g_draw_list.ellipse_filled({500, 200}, {50, 70}, std::numbers::pi_v * -.35f, primitive::colors::red, 8); - g_draw_list.ellipse({600, 100}, {50, 70}, std::numbers::pi_v * .35f, primitive::colors::red, 16); - g_draw_list.ellipse_filled({600, 200}, {50, 70}, std::numbers::pi_v * -.35f, primitive::colors::red, 16); - g_draw_list.ellipse({700, 100}, {50, 70}, std::numbers::pi_v * .35f, primitive::colors::red, 24); - g_draw_list.ellipse_filled({700, 200}, {50, 70}, std::numbers::pi_v * -.35f, primitive::colors::red, 24); - g_draw_list.ellipse({800, 100}, {50, 70}, std::numbers::pi_v * .35f, primitive::colors::red); - g_draw_list.ellipse_filled({800, 200}, {50, 70}, std::numbers::pi_v * -.35f, primitive::colors::red); - - g_draw_list.circle_filled({500, 300}, 5, primitive::colors::red); - g_draw_list.circle_filled({600, 350}, 5, primitive::colors::red); - g_draw_list.circle_filled({450, 500}, 5, primitive::colors::red); - g_draw_list.circle_filled({550, 550}, 5, primitive::colors::red); - g_draw_list.bezier_cubic({500, 300}, {600, 350}, {450, 500}, {550, 550}, primitive::colors::green); - - g_draw_list.circle_filled({600, 300}, 5, primitive::colors::red); - g_draw_list.circle_filled({700, 350}, 5, primitive::colors::red); - g_draw_list.circle_filled({550, 500}, 5, primitive::colors::red); - g_draw_list.circle_filled({650, 550}, 5, primitive::colors::red); - g_draw_list.bezier_cubic({600, 300}, {700, 350}, {550, 500}, {650, 550}, primitive::colors::green, 5); - - g_draw_list.circle_filled({500, 600}, 5, primitive::colors::red); - g_draw_list.circle_filled({600, 650}, 5, primitive::colors::red); - g_draw_list.circle_filled({450, 800}, 5, primitive::colors::red); - g_draw_list.bezier_quadratic({500, 600}, {600, 650}, {450, 800}, primitive::colors::green); - - g_draw_list.circle_filled({600, 600}, 5, primitive::colors::red); - g_draw_list.circle_filled({700, 650}, 5, primitive::colors::red); - g_draw_list.circle_filled({550, 800}, 5, primitive::colors::red); - g_draw_list.bezier_quadratic({600, 600}, {700, 650}, {550, 800}, primitive::colors::green, 5); - - // push bound - // [800,350] => [1000, 550] (200 x 200) - g_draw_list.push_clip_rect({800, 350}, {1000, 550}, true); - g_draw_list.rect({800, 350}, {1000, 550}, primitive::colors::red); - // out-of-bound - g_draw_list.triangle_filled({700, 250}, {900, 400}, {850, 450}, primitive::colors::green); - // in-bound - g_draw_list.triangle_filled({900, 450}, {1000, 450}, {950, 550}, primitive::colors::blue); - // pop bound - g_draw_list.pop_clip_rect(); - - g_draw_list.triangle_filled({800, 450}, {700, 750}, {850, 800}, primitive::colors::gold); - - // font - g_draw_list.image(draw::Context::instance().font().texture_id(), {900, 20, 300, 300}); - // image - g_draw_list.image_rounded(static_cast(g_additional_picture_handle.ptr), {900, 350, 300, 300}, 10); + static auto test_string = [] + { + std::string string{}; + string.resize(200); + for (int i = 0; i < 200; ++i) + { + if (i != 0 and i % 25 == 0) + { + string[i] = '\n'; + } + else + { + const auto c = 'a' + i % ('z' - 'a'); + string[i] = static_cast(c); + } + } + return string; + }(); + + if (static bool window_closed = false; + not window_closed) + { + window_closed = gui::begin_window("Window 1", {640, 480}); + + static bool theme_window_closed = true; + theme_window_closed ^= gui::draw_button("OpenThemeEditor"); + + if (not theme_window_closed) + { + theme_window_closed = gui::show_theme_editor(); + } + + gui::draw_text_colored("Text:", primitive::colors::red); + { + gui::draw_text("Hello"); + gui::draw_text("World"); + + gui::draw_text("你好,"); + gui::layout_same_line(); + gui::draw_text("世界"); + + gui::push_text_wrap_width(150); + gui::draw_text(test_string); + gui::pop_text_wrap_width(); + + gui::push_text_wrap_width(300); + gui::draw_text(test_string); + gui::pop_text_wrap_width(); + } + + gui::draw_text_colored("Button:", primitive::colors::red); + { + if (gui::draw_button("Button")) + { + std::println(stdout, "Press Button!"); + } + if (gui::draw_small_button("SmallButton")) + { + std::println(stdout, "Press SmallButton!"); + } + } + + gui::draw_text_colored("RadioButton:", primitive::colors::red); + { + struct radio_button + { + int value; + + [[nodiscard]] constexpr auto operator==(const radio_button& other) const noexcept -> bool + { + return value == other.value; + } + }; + static radio_button value{.value = 0}; + + gui::draw_radio_button("RadioButton1", value, radio_button{.value = 0}); + gui::layout_same_line(); + gui::draw_radio_button("RadioButton2", value, radio_button{.value = 1}); + gui::layout_same_line(); + gui::draw_radio_button("RadioButton3", value, radio_button{.value = 2}); + gui::layout_same_line(); + gui::draw_text(std::format("> Select: {}", value.value)); + } + + gui::draw_text_colored("Checkbox:", primitive::colors::red); + { + struct checkbox + { + int value; + + [[nodiscard]] constexpr auto operator==(const checkbox& other) const noexcept -> bool + { + return value == other.value; + } + }; + static checkbox value{.value = 0}; + + gui::draw_checkbox("Checkbox", value, checkbox{.value = 1}, checkbox{.value = 0}); + gui::layout_same_line(); + gui::draw_text(std::format("> Select: {}", value.value)); + } + + gui::draw_text_colored("Slider", primitive::colors::red); + { + static bool open_slider = false; + open_slider ^= gui::draw_button("OpenSlider"); + + if (open_slider) + { + gui::begin_window("SliderWindow"); + + gui::draw_text(test_string); + + gui::begin_child_window("SliderWindowChild"); + + static std::array v{0, 0, 0, 0, 0}; + + gui::draw_slider("Slider1", v[0], -1, 1); + gui::draw_slider_n("Slider1(n)", {v.begin(), v.begin() + 1}, -1, 1); + gui::draw_slider_n("Slider2", {v.begin(), v.begin() + 2}, -1, 1); + gui::draw_slider_n("Slider3", {v.begin(), v.begin() + 3}, -1, 1); + gui::draw_slider_n("Slider4", {v.begin(), v.begin() + 4}, -1, 1); + gui::draw_slider_n("Slider5", {v.begin(), v.begin() + 5}, -1, 1); + + gui::end_child_window(); + + gui::end_window(); + } + + gui::end_window(); + } + } + + gui::render(); } auto prometheus_draw() -> void @@ -695,9 +754,32 @@ auto prometheus_draw() -> void auto& this_frame = g_frame_resource[this_frame_index]; auto& [this_frame_index_buffer, this_frame_index_count, this_frame_vertex_buffer, this_frame_vertex_count] = this_frame; - const auto command_list = g_draw_list.command_list(); - const auto vertex_list = g_draw_list.vertex_list(); - const auto index_list = g_draw_list.index_list(); + const auto& draw_datas = gui::get_draw_data(); + + const auto [total_vertex_count, total_index_count] = [&]() noexcept + { + struct sum + { + UINT vertex; + UINT index; + }; + + return std::ranges::fold_left( + draw_datas, + sum{.vertex = 0, .index = 0}, + [](const sum s, const gui::DrawData& draw_data) noexcept -> sum + { + const auto vertex_list = draw_data.vertex_list.get(); + const auto index_list = draw_data.index_list.get(); + + return + { + .vertex = s.vertex + static_cast(vertex_list.size()), + .index = s.index + static_cast(index_list.size()) + }; + } + ); + }(); constexpr D3D12_HEAP_PROPERTIES heap_properties{ .Type = D3D12_HEAP_TYPE_UPLOAD, @@ -707,10 +789,10 @@ auto prometheus_draw() -> void .VisibleNodeMask = 0 }; // Create and grow vertex/index buffers if needed - if (this_frame_vertex_buffer == nullptr or this_frame_vertex_count < vertex_list.size()) + if (this_frame_vertex_buffer == nullptr or this_frame_vertex_count < total_vertex_count) { // todo: grow factor - this_frame_vertex_count = static_cast(vertex_list.size()) + 5000; + this_frame_vertex_count = total_vertex_count + 5000; const D3D12_RESOURCE_DESC resource_desc{ .Dimension = D3D12_RESOURCE_DIMENSION_BUFFER, @@ -735,10 +817,10 @@ auto prometheus_draw() -> void ) ); } - if (this_frame_index_buffer == nullptr or this_frame_index_count < index_list.size()) + if (this_frame_index_buffer == nullptr or this_frame_index_count < total_index_count) { // todo: grow factor - this_frame_index_count = static_cast(index_list.size()) + 10000; + this_frame_index_count = total_index_count + 10000; const D3D12_RESOURCE_DESC resource_desc{ .Dimension = D3D12_RESOURCE_DIMENSION_BUFFER, @@ -776,20 +858,42 @@ auto prometheus_draw() -> void auto* mapped_vertex = static_cast(mapped_vertex_resource); auto* mapped_index = static_cast(mapped_index_resource); - std::ranges::transform( - vertex_list, - mapped_vertex, - [](const draw::DrawList::vertex_type& vertex) -> d3d_vertex_type + UINT vertex_offset = 0; + UINT index_offset = 0; + + std::ranges::for_each( + draw_datas, + [&](const gui::DrawData& draw_data) noexcept -> void { - // return { - // .position = {vertex.position.x, vertex.position.y}, - // .uv = {vertex.uv.x, vertex.uv.y}, - // .color = vertex.color.to(primitive::color_format) - // }; - return std::bit_cast(vertex); + const auto vertex_list = draw_data.vertex_list.get(); + const auto index_list = draw_data.index_list.get(); + + std::ranges::transform( + vertex_list, + mapped_vertex + vertex_offset, + [](const gui::vertex_type& vertex) -> d3d_vertex_type + { + // return { + // .position = {vertex.position.x, vertex.position.y}, + // .uv = {vertex.uv.x, vertex.uv.y}, + // .color = vertex.color.to(primitive::color_format) + // }; + return std::bit_cast(vertex); + } + ); + std::ranges::transform( + index_list, + mapped_index + index_offset, + [vertex_offset](const gui::index_type index) noexcept -> d3d_index_type + { + return static_cast(index + vertex_offset); + } + ); + + vertex_offset += static_cast(vertex_list.size()); + index_offset += static_cast(index_list.size()); } ); - std::ranges::copy(index_list, mapped_index); this_frame_vertex_buffer->Unmap(0, &vertex_range); this_frame_index_buffer->Unmap(0, &index_range); @@ -857,21 +961,43 @@ auto prometheus_draw() -> void constexpr float blend_factor[4]{.0f, .0f, .0f, .0f}; g_command_list->OMSetBlendFactor(blend_factor); - for (const auto& [clip_rect, texture, index_offset, element_count]: command_list) - { - const auto [point, extent] = clip_rect; - const D3D12_RECT rect{static_cast(point.x), static_cast(point.y), static_cast(point.x + extent.width), static_cast(point.y + extent.height)}; - g_command_list->RSSetScissorRects(1, &rect); + UINT total_index_offset = 0; + std::ranges::for_each( + draw_datas, + [&total_index_offset](const gui::DrawData& draw_data) noexcept -> void + { + const auto vertex_list = draw_data.vertex_list.get(); + const auto index_list = draw_data.index_list.get(); - GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(texture != 0, "push_texture_id when create texture view"); - const D3D12_GPU_DESCRIPTOR_HANDLE texture_handle{.ptr = static_cast(texture)}; - g_command_list->SetGraphicsRootDescriptorTable(1, texture_handle); + for (const auto& command_list = draw_data.command_list.get(); + const auto& [clip_rect, texture, index_offset, element_count]: command_list) + { + const auto [point, extent] = clip_rect; + const D3D12_RECT rect + { + static_cast(point.x), + static_cast(point.y), + static_cast(point.x + extent.width), + static_cast(point.y + extent.height) + }; + g_command_list->RSSetScissorRects(1, &rect); + + GAL_PROMETHEUS_ERROR_DEBUG_ASSUME(texture != 0, "push_texture_id when create texture view"); + const D3D12_GPU_DESCRIPTOR_HANDLE texture_handle{.ptr = static_cast(texture)}; + g_command_list->SetGraphicsRootDescriptorTable(1, texture_handle); + + const auto this_index_offset = static_cast(total_index_offset + index_offset); + g_command_list->DrawIndexedInstanced(static_cast(element_count), 1, this_index_offset, 0, 0); + } - g_command_list->DrawIndexedInstanced(static_cast(element_count), 1, static_cast(index_offset), 0, 0); - } + total_index_offset += static_cast(index_list.size()); + } + ); } auto prometheus_shutdown() -> void { print_time(); + + gui::destroy_current_context(); } diff --git a/unit_test/src/draw/dx12/main.cpp b/unit_test/src/gui/dx12/main.cpp similarity index 99% rename from unit_test/src/draw/dx12/main.cpp rename to unit_test/src/gui/dx12/main.cpp index fe864e53..9537715d 100644 --- a/unit_test/src/draw/dx12/main.cpp +++ b/unit_test/src/gui/dx12/main.cpp @@ -37,7 +37,7 @@ int g_window_height = 960; double g_last_time = 0; std::uint64_t g_frame_count = 0; -float g_fps = 0; +float g_fps = 60; extern auto glfw_callback_setup(GLFWwindow& w) -> void; diff --git a/unit_test/src/draw/win/def.hpp b/unit_test/src/gui/win/def.hpp similarity index 73% rename from unit_test/src/draw/win/def.hpp rename to unit_test/src/gui/win/def.hpp index 0e64447c..575723ef 100644 --- a/unit_test/src/draw/win/def.hpp +++ b/unit_test/src/gui/win/def.hpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include @@ -15,11 +15,11 @@ struct d3d_vertex_type std::uint32_t color; }; -using d3d_index_type = gal::prometheus::draw::DrawList::index_type; +using d3d_index_type = gal::prometheus::gui::index_type; using d3d_projection_matrix_type = float[4][4]; -static_assert(sizeof(gal::prometheus::draw::DrawList::vertex_type) == sizeof(d3d_vertex_type)); -static_assert(sizeof(gal::prometheus::draw::DrawList::index_type) == sizeof(d3d_index_type)); +static_assert(sizeof(gal::prometheus::gui::vertex_type) == sizeof(d3d_vertex_type)); +static_assert(sizeof(gal::prometheus::gui::index_type) == sizeof(d3d_index_type)); template auto check_hr_error(