From fcdce043777389aa900328d2b6e533c09e3c424d Mon Sep 17 00:00:00 2001 From: "GuillaumeG." Date: Sun, 1 Nov 2026 20:51:55 +0100 Subject: [PATCH 1/8] VertexBuffer: add new helper methods to append triangles, rect and circle --- includes/FastEngine/vulkan/C_vertexBuffer.hpp | 53 ++++++++++++++ sources/vulkan/C_vertexBuffer.cpp | 70 ++++++++++++++++++- 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/includes/FastEngine/vulkan/C_vertexBuffer.hpp b/includes/FastEngine/vulkan/C_vertexBuffer.hpp index 3551c7fa..f0c50955 100644 --- a/includes/FastEngine/vulkan/C_vertexBuffer.hpp +++ b/includes/FastEngine/vulkan/C_vertexBuffer.hpp @@ -20,6 +20,7 @@ #include "FastEngine/fge_extern.hpp" #include "C_vertex.hpp" #include "FastEngine/C_rect.hpp" +#include "FastEngine/graphic/C_color.hpp" #include "FastEngine/vulkan/C_contextAware.hpp" #include "FastEngine/vulkan/vulkanGlobal.hpp" #include @@ -41,6 +42,13 @@ enum class BufferTypes DEFAULT = LOCAL }; +/** + * \class VertexBuffer + * \ingroup vulkan + * \brief A vertex buffer abstraction + * + * This class is used to create a vertex buffer and manage its data local only or on the device. + */ class FGE_API VertexBuffer : public ContextAware { public: @@ -70,6 +78,44 @@ class FGE_API VertexBuffer : public ContextAware [[nodiscard]] Vertex& operator[](std::size_t index); [[nodiscard]] Vertex const& operator[](std::size_t index) const; + /** + * \warning Only works with triangle topology, otherwise the result is undefined + */ + Vertex* appendTriangle(fge::Vector2f const& positionA, + fge::Vector2f const& texCoordA, + fge::Vector2f const& positionB, + fge::Vector2f const& texCoordB, + fge::Vector2f const& positionC, + fge::Vector2f const& texCoordC, + fge::Color const& color); + /** + * \warning Only works with triangle topology, otherwise the result is undefined + */ + Vertex* appendTriangle(fge::Vector2f const& positionA, + fge::Vector2f const& positionB, + fge::Vector2f const& positionC, + fge::Color const& color); + + /** + * \warning Only works with triangle topology, otherwise the result is undefined + */ + Vertex* appendRect(fge::Vector2f const& position, + fge::Vector2f const& size, + fge::Vector2f const& texCoordUpLeft, + fge::Vector2f const& texCoordUpRight, + fge::Vector2f const& texCoordDownRight, + fge::Vector2f const& texCoordDownLeft, + fge::Color color); + /** + * \warning Only works with triangle topology, otherwise the result is undefined + */ + Vertex* appendRect(fge::Vector2f const& position, fge::Vector2f const& size, fge::Color color); + + /** + * \warning Only works with triangle topology, otherwise the result is undefined + */ + Vertex* appendCircle(fge::Vector2f const& center, float radius, fge::Color color, unsigned int segmentCount = 16); + void setPrimitiveTopology(VkPrimitiveTopology topology); [[nodiscard]] VkPrimitiveTopology getPrimitiveTopology() const; @@ -98,6 +144,13 @@ class FGE_API VertexBuffer : public ContextAware mutable VkPrimitiveTopology g_primitiveTopology; }; +/** + * \class IndexBuffer + * \ingroup vulkan + * \brief An index buffer abstraction + * + * This class is used to create an index buffer and manage its data local only or on the device. + */ class FGE_API IndexBuffer : public ContextAware { public: diff --git a/sources/vulkan/C_vertexBuffer.cpp b/sources/vulkan/C_vertexBuffer.cpp index 8c169e8c..7726c23a 100644 --- a/sources/vulkan/C_vertexBuffer.cpp +++ b/sources/vulkan/C_vertexBuffer.cpp @@ -17,9 +17,9 @@ #include "FastEngine/vulkan/C_vertexBuffer.hpp" #include "FastEngine/fge_except.hpp" #include "FastEngine/vulkan/C_context.hpp" -#include "FastEngine/vulkan/C_logicalDevice.hpp" #include "FastEngine/vulkan/vulkanGlobal.hpp" #include +#include namespace fge::vulkan { @@ -231,6 +231,74 @@ Vertex const& VertexBuffer::operator[](std::size_t index) const return this->g_vertices[index]; } +Vertex* VertexBuffer::appendTriangle(fge::Vector2f const& positionA, + fge::Vector2f const& texCoordA, + fge::Vector2f const& positionB, + fge::Vector2f const& texCoordB, + fge::Vector2f const& positionC, + fge::Vector2f const& texCoordC, + fge::Color const& color) +{ + auto const index = this->g_vertices.size(); + this->append({positionA, color, texCoordA}); + this->append({positionB, color, texCoordB}); + this->append({positionC, color, texCoordC}); + return &this->g_vertices[index]; +} +Vertex* VertexBuffer::appendTriangle(fge::Vector2f const& positionA, + fge::Vector2f const& positionB, + fge::Vector2f const& positionC, + fge::Color const& color) +{ + auto const index = this->g_vertices.size(); + this->append({positionA, color, {}}); + this->append({positionB, color, {}}); + this->append({positionC, color, {}}); + return &this->g_vertices[index]; +} + +Vertex* VertexBuffer::appendRect(fge::Vector2f const& position, + fge::Vector2f const& size, + fge::Vector2f const& texCoordUpLeft, + fge::Vector2f const& texCoordUpRight, + fge::Vector2f const& texCoordDownRight, + fge::Vector2f const& texCoordDownLeft, + fge::Color color) +{ + auto const index = this->g_vertices.size(); + + fge::Vector2f const end = position + size; + this->appendTriangle(position, texCoordUpLeft, {end.x, position.y}, texCoordUpRight, end, texCoordDownRight, color); + this->appendTriangle(position, texCoordUpLeft, end, texCoordDownRight, {position.x, end.y}, texCoordDownLeft, + color); + return &this->g_vertices[index]; +} +Vertex* VertexBuffer::appendRect(fge::Vector2f const& position, fge::Vector2f const& size, fge::Color color) +{ + auto const index = this->g_vertices.size(); + + fge::Vector2f const end = position + size; + this->appendTriangle(position, {end.x, position.y}, end, color); + this->appendTriangle(position, end, {position.x, end.y}, color); + return &this->g_vertices[index]; +} + +Vertex* +VertexBuffer::appendCircle(fge::Vector2f const& center, float radius, fge::Color color, unsigned int segmentCount) +{ + auto const index = this->g_vertices.size(); + float const angleStep = 2.0f * std::numbers::pi_v / static_cast(segmentCount); + fge::Vector2f previous{center.x + radius, center.y}; + for (unsigned int i = 1; i <= segmentCount; ++i) + { + float const angle = angleStep * static_cast(i); + fge::Vector2f const next{center.x + std::cos(angle) * radius, center.y + std::sin(angle) * radius}; + this->appendTriangle(center, previous, next, color); + previous = next; + } + return &this->g_vertices[index]; +} + void VertexBuffer::setPrimitiveTopology(VkPrimitiveTopology topology) { this->g_primitiveTopology = topology; From cdc9b90c04b85c7d6259860562f97144eda0192b Mon Sep 17 00:00:00 2001 From: "GuillaumeG." Date: Sun, 1 Nov 2026 20:53:42 +0100 Subject: [PATCH 2/8] Add the first optional extension to FGE: the ObjChart to add point graphics --- CMakeLists.txt | 9 + docs/Doxyfile.in | 1 + examples/objChart_010/CMakeLists.txt | 9 + examples/objChart_010/main.cpp | 290 ++++++ extensions/CMakeLists.txt | 4 + extensions/objChart/CMakeLists.txt | 17 + .../FastEngine/extensions/C_objChart.hpp | 324 +++++++ .../FastEngine/extensions/C_objChart.inl | 863 ++++++++++++++++++ use_clang_format.sh | 3 + verify_clang_format.sh | 3 + 10 files changed, 1523 insertions(+) create mode 100644 examples/objChart_010/CMakeLists.txt create mode 100644 examples/objChart_010/main.cpp create mode 100644 extensions/CMakeLists.txt create mode 100644 extensions/objChart/CMakeLists.txt create mode 100644 extensions/objChart/includes/FastEngine/extensions/C_objChart.hpp create mode 100644 extensions/objChart/includes/FastEngine/extensions/C_objChart.inl diff --git a/CMakeLists.txt b/CMakeLists.txt index 474f5a57..3515d62e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,7 @@ else() endif() option(FGE_BUILD_EXAMPLES "Build examples" ON) +option(FGE_BUILD_EXTENSIONS "Build extensions (see extensions/ directory)" ON) option(FGE_BUILD_TESTS "Build tests" ON) option(FGE_ENABLE_SERVER_NETWORK_RANDOM_LOST "enable/disable random packet lost for server" OFF) @@ -476,6 +477,11 @@ install(TARGETS ${FGE_SERVER_LIB_NAME} EXPORT ${PROJECT_NAME}Targets RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} FILE_SET publicHeaders DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}) +#Extensions +if (FGE_BUILD_EXTENSIONS) + add_subdirectory(extensions) +endif() + install(FILES fge_changelog.txt DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/${PROJECT_NAME}) install(FILES IMAGE_AUDIO_LOGO_LICENSE DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/${PROJECT_NAME}) install(FILES LICENSE DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/${PROJECT_NAME}) @@ -585,4 +591,7 @@ if (FGE_BUILD_EXAMPLES) add_subdirectory(examples/mipmaps_007) add_subdirectory(examples/noWindowOnlyRenderTexture_008) add_subdirectory(examples/shaderChain_009) + if (FGE_BUILD_EXTENSIONS) + add_subdirectory(examples/objChart_010) + endif() endif() diff --git a/docs/Doxyfile.in b/docs/Doxyfile.in index a99d17e6..06177236 100644 --- a/docs/Doxyfile.in +++ b/docs/Doxyfile.in @@ -1027,6 +1027,7 @@ WARN_LOGFILE = # Note: If this tag is empty the current directory is searched. INPUT = "../includes" \ + "../extensions" \ "../README.md" # This tag can be used to specify the character encoding of the source files diff --git a/examples/objChart_010/CMakeLists.txt b/examples/objChart_010/CMakeLists.txt new file mode 100644 index 00000000..57c11713 --- /dev/null +++ b/examples/objChart_010/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.10) +project(example_objChart_010) + +add_executable(${PROJECT_NAME} main.cpp) +add_dependencies(${PROJECT_NAME} FgeClientExeDeps) + +target_link_libraries(${PROJECT_NAME} ${FGE_CLIENT_LIBS} FastEngine::ObjChart) + +setMSVCDefaultWorkingDir(${PROJECT_NAME}) diff --git a/examples/objChart_010/main.cpp b/examples/objChart_010/main.cpp new file mode 100644 index 00000000..e528a16a --- /dev/null +++ b/examples/objChart_010/main.cpp @@ -0,0 +1,290 @@ +/* + * Copyright 2026 Guillaume Guillet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "FastEngine/C_clock.hpp" +#include "FastEngine/C_random.hpp" +#include "FastEngine/C_scene.hpp" +#include "FastEngine/extensions/C_objChart.hpp" +#include "FastEngine/graphic/C_renderWindow.hpp" +#include "FastEngine/manager/shader_manager.hpp" +#include "FastEngine/manager/texture_manager.hpp" +#include "FastEngine/object/C_objShape.hpp" +#include "FastEngine/object/C_objText.hpp" +#include "FastEngine/object/C_objWindow.hpp" +#include "SDL3/SDL.h" +#include "SDL3/SDL_main.h" +#include +#include + +#define CHART_POINT_COUNT 25 +#define CHART_MOVE_INTERVAL std::chrono::milliseconds(2000) + +//Create the MainScene class +class MainScene : public fge::Scene +{ +public: + void start(fge::RenderWindow& renderWindow) + { + fge::Event event(renderWindow); + fge::GuiElementHandler guiElementHandler(event, renderWindow); + guiElementHandler.setEventCallback(); + + this->setLinkedRenderTarget(&renderWindow); + + //Set default callback context + this->setCallbackContext({&event, &guiElementHandler}); + + //Init texture manager + if (!fge::texture::gManager.initialize()) + { + std::cout << "Failed to initialize texture manager" << std::endl; + return; + } + //Init font manager + if (!fge::font::gManager.initialize()) + { + std::cout << "Failed to initialize font manager" << std::endl; + return; + } + + //Load texture + fge::texture::gManager.loadFromFile("close", "resources/images/window/close.png"); + fge::texture::gManager.loadFromFile("minimize", "resources/images/window/minimize.png"); + fge::texture::gManager.loadFromFile("resize", "resources/images/window/resize.png"); + fge::texture::gManager.loadFromFile("window", "resources/images/window/window.png"); + + //Load font + fge::font::gManager.loadFromFile("base", "resources/fonts/SourceSansPro-Regular.ttf"); + + fge::Clock tick; + + //Create a text object with explanation + auto* explainText = + this->newObject({FGE_SCENE_PLAN_HIGH_TOP + 1}, "", "base", fge::Vector2f{}, 18); + explainText->setFillColor(fge::Color::Black); + + //Create the window + auto* objWindow = this->newObject({FGE_SCENE_PLAN_HIGH_TOP}); + objWindow->setTextureClose("close"); + objWindow->setTextureMinimize("minimize"); + objWindow->setTextureResize("resize"); + objWindow->setTexture("window"); + objWindow->setSize({500.0f, 360.0f}); + objWindow->setPosition({150.0f, 130.0f}); + objWindow->showExitButton(false); + + //Create the chart (using double for the data) that fill the window + auto* objChart = objWindow->_windowScene.newObject(); + objChart->setFont("base"); + objChart->setDynamicSize( + {{0.0f, 0.0f}, {fge::DynamicSize::SizeModes::SIZE_AUTO, fge::DynamicSize::SizeModes::SIZE_AUTO}}); + + //Add some points, a noisy sine wave + for (std::size_t i = 0; i < CHART_POINT_COUNT; ++i) + { + double const x = static_cast(i) * 0.5; + objChart->addPoint({x, std::sin(x) * 10.0 + fge::_random.range(-2.0, 2.0)}); + } + + //Move every point randomly, the chart will animate the change + auto const movePoints = [objChart]() { + for (std::size_t i = 0; i < objChart->getPointCount(); ++i) + { + auto point = objChart->getPoint(i); + point.y += fge::_random.range(-4.0, 4.0); + objChart->setPoint(i, point); + } + }; + + auto const updateExplainText = [explainText, objChart]() { + char const* shapeName = "circle"; + switch (objChart->getPointShape()) + { + case fge::ObjChartD::PointShapes::SHAPE_SQUARE: + shapeName = "square"; + break; + case fge::ObjChartD::PointShapes::SHAPE_DIAMOND: + shapeName = "diamond"; + break; + default: + break; + } + + explainText->setString(std::string{"Points are moving every 2 seconds, use your mouse to play with the " + "window\n" + "SPACE: move the points now\n" + "A: toggle animation ("} + + (objChart->isAnimationEnabled() ? "on" : "off") + + ")\n" + "R: add a point, D: remove the last point\n" + "S: change point shape (" + + shapeName + ")"); + }; + updateExplainText(); + + event._onKeyDown.addLambda([&]([[maybe_unused]] fge::Event const& evt, SDL_KeyboardEvent const& keyEvent) { + switch (keyEvent.key) + { + case SDLK_SPACE: + movePoints(); + break; + case SDLK_A: + objChart->enableAnimation(!objChart->isAnimationEnabled()); + break; + case SDLK_R: + { + double x = 0.0; + if (objChart->getPointCount() > 0) + { + x = objChart->getPoint(objChart->getPointCount() - 1).x + 0.5; + } + objChart->addPoint({x, std::sin(x) * 10.0 + fge::_random.range(-2.0, 2.0)}, fge::Color{220, 70, 50}); + break; + } + case SDLK_D: + if (objChart->getPointCount() > 0) + { + objChart->removePoint(objChart->getPointCount() - 1); + } + break; + case SDLK_S: + switch (objChart->getPointShape()) + { + case fge::ObjChartD::PointShapes::SHAPE_CIRCLE: + objChart->setPointShape(fge::ObjChartD::PointShapes::SHAPE_SQUARE); + break; + case fge::ObjChartD::PointShapes::SHAPE_SQUARE: + objChart->setPointShape(fge::ObjChartD::PointShapes::SHAPE_DIAMOND); + break; + case fge::ObjChartD::PointShapes::SHAPE_DIAMOND: + objChart->setPointShape(fge::ObjChartD::PointShapes::SHAPE_CIRCLE); + break; + } + break; + default: + return; + } + updateExplainText(); + }); + + fge::Clock moveClock; + + //Begin loop + bool running = true; + while (running) + { + //Update event + event.process(); + if (event.isEventType(SDL_EVENT_QUIT)) + { + running = false; + } + if (event.isEventType(SDL_EVENT_WINDOW_RESIZED)) + { + auto view = renderWindow.getView(); + view.resizeFixCenter({event.getWindowSize().x, event.getWindowSize().y}); + renderWindow.setView(view); + guiElementHandler.checkViewSize(); + } + + if (moveClock.reached(CHART_MOVE_INTERVAL)) + { + moveClock.restart(); + movePoints(); + } + + //Update scene + auto deltaTick = tick.restart(); + this->update(renderWindow, event, std::chrono::duration_cast(deltaTick)); + + //Drawing + auto imageIndex = renderWindow.prepareNextFrame(nullptr, FGE_RENDER_TIMEOUT_BLOCKING); + if (imageIndex != FGE_RENDER_BAD_IMAGE_INDEX) + { + fge::vulkan::GetActiveContext()._garbageCollector.setCurrentFrame(renderWindow.getCurrentFrame()); + + renderWindow.beginRenderPass(imageIndex); + + this->draw(renderWindow); + + renderWindow.endRenderPass(); + + renderWindow.display(imageIndex); + } + } + + fge::vulkan::GetActiveContext().waitIdle(); + + fge::vulkan::GetActiveContext()._garbageCollector.enable(false); + } +}; + +int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) +{ + using namespace fge::vulkan; + + auto instance = Context::init(SDL_INIT_VIDEO | SDL_INIT_EVENTS, "example 010: objChart"); + Context::enumerateExtensions(); + + SurfaceSDLWindow window(instance, FGE_WINDOWPOS_CENTERED, {800, 600}, SDL_WINDOW_RESIZABLE); + + // Check that the window was successfully created + if (!window.isCreated()) + { + // In the case that the window could not be made... + std::cout << "Could not create window: " << SDL_GetError() << std::endl; + return 1; + } + + Context vulkanContext(window); + vulkanContext._garbageCollector.enable(true); + + if (!fge::shader::gManager.initialize()) + { + std::cout << "Could not initialize shader manager" << std::endl; + return 1; + } + fge::shader::gManager.loadFromFile(FGE_OBJSHAPE_INSTANCES_SHADER_VERTEX, + "resources/shaders/objShapeInstances_vertex.vert", Shader::Type::SHADER_VERTEX, + fge::shader::ShaderInputTypes::SHADER_GLSL); + fge::shader::gManager.loadFromFile(FGE_OBJSPRITEBATCHES_SHADER_FRAGMENT, + "resources/shaders/objSpriteBatches_fragment.frag", + Shader::Type::SHADER_FRAGMENT, fge::shader::ShaderInputTypes::SHADER_GLSL); + fge::shader::gManager.loadFromFile(FGE_OBJSPRITEBATCHES_SHADER_VERTEX, + "resources/shaders/objSpriteBatches_vertex.vert", Shader::Type::SHADER_VERTEX, + fge::shader::ShaderInputTypes::SHADER_GLSL); + + fge::RenderWindow renderWindow(vulkanContext, window); + renderWindow.setClearColor(fge::Color::White); + + std::unique_ptr scene = std::make_unique(); + scene->start(renderWindow); + scene.reset(); + + fge::texture::gManager.uninitialize(); + fge::font::gManager.uninitialize(); + fge::shader::gManager.uninitialize(); + + renderWindow.destroy(); + + vulkanContext.destroy(); + + window.destroy(); + instance.destroy(); + SDL_Quit(); + + return 0; +} diff --git a/extensions/CMakeLists.txt b/extensions/CMakeLists.txt new file mode 100644 index 00000000..39883c94 --- /dev/null +++ b/extensions/CMakeLists.txt @@ -0,0 +1,4 @@ +#FastEngine extensions +#Extensions are optional modules built on top of FastEngine, link the ones you need. + +add_subdirectory(objChart) diff --git a/extensions/objChart/CMakeLists.txt b/extensions/objChart/CMakeLists.txt new file mode 100644 index 00000000..588ed8b6 --- /dev/null +++ b/extensions/objChart/CMakeLists.txt @@ -0,0 +1,17 @@ +#ObjChart extension (header only) +set(FGE_EXT_OBJCHART_LIB_NAME "${PROJECT_NAME}ObjChart") + +add_library(${FGE_EXT_OBJCHART_LIB_NAME} INTERFACE) +add_library(${PROJECT_NAME}::ObjChart ALIAS ${FGE_EXT_OBJCHART_LIB_NAME}) +set_target_properties(${FGE_EXT_OBJCHART_LIB_NAME} PROPERTIES EXPORT_NAME ObjChart) + +target_sources(${FGE_EXT_OBJCHART_LIB_NAME} INTERFACE FILE_SET publicHeaders TYPE HEADERS + BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/includes + FILES + includes/FastEngine/extensions/C_objChart.hpp + includes/FastEngine/extensions/C_objChart.inl) + +target_link_libraries(${FGE_EXT_OBJCHART_LIB_NAME} INTERFACE ${FGE_LIB_NAME}) + +install(TARGETS ${FGE_EXT_OBJCHART_LIB_NAME} EXPORT ${PROJECT_NAME}Targets + FILE_SET publicHeaders DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}) diff --git a/extensions/objChart/includes/FastEngine/extensions/C_objChart.hpp b/extensions/objChart/includes/FastEngine/extensions/C_objChart.hpp new file mode 100644 index 00000000..a013cf0b --- /dev/null +++ b/extensions/objChart/includes/FastEngine/extensions/C_objChart.hpp @@ -0,0 +1,324 @@ +/* + * Copyright 2026 Guillaume Guillet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _FGE_EXT_C_OBJCHART_HPP_INCLUDED +#define _FGE_EXT_C_OBJCHART_HPP_INCLUDED + +#include "FastEngine/C_guiElement.hpp" +#include "FastEngine/C_rect.hpp" +#include "FastEngine/C_vector.hpp" +#include "FastEngine/graphic/C_color.hpp" +#include "FastEngine/object/C_objText.hpp" +#include "FastEngine/object/C_object.hpp" +#include "FastEngine/vulkan/C_vertexBuffer.hpp" +#include "FastEngine/vulkan/vulkanGlobal.hpp" +#include +#include +#include +#include +#include + +#define FGE_OBJCHART_CLASSNAME_FLOAT "FGE:EXT:OBJ:CHART:F" +#define FGE_OBJCHART_CLASSNAME_DOUBLE "FGE:EXT:OBJ:CHART:D" + +#define FGE_OBJCHART_DEFAULT_SIZE_X 320.0f +#define FGE_OBJCHART_DEFAULT_SIZE_Y 200.0f +#define FGE_OBJCHART_DEFAULT_TICK_COUNT 5 +#define FGE_OBJCHART_DEFAULT_POINT_SIZE 6.0f +#define FGE_OBJCHART_DEFAULT_CHARACTER_SIZE 12 +#define FGE_OBJCHART_DEFAULT_ANIMATION_SPEED 6.0f +#define FGE_OBJCHART_TICK_LENGTH 4.0f +#define FGE_OBJCHART_LINE_THICKNESS 1.0f +#define FGE_OBJCHART_MAX_TICKS 64 +#define FGE_OBJCHART_CIRCLE_SEGMENTS 12 + +namespace fge +{ + +/** + * Brief The data types supported by ObjChart + */ +template +concept ObjChartValueType = std::same_as || std::same_as; + +/** + * \class ObjChart + * \ingroup extensions + * \brief A point chart (scatter plot) with linear axes + * + * The chart can be configured to store its data as float or double (see ObjChartF and ObjChartD). + * Axes ranges can be computed automatically from the points, or fixed by the user. + * + * When animation is enabled (the default), modifying a point does not teleport it, + * the displayed point gently moves toward its new position. Auto ranges are animated the same way. + * + * The chart can be sized with a fixed size or with a fge::DynamicSize relative to its + * fge::GuiElementHandler (useful inside a fge::ObjWindow). + * + * \tparam T The data type, float or double + */ +template +class ObjChart : public fge::Object, public fge::Subscriber +{ +public: + using ValueType = T; + using Point = fge::Vector2; + + struct Range + { + T _min{0}; + T _max{1}; + + [[nodiscard]] constexpr T getLength() const { return this->_max - this->_min; } + [[nodiscard]] constexpr bool operator==(Range const& r) const = default; + }; + + struct Padding + { + float _left{48.0f}; + float _top{10.0f}; + float _right{14.0f}; + float _bottom{26.0f}; + }; + + enum class RangeModes + { + RANGE_AUTO, + RANGE_FIXED + }; + + enum class PointShapes + { + SHAPE_SQUARE, + SHAPE_CIRCLE, + SHAPE_DIAMOND + }; + + ObjChart(); + explicit ObjChart(fge::Font font, + fge::Vector2f const& size = {FGE_OBJCHART_DEFAULT_SIZE_X, FGE_OBJCHART_DEFAULT_SIZE_Y}); + ~ObjChart() override = default; + + FGE_OBJ_DEFAULT_COPYMETHOD(fge::ObjChart) + + void first(fge::Scene& scene) override; + void callbackRegister(fge::Event& event, fge::GuiElementHandler* guiElementHandlerPtr) override; + FGE_OBJ_UPDATE_DECLARE + FGE_OBJ_DRAW_DECLARE + + //Size + + /** + * \brief Set a fixed size for the chart + * + * This disables any dynamic size previously set. + */ + void setSize(fge::Vector2f const& size); + /** + * \brief Set a dynamic size, relative to the GuiElementHandler the chart is registered to + */ + void setDynamicSize(fge::DynamicSize const& size); + [[nodiscard]] fge::Vector2f const& getSize() const; + + void setPadding(Padding const& padding); + [[nodiscard]] Padding const& getPadding() const; + + /** + * \brief Get the area (in local coordinates) where the points are drawn + */ + [[nodiscard]] fge::RectFloat getPlotArea() const; + + //Points + + std::size_t addPoint(Point const& point, std::optional color = std::nullopt); + /** + * \brief Set the target position of a point + * + * If animation is enabled, the displayed point will gently move toward this new position. + */ + void setPoint(std::size_t index, Point const& point); + /** + * \brief Replace all the points + * + * Existing points (same index) are moved toward their new position, + * extra points are added and the remaining ones are removed. + */ + void setPoints(std::span points); + void setPointColor(std::size_t index, std::optional color); + void removePoint(std::size_t index); + void clearPoints(); + + [[nodiscard]] std::size_t getPointCount() const; + [[nodiscard]] Point const& getPoint(std::size_t index) const; + [[nodiscard]] Point const& getDisplayedPoint(std::size_t index) const; + [[nodiscard]] std::optional const& getPointColor(std::size_t index) const; + + //Axes + + void setXRange(T min, T max); + void setYRange(T min, T max); + void setXRangeAuto(); + void setYRangeAuto(); + + [[nodiscard]] RangeModes getXRangeMode() const; + [[nodiscard]] RangeModes getYRangeMode() const; + /** + * \brief Get the currently displayed X range (can be in transition if animated) + */ + [[nodiscard]] Range const& getXRange() const; + /** + * \brief Get the currently displayed Y range (can be in transition if animated) + */ + [[nodiscard]] Range const& getYRange() const; + + /** + * \brief Set the desired number of ticks for each axis + * + * The real count can slightly differ as ticks are placed on "nice" values (1, 2, 5 * 10^n). + */ + void setTickCount(std::size_t xCount, std::size_t yCount); + [[nodiscard]] fge::Vector2 getTickCount() const; + + //Animation + + void enableAnimation(bool enable); + [[nodiscard]] bool isAnimationEnabled() const; + /** + * \brief Set the animation speed + * + * The displayed values exponentially approach their target, the speed is the rate per second. + * A higher value means a faster animation. + */ + void setAnimationSpeed(float speed); + [[nodiscard]] float getAnimationSpeed() const; + [[nodiscard]] bool isAnimating() const; + /** + * \brief Immediately set every displayed value to its target + */ + void finishAnimation(); + + //Style + + void setFont(fge::Font font); + [[nodiscard]] fge::Font const& getFont() const; + void setCharacterSize(fge::CharacterSize size); + [[nodiscard]] fge::CharacterSize getCharacterSize() const; + + void setPointShape(PointShapes shape); + [[nodiscard]] PointShapes getPointShape() const; + void setPointSize(float size); + [[nodiscard]] float getPointSize() const; + + void setDefaultPointColor(fge::Color color); + [[nodiscard]] fge::Color getDefaultPointColor() const; + void setBackgroundColor(fge::Color color); + [[nodiscard]] fge::Color getBackgroundColor() const; + void setAxisColor(fge::Color color); + [[nodiscard]] fge::Color getAxisColor() const; + void setGridColor(fge::Color color); + [[nodiscard]] fge::Color getGridColor() const; + void setLabelColor(fge::Color color); + [[nodiscard]] fge::Color getLabelColor() const; + + void showGrid(bool show); + [[nodiscard]] bool isGridShown() const; + + //Mapping + + /** + * \brief Convert a data value into a local position, using the displayed ranges + */ + [[nodiscard]] fge::Vector2f mapValueToLocal(Point const& value) const; + /** + * \brief Convert a local position into a data value, using the displayed ranges + */ + [[nodiscard]] Point mapLocalToValue(fge::Vector2f const& position) const; + + [[nodiscard]] char const* getClassName() const override; + [[nodiscard]] char const* getReadableClassName() const override; + + [[nodiscard]] fge::RectFloat getGlobalBounds() const override; + [[nodiscard]] fge::RectFloat getLocalBounds() const override; + +private: + struct PointData + { + Point _target; + Point _displayed; + std::optional _color; + }; + + struct AxisData + { + RangeModes _mode{RangeModes::RANGE_AUTO}; + Range _target{}; + Range _displayed{}; + std::size_t _tickCount{FGE_OBJCHART_DEFAULT_TICK_COUNT}; + }; + + void onGuiResized(fge::GuiElementHandler const& handler, fge::Vector2f const& size); + + void refreshSize(fge::Vector2f const& targetSize); + void refreshAutoRanges(AxisData& axis, bool isXAxis); + void refreshAutoRanges(); + void applyTargets(bool forceSnap); + + void ensureGeometryUpdate() const; + void buildAxis(AxisData const& axis, bool isXAxis, fge::RectFloat const& plotArea) const; + + [[nodiscard]] static T computeStep(T length, std::size_t tickCount); + + fge::GuiElementHandler* g_guiElementHandler{nullptr}; + std::optional g_dynamicSize; + fge::Vector2f g_size{FGE_OBJCHART_DEFAULT_SIZE_X, FGE_OBJCHART_DEFAULT_SIZE_Y}; + Padding g_padding; + + std::vector g_points; + AxisData g_axisX; + AxisData g_axisY; + + bool g_animationEnabled{true}; + float g_animationSpeed{FGE_OBJCHART_DEFAULT_ANIMATION_SPEED}; + bool g_animating{false}; + + fge::Font g_font; + fge::CharacterSize g_characterSize{FGE_OBJCHART_DEFAULT_CHARACTER_SIZE}; + + PointShapes g_pointShape{PointShapes::SHAPE_CIRCLE}; + float g_pointSize{FGE_OBJCHART_DEFAULT_POINT_SIZE}; + + fge::Color g_colorDefaultPoint{30, 110, 220}; + fge::Color g_colorBackground{250, 250, 250}; + fge::Color g_colorAxis{60, 60, 60}; + fge::Color g_colorGrid{220, 220, 220}; + fge::Color g_colorLabel{40, 40, 40}; + bool g_showGrid{true}; + + mutable bool g_needGeometryUpdate{true}; + mutable fge::vulkan::VertexBuffer g_frameVertices; + mutable fge::vulkan::VertexBuffer g_pointVertices; + mutable std::vector g_xLabels; + mutable std::vector g_yLabels; +}; + +using ObjChartF = ObjChart; +using ObjChartD = ObjChart; + +} // namespace fge + +#include "C_objChart.inl" + +#endif // _FGE_EXT_C_OBJCHART_HPP_INCLUDED diff --git a/extensions/objChart/includes/FastEngine/extensions/C_objChart.inl b/extensions/objChart/includes/FastEngine/extensions/C_objChart.inl new file mode 100644 index 00000000..0b3740b9 --- /dev/null +++ b/extensions/objChart/includes/FastEngine/extensions/C_objChart.inl @@ -0,0 +1,863 @@ +/* + * Copyright 2026 Guillaume Guillet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include + +namespace fge +{ + +template +ObjChart::ObjChart() : + g_frameVertices(fge::vulkan::GetActiveContext()), + g_pointVertices(fge::vulkan::GetActiveContext()) +{ + this->g_frameVertices.create(0, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST); + this->g_pointVertices.create(0, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST); + + this->refreshAutoRanges(); + this->applyTargets(true); +} +template +ObjChart::ObjChart(fge::Font font, fge::Vector2f const& size) : + ObjChart() +{ + this->g_font = std::move(font); + this->g_size = size; +} + +template +void ObjChart::first([[maybe_unused]] fge::Scene& scene) +{ + this->_drawMode = fge::Object::DrawModes::DRAW_ALWAYS_DRAWN; +} +template +void ObjChart::callbackRegister([[maybe_unused]] fge::Event& event, fge::GuiElementHandler* guiElementHandlerPtr) +{ + this->detachAll(); + + this->g_guiElementHandler = guiElementHandlerPtr; + if (guiElementHandlerPtr == nullptr) + { + return; + } + + guiElementHandlerPtr->_onGuiResized.addObjectFunctor(&fge::ObjChart::onGuiResized, this, this); + + this->refreshSize(guiElementHandlerPtr->_lastSize); +} + +template +FGE_OBJ_UPDATE_BODY(ObjChart) +{ + if (!this->g_animating) + { + return; + } + + if (!this->g_animationEnabled || this->g_animationSpeed <= 0.0f) + { + this->applyTargets(true); + return; + } + + //Exponential approach, independent of the frame rate + T const deltaSeconds = std::chrono::duration(deltaTime).count(); + T const factor = T{1} - std::exp(-static_cast(this->g_animationSpeed) * deltaSeconds); + + auto const approach = [factor](T& current, T targetValue, T epsilon) { + if (!std::isfinite(current) || !std::isfinite(targetValue)) + { + current = targetValue; + return false; + } + + T const diff = targetValue - current; + if (std::abs(diff) <= epsilon) + { + current = targetValue; + return false; + } + current += diff * factor; + return true; + }; + + T const epsilonX = + std::max(this->g_axisX._target.getLength(), this->g_axisX._displayed.getLength()) * static_cast(1e-4); + T const epsilonY = + std::max(this->g_axisY._target.getLength(), this->g_axisY._displayed.getLength()) * static_cast(1e-4); + + bool stillAnimating = false; + + stillAnimating |= approach(this->g_axisX._displayed._min, this->g_axisX._target._min, epsilonX); + stillAnimating |= approach(this->g_axisX._displayed._max, this->g_axisX._target._max, epsilonX); + stillAnimating |= approach(this->g_axisY._displayed._min, this->g_axisY._target._min, epsilonY); + stillAnimating |= approach(this->g_axisY._displayed._max, this->g_axisY._target._max, epsilonY); + + for (auto& point: this->g_points) + { + stillAnimating |= approach(point._displayed.x, point._target.x, epsilonX); + stillAnimating |= approach(point._displayed.y, point._target.y, epsilonY); + } + + this->g_animating = stillAnimating; + this->g_needGeometryUpdate = true; +} + +#ifndef FGE_DEF_SERVER +template +FGE_OBJ_DRAW_BODY(ObjChart) +{ + this->ensureGeometryUpdate(); + + auto const transformIndex = target.requestGlobalTransform(*this, states._resTransform); + + auto copyStates = states.copy(); + copyStates._resTransform.set(transformIndex); + + if (this->g_frameVertices.getCount() > 0) + { + copyStates._vertexBuffer = &this->g_frameVertices; + target.draw(copyStates); + } + + if (this->g_pointVertices.getCount() > 0) + { + copyStates._vertexBuffer = &this->g_pointVertices; + target.draw(copyStates); + } + + auto textStates = states.copy(); + textStates._resTransform.set(transformIndex); + + for (auto const& label: this->g_xLabels) + { + label.draw(target, textStates); + } + for (auto const& label: this->g_yLabels) + { + label.draw(target, textStates); + } +} +#endif + +template +void ObjChart::setSize(fge::Vector2f const& size) +{ + this->g_dynamicSize.reset(); + this->g_size = {std::max(size.x, 0.0f), std::max(size.y, 0.0f)}; + this->g_needGeometryUpdate = true; +} +template +void ObjChart::setDynamicSize(fge::DynamicSize const& size) +{ + this->g_dynamicSize = size; + if (this->g_guiElementHandler != nullptr) + { + this->refreshSize(this->g_guiElementHandler->_lastSize); + } +} +template +fge::Vector2f const& ObjChart::getSize() const +{ + return this->g_size; +} + +template +void ObjChart::setPadding(Padding const& padding) +{ + this->g_padding = padding; + this->g_needGeometryUpdate = true; +} +template +typename ObjChart::Padding const& ObjChart::getPadding() const +{ + return this->g_padding; +} + +template +fge::RectFloat ObjChart::getPlotArea() const +{ + return {{this->g_padding._left, this->g_padding._top}, + {std::max(this->g_size.x - this->g_padding._left - this->g_padding._right, 0.0f), + std::max(this->g_size.y - this->g_padding._top - this->g_padding._bottom, 0.0f)}}; +} + +template +std::size_t ObjChart::addPoint(Point const& point, std::optional color) +{ + this->g_points.push_back({point, point, color}); + this->refreshAutoRanges(); + this->applyTargets(false); + return this->g_points.size() - 1; +} +template +void ObjChart::setPoint(std::size_t index, Point const& point) +{ + this->g_points[index]._target = point; + this->refreshAutoRanges(); + this->applyTargets(false); +} +template +void ObjChart::setPoints(std::span points) +{ + std::size_t const commonCount = std::min(points.size(), this->g_points.size()); + + for (std::size_t i = 0; i < commonCount; ++i) + { + this->g_points[i]._target = points[i]; + } + this->g_points.resize(commonCount); + for (std::size_t i = commonCount; i < points.size(); ++i) + { + this->g_points.push_back({points[i], points[i], std::nullopt}); + } + + this->refreshAutoRanges(); + this->applyTargets(false); +} +template +void ObjChart::setPointColor(std::size_t index, std::optional color) +{ + this->g_points[index]._color = color; + this->g_needGeometryUpdate = true; +} +template +void ObjChart::removePoint(std::size_t index) +{ + this->g_points.erase(this->g_points.begin() + static_cast(index)); + this->refreshAutoRanges(); + this->applyTargets(false); +} +template +void ObjChart::clearPoints() +{ + this->g_points.clear(); + this->refreshAutoRanges(); + this->applyTargets(false); +} + +template +std::size_t ObjChart::getPointCount() const +{ + return this->g_points.size(); +} +template +typename ObjChart::Point const& ObjChart::getPoint(std::size_t index) const +{ + return this->g_points[index]._target; +} +template +typename ObjChart::Point const& ObjChart::getDisplayedPoint(std::size_t index) const +{ + return this->g_points[index]._displayed; +} +template +std::optional const& ObjChart::getPointColor(std::size_t index) const +{ + return this->g_points[index]._color; +} + +template +void ObjChart::setXRange(T min, T max) +{ + if (min > max) + { + std::swap(min, max); + } + if (min == max) + { + max = min + T{1}; + } + + this->g_axisX._mode = RangeModes::RANGE_FIXED; + this->g_axisX._target = {min, max}; + this->applyTargets(false); +} +template +void ObjChart::setYRange(T min, T max) +{ + if (min > max) + { + std::swap(min, max); + } + if (min == max) + { + max = min + T{1}; + } + + this->g_axisY._mode = RangeModes::RANGE_FIXED; + this->g_axisY._target = {min, max}; + this->applyTargets(false); +} +template +void ObjChart::setXRangeAuto() +{ + this->g_axisX._mode = RangeModes::RANGE_AUTO; + this->refreshAutoRanges(); + this->applyTargets(false); +} +template +void ObjChart::setYRangeAuto() +{ + this->g_axisY._mode = RangeModes::RANGE_AUTO; + this->refreshAutoRanges(); + this->applyTargets(false); +} + +template +typename ObjChart::RangeModes ObjChart::getXRangeMode() const +{ + return this->g_axisX._mode; +} +template +typename ObjChart::RangeModes ObjChart::getYRangeMode() const +{ + return this->g_axisY._mode; +} +template +typename ObjChart::Range const& ObjChart::getXRange() const +{ + return this->g_axisX._displayed; +} +template +typename ObjChart::Range const& ObjChart::getYRange() const +{ + return this->g_axisY._displayed; +} + +template +void ObjChart::setTickCount(std::size_t xCount, std::size_t yCount) +{ + this->g_axisX._tickCount = std::max(xCount, 1); + this->g_axisY._tickCount = std::max(yCount, 1); + this->refreshAutoRanges(); + this->applyTargets(false); +} +template +fge::Vector2 ObjChart::getTickCount() const +{ + return {this->g_axisX._tickCount, this->g_axisY._tickCount}; +} + +template +void ObjChart::enableAnimation(bool enable) +{ + this->g_animationEnabled = enable; + if (!enable) + { + this->applyTargets(true); + } +} +template +bool ObjChart::isAnimationEnabled() const +{ + return this->g_animationEnabled; +} +template +void ObjChart::setAnimationSpeed(float speed) +{ + this->g_animationSpeed = std::max(speed, 0.0f); +} +template +float ObjChart::getAnimationSpeed() const +{ + return this->g_animationSpeed; +} +template +bool ObjChart::isAnimating() const +{ + return this->g_animating; +} +template +void ObjChart::finishAnimation() +{ + this->applyTargets(true); +} + +template +void ObjChart::setFont(fge::Font font) +{ + this->g_font = std::move(font); + this->g_needGeometryUpdate = true; +} +template +fge::Font const& ObjChart::getFont() const +{ + return this->g_font; +} +template +void ObjChart::setCharacterSize(fge::CharacterSize size) +{ + this->g_characterSize = size; + this->g_needGeometryUpdate = true; +} +template +fge::CharacterSize ObjChart::getCharacterSize() const +{ + return this->g_characterSize; +} + +template +void ObjChart::setPointShape(PointShapes shape) +{ + this->g_pointShape = shape; + this->g_needGeometryUpdate = true; +} +template +typename ObjChart::PointShapes ObjChart::getPointShape() const +{ + return this->g_pointShape; +} +template +void ObjChart::setPointSize(float size) +{ + this->g_pointSize = std::max(size, 0.0f); + this->g_needGeometryUpdate = true; +} +template +float ObjChart::getPointSize() const +{ + return this->g_pointSize; +} + +template +void ObjChart::setDefaultPointColor(fge::Color color) +{ + this->g_colorDefaultPoint = color; + this->g_needGeometryUpdate = true; +} +template +fge::Color ObjChart::getDefaultPointColor() const +{ + return this->g_colorDefaultPoint; +} +template +void ObjChart::setBackgroundColor(fge::Color color) +{ + this->g_colorBackground = color; + this->g_needGeometryUpdate = true; +} +template +fge::Color ObjChart::getBackgroundColor() const +{ + return this->g_colorBackground; +} +template +void ObjChart::setAxisColor(fge::Color color) +{ + this->g_colorAxis = color; + this->g_needGeometryUpdate = true; +} +template +fge::Color ObjChart::getAxisColor() const +{ + return this->g_colorAxis; +} +template +void ObjChart::setGridColor(fge::Color color) +{ + this->g_colorGrid = color; + this->g_needGeometryUpdate = true; +} +template +fge::Color ObjChart::getGridColor() const +{ + return this->g_colorGrid; +} +template +void ObjChart::setLabelColor(fge::Color color) +{ + this->g_colorLabel = color; + this->g_needGeometryUpdate = true; +} +template +fge::Color ObjChart::getLabelColor() const +{ + return this->g_colorLabel; +} + +template +void ObjChart::showGrid(bool show) +{ + this->g_showGrid = show; + this->g_needGeometryUpdate = true; +} +template +bool ObjChart::isGridShown() const +{ + return this->g_showGrid; +} + +template +fge::Vector2f ObjChart::mapValueToLocal(Point const& value) const +{ + auto const plotArea = this->getPlotArea(); + auto const& rangeX = this->g_axisX._displayed; + auto const& rangeY = this->g_axisY._displayed; + + T const ratioX = rangeX.getLength() != T{0} ? (value.x - rangeX._min) / rangeX.getLength() : static_cast(0.5); + T const ratioY = rangeY.getLength() != T{0} ? (value.y - rangeY._min) / rangeY.getLength() : static_cast(0.5); + + return {plotArea._x + static_cast(ratioX) * plotArea._width, + plotArea._y + plotArea._height - static_cast(ratioY) * plotArea._height}; +} +template +typename ObjChart::Point ObjChart::mapLocalToValue(fge::Vector2f const& position) const +{ + auto const plotArea = this->getPlotArea(); + auto const& rangeX = this->g_axisX._displayed; + auto const& rangeY = this->g_axisY._displayed; + + T const ratioX = plotArea._width != 0.0f ? static_cast((position.x - plotArea._x) / plotArea._width) : T{0}; + T const ratioY = plotArea._height != 0.0f + ? static_cast((plotArea._y + plotArea._height - position.y) / plotArea._height) + : T{0}; + + return {rangeX._min + ratioX * rangeX.getLength(), rangeY._min + ratioY * rangeY.getLength()}; +} + +template +char const* ObjChart::getClassName() const +{ + if constexpr (std::same_as) + { + return FGE_OBJCHART_CLASSNAME_FLOAT; + } + else + { + return FGE_OBJCHART_CLASSNAME_DOUBLE; + } +} +template +char const* ObjChart::getReadableClassName() const +{ + return "chart"; +} + +template +fge::RectFloat ObjChart::getGlobalBounds() const +{ + return this->getTransform() * this->getLocalBounds(); +} +template +fge::RectFloat ObjChart::getLocalBounds() const +{ + return {{0.0f, 0.0f}, this->g_size}; +} + +template +void ObjChart::onGuiResized([[maybe_unused]] fge::GuiElementHandler const& handler, fge::Vector2f const& size) +{ + this->refreshSize(size); +} + +template +void ObjChart::refreshSize(fge::Vector2f const& targetSize) +{ + if (!this->g_dynamicSize) + { + return; + } + + auto const size = this->g_dynamicSize->getSize(this->getPosition(), targetSize); + fge::Vector2f const newSize{std::max(size.x, 0.0f), std::max(size.y, 0.0f)}; + if (newSize != this->g_size) + { + this->g_size = newSize; + this->g_needGeometryUpdate = true; + } +} +template +void ObjChart::refreshAutoRanges(AxisData& axis, bool isXAxis) +{ + if (axis._mode != RangeModes::RANGE_AUTO) + { + return; + } + + T min = std::numeric_limits::max(); + T max = std::numeric_limits::lowest(); + for (auto const& point: this->g_points) + { + T const value = isXAxis ? point._target.x : point._target.y; + if (std::isfinite(value)) + { + min = std::min(min, value); + max = std::max(max, value); + } + } + + if (min > max) + { //No valid points + axis._target = {T{0}, T{1}}; + return; + } + + if (max - min <= T{0}) + { + T const pad = min != T{0} ? std::abs(min) * static_cast(0.1) : T{1}; + min -= pad; + max += pad; + } + else + { //Keep a small margin so points don't touch the borders + T const pad = (max - min) * static_cast(0.05); + min -= pad; + max += pad; + } + + T const step = computeStep(max - min, axis._tickCount); + axis._target = {std::floor(min / step) * step, std::ceil(max / step) * step}; +} +template +void ObjChart::refreshAutoRanges() +{ + this->refreshAutoRanges(this->g_axisX, true); + this->refreshAutoRanges(this->g_axisY, false); +} +template +void ObjChart::applyTargets(bool forceSnap) +{ + if (forceSnap || !this->g_animationEnabled) + { + this->g_axisX._displayed = this->g_axisX._target; + this->g_axisY._displayed = this->g_axisY._target; + for (auto& point: this->g_points) + { + point._displayed = point._target; + } + this->g_animating = false; + } + else + { + this->g_animating = true; + } + + this->g_needGeometryUpdate = true; +} + +template +void ObjChart::ensureGeometryUpdate() const +{ + if (!this->g_needGeometryUpdate) + { + return; + } + this->g_needGeometryUpdate = false; + + this->g_frameVertices.clear(); + this->g_pointVertices.clear(); + + this->g_frameVertices.appendRect({0.0f, 0.0f}, this->g_size, this->g_colorBackground); + + auto const plotArea = this->getPlotArea(); + if (plotArea._width <= 0.0f || plotArea._height <= 0.0f) + { + this->g_xLabels.clear(); + this->g_yLabels.clear(); + return; + } + + //Grid, ticks and labels + this->buildAxis(this->g_axisX, true, plotArea); + this->buildAxis(this->g_axisY, false, plotArea); + + //Axis lines + this->g_frameVertices.appendRect({plotArea._x - FGE_OBJCHART_LINE_THICKNESS, plotArea._y}, + {FGE_OBJCHART_LINE_THICKNESS, plotArea._height + FGE_OBJCHART_LINE_THICKNESS}, + this->g_colorAxis); + + this->g_frameVertices.appendRect({plotArea._x - FGE_OBJCHART_LINE_THICKNESS, plotArea._y + plotArea._height}, + {plotArea._width + FGE_OBJCHART_LINE_THICKNESS, FGE_OBJCHART_LINE_THICKNESS}, + this->g_colorAxis); + + //Points + if (this->g_pointSize <= 0.0f) + { + return; + } + + float const halfSize = this->g_pointSize / 2.0f; + constexpr float tolerance = 0.5f; + + for (auto const& point: this->g_points) + { + auto const center = this->mapValueToLocal(point._displayed); + if (!std::isfinite(center.x) || !std::isfinite(center.y) || center.x < plotArea._x - tolerance || + center.y < plotArea._y - tolerance || center.x > plotArea._x + plotArea._width + tolerance || + center.y > plotArea._y + plotArea._height + tolerance) + { + continue; + } + + fge::Color const color = point._color.value_or(this->g_colorDefaultPoint); + + switch (this->g_pointShape) + { + case PointShapes::SHAPE_SQUARE: + this->g_pointVertices.appendRect(center - fge::Vector2f{halfSize, halfSize}, + {this->g_pointSize, this->g_pointSize}, color); + break; + case PointShapes::SHAPE_DIAMOND: + { + fge::Vector2f const top{center.x, center.y - halfSize}; + fge::Vector2f const right{center.x + halfSize, center.y}; + fge::Vector2f const bottom{center.x, center.y + halfSize}; + fge::Vector2f const left{center.x - halfSize, center.y}; + this->g_pointVertices.appendTriangle(top, right, bottom, color); + this->g_pointVertices.appendTriangle(top, bottom, left, color); + break; + } + case PointShapes::SHAPE_CIRCLE: + { + this->g_pointVertices.appendCircle(center, halfSize, color, FGE_OBJCHART_CIRCLE_SEGMENTS); + break; + } + } + } +} +template +void ObjChart::buildAxis(AxisData const& axis, bool isXAxis, fge::RectFloat const& plotArea) const +{ + auto& labels = isXAxis ? this->g_xLabels : this->g_yLabels; + std::size_t labelCount = 0; + + Range const& range = axis._displayed; + T const step = computeStep(range.getLength(), axis._tickCount); + T const firstTick = std::ceil(range._min / step) * step; + + //Number of decimals needed to display the step + int const decimals = + step >= T{1} ? 0 : std::clamp(static_cast(std::ceil(-std::log10(step) - static_cast(1e-6))), 0, 12); + + bool const haveFont = this->g_font.valid(); + + for (std::size_t i = 0; i < FGE_OBJCHART_MAX_TICKS; ++i) + { + T value = firstTick + static_cast(i) * step; + if (value > range._max + step * static_cast(1e-6)) + { + break; + } + if (std::abs(value) < step * static_cast(1e-6)) + { //Avoid "-0" + value = T{0}; + } + + auto const position = std::round(isXAxis ? this->mapValueToLocal({value, range._min}).x + : this->mapValueToLocal({this->g_axisX._displayed._min, value}).y); + + if (isXAxis) + { + if (this->g_showGrid) + { + this->g_frameVertices.appendRect({position, plotArea._y}, + {FGE_OBJCHART_LINE_THICKNESS, plotArea._height}, this->g_colorGrid); + } + this->g_frameVertices.appendRect({position, plotArea._y + plotArea._height}, + {FGE_OBJCHART_LINE_THICKNESS, FGE_OBJCHART_TICK_LENGTH}, + this->g_colorAxis); + } + else + { + if (this->g_showGrid) + { + this->g_frameVertices.appendRect({plotArea._x, position}, + {plotArea._width, FGE_OBJCHART_LINE_THICKNESS}, this->g_colorGrid); + } + this->g_frameVertices.appendRect( + {plotArea._x - FGE_OBJCHART_TICK_LENGTH - FGE_OBJCHART_LINE_THICKNESS, position}, + {FGE_OBJCHART_TICK_LENGTH, FGE_OBJCHART_LINE_THICKNESS}, this->g_colorAxis); + } + + if (!haveFont) + { + continue; + } + + char buffer[64]; + int const length = std::snprintf(buffer, sizeof(buffer), "%.*f", decimals, static_cast(value)); + if (length <= 0) + { + continue; + } + + if (labelCount >= labels.size()) + { + labels.emplace_back(); + } + auto& label = labels[labelCount++]; + label.setFont(this->g_font); + label.setCharacterSize(this->g_characterSize); + label.setFillColor(this->g_colorLabel); + //Note: passing the char array directly would use the whole array as a literal (including garbage) + label.setString( + tiny_utf8::string{buffer, std::min(static_cast(length), sizeof(buffer) - 1)}); + + auto const bounds = label.getLocalBounds(); + if (isXAxis) + { + label.setOrigin({std::round(bounds._x + bounds._width / 2.0f), bounds._y}); + label.setPosition({position, plotArea._y + plotArea._height + FGE_OBJCHART_TICK_LENGTH + 3.0f}); + } + else + { + label.setOrigin({bounds._x + bounds._width, std::round(bounds._y + bounds._height / 2.0f)}); + label.setPosition({plotArea._x - FGE_OBJCHART_TICK_LENGTH - FGE_OBJCHART_LINE_THICKNESS - 3.0f, position}); + } + } + + labels.resize(labelCount); +} + +template +T ObjChart::computeStep(T length, std::size_t tickCount) +{ + if (!(length > T{0}) || !std::isfinite(length)) + { + return T{1}; + } + + T const rawStep = length / static_cast(std::max(tickCount, 1)); + T const magnitude = std::pow(T{10}, std::floor(std::log10(rawStep))); + T const normalized = rawStep / magnitude; + + T step; + if (normalized < T{1.5}) + { + step = T{1}; + } + else if (normalized < T{3}) + { + step = T{2}; + } + else if (normalized < T{7}) + { + step = T{5}; + } + else + { + step = T{10}; + } + + return step * magnitude; +} + +} // namespace fge diff --git a/use_clang_format.sh b/use_clang_format.sh index 69a7b0f3..0412d1d4 100644 --- a/use_clang_format.sh +++ b/use_clang_format.sh @@ -12,3 +12,6 @@ find includes/private/ -iname *.hpp -o -iname *.cpp -o -iname *.inl | find examples/ -iname *.hpp -o -iname *.cpp -o -iname *.inl | xargs clang-format --style=file --verbose -i + +find extensions/ -iname *.hpp -o -iname *.cpp -o -iname *.inl | + xargs clang-format --style=file --verbose -i diff --git a/verify_clang_format.sh b/verify_clang_format.sh index 274dbc9c..7514a546 100755 --- a/verify_clang_format.sh +++ b/verify_clang_format.sh @@ -14,3 +14,6 @@ find includes/private/ -iname *.hpp -o -iname *.cpp -o -iname *.inl | find examples/ -iname *.hpp -o -iname *.cpp -o -iname *.inl | xargs clang-format --style=file --Werror --ferror-limit=1 --verbose -n + +find extensions/ -iname *.hpp -o -iname *.cpp -o -iname *.inl | + xargs clang-format --style=file --Werror --ferror-limit=1 --verbose -n From ad92cd94b754e72b65047bee82638ba21a1e93ad Mon Sep 17 00:00:00 2001 From: "GuillaumeG." Date: Sun, 1 Nov 2026 20:56:56 +0100 Subject: [PATCH 3/8] add extensions/README.md --- extensions/README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 extensions/README.md diff --git a/extensions/README.md b/extensions/README.md new file mode 100644 index 00000000..664b975e --- /dev/null +++ b/extensions/README.md @@ -0,0 +1,32 @@ +# FastEngine extensions + +Extensions are optional modules built on top of FastEngine. They are not part of the core library: +link only the ones you need. + +Extensions are enabled with the `FGE_BUILD_EXTENSIONS` CMake option (`ON` by default). + +| Extension | CMake target | Description | +|-----------|------------------------|-----------------------------------------------| +| ObjChart | `FastEngine::ObjChart` | Point chart with linear axes and animated points | + +## ObjChart + +A header-only point chart (scatter plot) object. Data can be stored as `float` (`fge::ObjChartF`) +or `double` (`fge::ObjChartD`). + +- Linear X/Y axes, automatic ticks or fixed ranges +- Optional grid and tick labels (when a font is set) +- Optional animation: a modified point gently moves toward its new position +- Fixed size or `fge::DynamicSize` (e.g. fill a `fge::ObjWindow`) + +```cpp +#include "FastEngine/extensions/C_objChart.hpp" + +auto* chart = scene.newObject(); +chart->setFont("base"); +chart->setSize({400.0f, 250.0f}); +chart->addPoint({1.0, 2.5}); +chart->setPoint(0, {1.0, 4.0}); // animated +``` + +See `examples/objChart_010` for a complete example. From 16aa4df603402e5dd2682a913ee8347bc431e4f8 Mon Sep 17 00:00:00 2001 From: "GuillaumeG." Date: Sun, 1 Nov 2026 21:11:04 +0100 Subject: [PATCH 4/8] bump spirv_reflect to vulkan-sdk-1.4.304.1. This follow version of all Vulkan deps - Completely forgot to update this hard-dependency --- includes/private/spirv_reflect.h | 12 ++- sources/private/spirv_reflect.c | 146 +++++++++++++++++++++++++------ 2 files changed, 128 insertions(+), 30 deletions(-) diff --git a/includes/private/spirv_reflect.h b/includes/private/spirv_reflect.h index 77d5d1a5..2b1eb0ac 100644 --- a/includes/private/spirv_reflect.h +++ b/includes/private/spirv_reflect.h @@ -393,7 +393,9 @@ typedef struct SpvReflectTypeDescription { const char* type_name; // Non-NULL if type is member of a struct const char* struct_member_name; - SpvStorageClass storage_class; + + // The storage class (SpvStorageClass) if the type, and -1 if it does not have a storage class. + int storage_class; SpvReflectTypeFlags type_flags; SpvReflectDecorationFlags decoration_flags; @@ -429,7 +431,9 @@ typedef struct SpvReflectInterfaceVariable { SpvStorageClass storage_class; const char* semantic; SpvReflectDecorationFlags decoration_flags; - SpvBuiltIn built_in; + + // The builtin id (SpvBuiltIn) if the variable is a builtin, and -1 otherwise. + int built_in; SpvReflectNumericTraits numeric; SpvReflectArrayTraits array; @@ -1801,9 +1805,9 @@ inline const char* ShaderModule::GetEntryPointName() const { return this->GetEntryPointName(0); } -/*! @fn GetEntryPoint +/*! @fn GetSourceFile - @return Returns entry point + @return Returns source file */ inline const char* ShaderModule::GetSourceFile() const { diff --git a/sources/private/spirv_reflect.c b/sources/private/spirv_reflect.c index 7ea4ed67..0100edfa 100644 --- a/sources/private/spirv_reflect.c +++ b/sources/private/spirv_reflect.c @@ -160,10 +160,14 @@ typedef struct SpvReflectPrvAccessedVariable { SpvReflectPrvNode* p_node; uint32_t result_id; uint32_t variable_ptr; + uint32_t function_id; + uint32_t function_parameter_index; } SpvReflectPrvAccessedVariable; typedef struct SpvReflectPrvFunction { uint32_t id; + uint32_t parameter_count; + uint32_t* parameters; uint32_t callee_count; uint32_t* callees; struct SpvReflectPrvFunction** callee_ptrs; @@ -642,6 +646,7 @@ static void DestroyParser(SpvReflectPrvParser* p_parser) { // Free functions for (size_t i = 0; i < p_parser->function_count; ++i) { + SafeFree(p_parser->functions[i].parameters); SafeFree(p_parser->functions[i].callees); SafeFree(p_parser->functions[i].callee_ptrs); SafeFree(p_parser->functions[i].accessed_variables); @@ -1095,6 +1100,7 @@ static SpvReflectResult ParseFunction(SpvReflectPrvParser* p_parser, SpvReflectP size_t first_label_index) { p_func->id = p_func_node->result_id; + p_func->parameter_count = 0; p_func->callee_count = 0; p_func->accessed_variable_count = 0; @@ -1105,7 +1111,11 @@ static SpvReflectResult ParseFunction(SpvReflectPrvParser* p_parser, SpvReflectP break; } switch (p_node->op) { + case SpvOpFunctionParameter: { + ++(p_func->parameter_count); + } break; case SpvOpFunctionCall: { + p_func->accessed_variable_count += p_node->word_count - 4; ++(p_func->callee_count); } break; case SpvOpLoad: @@ -1128,6 +1138,13 @@ static SpvReflectResult ParseFunction(SpvReflectPrvParser* p_parser, SpvReflectP } } + if (p_func->parameter_count > 0) { + p_func->parameters = (uint32_t*)calloc(p_func->parameter_count, sizeof(*(p_func->parameters))); + if (IsNull(p_func->parameters)) { + return SPV_REFLECT_RESULT_ERROR_ALLOC_FAILED; + } + } + if (p_func->callee_count > 0) { p_func->callees = (uint32_t*)calloc(p_func->callee_count, sizeof(*(p_func->callees))); if (IsNull(p_func->callees)) { @@ -1143,6 +1160,7 @@ static SpvReflectResult ParseFunction(SpvReflectPrvParser* p_parser, SpvReflectP } } + p_func->parameter_count = 0; p_func->callee_count = 0; p_func->accessed_variable_count = 0; // Now have allocation, fill in values @@ -1152,8 +1170,25 @@ static SpvReflectResult ParseFunction(SpvReflectPrvParser* p_parser, SpvReflectP break; } switch (p_node->op) { + case SpvOpFunctionParameter: { + CHECKED_READU32(p_parser, p_node->word_offset + 2, p_func->parameters[p_func->parameter_count]); + (++p_func->parameter_count); + } break; case SpvOpFunctionCall: { CHECKED_READU32(p_parser, p_node->word_offset + 3, p_func->callees[p_func->callee_count]); + const uint32_t result_index = p_node->word_offset + 2; + for (uint32_t j = 0, parameter_count = p_node->word_count - 4; j < parameter_count; j++) { + const uint32_t ptr_index = p_node->word_offset + 4 + j; + SpvReflectPrvAccessedVariable* access_ptr = &p_func->accessed_variables[p_func->accessed_variable_count]; + + access_ptr->p_node = p_node; + // Need to track Result ID as not sure there has been any memory access through here yet + CHECKED_READU32(p_parser, result_index, access_ptr->result_id); + CHECKED_READU32(p_parser, ptr_index, access_ptr->variable_ptr); + access_ptr->function_id = p_func->callees[p_func->callee_count]; + access_ptr->function_parameter_index = j; + (++p_func->accessed_variable_count); + } (++p_func->callee_count); } break; case SpvOpLoad: @@ -1238,14 +1273,12 @@ static SpvReflectResult ParseFunctions(SpvReflectPrvParser* p_parser) { // Skip over function declarations that aren't definitions bool func_definition = false; - // Intentionally reuse i to avoid iterating over these nodes more than - // once - for (; i < p_parser->node_count; ++i) { - if (p_parser->nodes[i].op == SpvOpLabel) { + for (size_t j = i; j < p_parser->node_count; ++j) { + if (p_parser->nodes[j].op == SpvOpLabel) { func_definition = true; break; } - if (p_parser->nodes[i].op == SpvOpFunctionEnd) { + if (p_parser->nodes[j].op == SpvOpFunctionEnd) { break; } } @@ -1953,6 +1986,13 @@ static SpvReflectResult ParseType(SpvReflectPrvParser* p_parser, SpvReflectPrvNo p_type->type_flags |= SPV_REFLECT_TYPE_FLAG_REF; IF_READU32_CAST(result, p_parser, p_node->word_offset + 2, SpvStorageClass, p_type->storage_class); + SpvReflectPrvNode* p_next_node = FindNode(p_parser, p_node->type_id); + if (IsNull(p_next_node)) { + result = SPV_REFLECT_RESULT_ERROR_SPIRV_INVALID_ID_REFERENCE; + SPV_REFLECT_ASSERT(false); + break; + } + bool found_recursion = false; if (p_type->storage_class == SpvStorageClassPhysicalStorageBuffer) { // Need to make sure we haven't started an infinite recursive loop @@ -1964,7 +2004,7 @@ static SpvReflectResult ParseType(SpvReflectPrvParser* p_parser, SpvReflectPrvNo return SPV_REFLECT_RESULT_SUCCESS; } } - if (!found_recursion) { + if (!found_recursion && p_next_node->op == SpvOpTypeStruct) { p_parser->physical_pointer_struct_count++; p_parser->physical_pointer_check[p_parser->physical_pointer_count] = p_type; p_parser->physical_pointer_count++; @@ -1974,12 +2014,7 @@ static SpvReflectResult ParseType(SpvReflectPrvParser* p_parser, SpvReflectPrvNo } } - // Parse type - SpvReflectPrvNode* p_next_node = FindNode(p_parser, p_node->type_id); - if (IsNull(p_next_node)) { - result = SPV_REFLECT_RESULT_ERROR_SPIRV_INVALID_ID_REFERENCE; - SPV_REFLECT_ASSERT(false); - } else if (!found_recursion) { + if (!found_recursion) { if (p_next_node->op == SpvOpTypeStruct) { p_type->struct_type_description = FindType(p_module, p_next_node->result_id); } @@ -2155,7 +2190,8 @@ static SpvReflectResult ParseDescriptorBindings(SpvReflectPrvParser* p_parser, S // from the pointer so that we can use it to deduce deescriptor types. SpvStorageClass pointer_storage_class = SpvStorageClassMax; if (p_type->op == SpvOpTypePointer) { - pointer_storage_class = p_type->storage_class; + assert(p_type->storage_class != -1 && "Pointer types must have a valid storage class."); + pointer_storage_class = (SpvStorageClass)p_type->storage_class; // Find the type's node SpvReflectPrvNode* p_type_node = FindNode(p_parser, p_type->id); if (IsNull(p_type_node)) { @@ -2511,15 +2547,19 @@ static SpvReflectResult ParseDescriptorBlockVariable(SpvReflectPrvParser* p_pars } } if (!found_recursion) { - uint32_t struct_id = FindType(p_module, p_member_type->id)->struct_type_description->id; - p_parser->physical_pointer_structs[p_parser->physical_pointer_struct_count].struct_id = struct_id; - p_parser->physical_pointer_structs[p_parser->physical_pointer_struct_count].p_var = p_member_var; - p_parser->physical_pointer_struct_count++; - - p_parser->physical_pointer_check[p_parser->physical_pointer_count] = p_member_type; - p_parser->physical_pointer_count++; - if (p_parser->physical_pointer_count >= MAX_RECURSIVE_PHYSICAL_POINTER_CHECK) { - return SPV_REFLECT_RESULT_ERROR_SPIRV_MAX_RECURSIVE_EXCEEDED; + SpvReflectTypeDescription* struct_type = FindType(p_module, p_member_type->id); + // could be pointer directly to non-struct type here + if (struct_type->struct_type_description) { + uint32_t struct_id = struct_type->struct_type_description->id; + p_parser->physical_pointer_structs[p_parser->physical_pointer_struct_count].struct_id = struct_id; + p_parser->physical_pointer_structs[p_parser->physical_pointer_struct_count].p_var = p_member_var; + p_parser->physical_pointer_struct_count++; + + p_parser->physical_pointer_check[p_parser->physical_pointer_count] = p_member_type; + p_parser->physical_pointer_count++; + if (p_parser->physical_pointer_count >= MAX_RECURSIVE_PHYSICAL_POINTER_CHECK) { + return SPV_REFLECT_RESULT_ERROR_SPIRV_MAX_RECURSIVE_EXCEEDED; + } } } @@ -2690,7 +2730,13 @@ static SpvReflectResult ParseDescriptorBlockVariableSizes(SpvReflectPrvParser* p if (IsNull(p_member_type)) { return SPV_REFLECT_RESULT_ERROR_SPIRV_INVALID_ID_REFERENCE; } - assert(p_member_type->op == SpvOpTypeStruct); + + // If we found a struct, we need to fall through and get the size of it or else we grab the size here + if (p_member_type->op != SpvOpTypeStruct) { + // TODO - we need to rework this loop as a function to get size for each type + // (or maybe determine this size doesn't matter if not a struct in the pointer) + break; + } FALLTHROUGH; } @@ -3439,6 +3485,44 @@ static SpvReflectResult ParseByteAddressBuffer(SpvReflectPrvParser* p_parser, Sp return SPV_REFLECT_RESULT_SUCCESS; } +static SpvReflectResult ParseFunctionParameterAccess(SpvReflectPrvParser* p_parser, uint32_t callee_function_id, + uint32_t function_parameter_index, uint32_t* p_accessed) { + SpvReflectPrvFunction* p_func = NULL; + for (size_t i = 0; i < p_parser->function_count; ++i) { + if (p_parser->functions[i].id == callee_function_id) { + p_func = &(p_parser->functions[i]); + break; + } + } + if (p_func == NULL) { + return SPV_REFLECT_RESULT_ERROR_SPIRV_INVALID_ID_REFERENCE; + } + + assert(function_parameter_index < p_func->parameter_count); + + for (size_t i = 0; i < p_func->accessed_variable_count; ++i) { + if (p_func->parameters[function_parameter_index] == p_func->accessed_variables[i].variable_ptr) { + SpvReflectPrvAccessedVariable* p_var = &p_func->accessed_variables[i]; + if (p_var->function_id > 0) { + SpvReflectResult result = + ParseFunctionParameterAccess(p_parser, p_var->function_id, p_var->function_parameter_index, p_accessed); + if (result != SPV_REFLECT_RESULT_SUCCESS) { + return result; + } + } else { + *p_accessed = 1; + } + // Early out as soon as p_accessed is true + if (*p_accessed) { + return SPV_REFLECT_RESULT_SUCCESS; + } + } + } + + *p_accessed = 0; + return SPV_REFLECT_RESULT_SUCCESS; +} + static SpvReflectResult ParseStaticallyUsedResources(SpvReflectPrvParser* p_parser, SpvReflectShaderModule* p_module, SpvReflectEntryPoint* p_entry, size_t uniform_count, uint32_t* uniforms, size_t push_constant_count, uint32_t* push_constants) { @@ -3535,8 +3619,18 @@ static SpvReflectResult ParseStaticallyUsedResources(SpvReflectPrvParser* p_pars uint32_t byte_address_buffer_offset_count = 0; for (uint32_t j = 0; j < used_acessed_count; j++) { - if (p_used_accesses[j].variable_ptr == p_binding->spirv_id) { - p_binding->accessed = 1; + SpvReflectPrvAccessedVariable* p_var = &p_used_accesses[j]; + if (p_var->variable_ptr == p_binding->spirv_id) { + if (p_var->function_id > 0) { + result = + ParseFunctionParameterAccess(p_parser, p_var->function_id, p_var->function_parameter_index, &p_binding->accessed); + if (result != SPV_REFLECT_RESULT_SUCCESS) { + SafeFree(p_used_accesses); + return result; + } + } else { + p_binding->accessed = 1; + } if (HasByteAddressBufferOffset(p_used_accesses[j].p_node, p_binding)) { byte_address_buffer_offset_count++; @@ -3934,7 +4028,7 @@ static SpvReflectResult ParsePushConstantBlocks(SpvReflectPrvParser* p_parser, S } p_push_constant->name = p_node->name; - result = ParseDescriptorBlockVariableSizes(p_parser, p_module, true, false, false, p_push_constant); + result = ParseDescriptorBlockVariableSizes(p_parser, p_module, true, false, true, p_push_constant); if (result != SPV_REFLECT_RESULT_SUCCESS) { return result; } From d2ffeaf759088cd35f89e7d5cc0e49aed2e71d16 Mon Sep 17 00:00:00 2001 From: "GuillaumeG." Date: Sun, 1 Nov 2026 21:18:19 +0100 Subject: [PATCH 5/8] fix: make sure compiler is forwarded to all deps when building --- .github/workflows/cmake_linux_gcc_64bit.yml | 2 +- .github/workflows/codeql.yml | 2 +- deps/CMakeLists.txt | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cmake_linux_gcc_64bit.yml b/.github/workflows/cmake_linux_gcc_64bit.yml index 7f54a9ab..289ae82c 100644 --- a/.github/workflows/cmake_linux_gcc_64bit.yml +++ b/.github/workflows/cmake_linux_gcc_64bit.yml @@ -45,7 +45,7 @@ jobs: shell: bash working-directory: ${{github.workspace}}/deps run: | - cmake . -D CMAKE_BUILD_TYPE=$BUILD_TYPE + cmake . -D CMAKE_C_COMPILER=gcc-11 -D CMAKE_CXX_COMPILER=g++-11 -D CMAKE_BUILD_TYPE=$BUILD_TYPE cmake --build . -j ${{steps.cpu-cores.outputs.count}} --config $BUILD_TYPE - name: Configure CMake diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f489281c..e98e53b5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -57,7 +57,7 @@ jobs: shell: bash working-directory: ${{github.workspace}}/deps run: | - cmake . -D CMAKE_BUILD_TYPE=$BUILD_TYPE + cmake . -D CMAKE_C_COMPILER=gcc-11 -D CMAKE_CXX_COMPILER=g++-11 -D CMAKE_BUILD_TYPE=$BUILD_TYPE cmake --build . -j ${{steps.cpu-cores.outputs.count}} --config $BUILD_TYPE # Initializes the CodeQL tools for scanning. diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 1ddae7f3..6e593a56 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -12,13 +12,16 @@ set(CMAKE_INSTALL_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/install/" CACHE PATH "" FO file(WRITE ${DEPS_GLOBAL_CMAKE_FILE} "") +#Forward the compilers to every dependency, mixing compiler versions breaks the final link (libstdc++ ABI) +set(DEPS_COMPILER_ARGS -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}) + function(AddCmakeDependency NAME URL TAG SHARED ARGS SOURCE_SUBDIR) separate_arguments(ARGS UNIX_COMMAND "${ARGS}") ExternalProject_Add(${NAME} GIT_REPOSITORY ${URL} GIT_TAG ${TAG} - CMAKE_ARGS -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=${SHARED} ${ARGS} + CMAKE_ARGS -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=${SHARED} ${ARGS} ${DEPS_COMPILER_ARGS} CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH=${CMAKE_CURRENT_BINARY_DIR}/libs/${NAME} PREFIX project/${NAME} TEST_COMMAND "" @@ -37,7 +40,7 @@ function(AddPatchedDependency NAME URL TAG SHARED ARGS SOURCE_SUBDIR PATCH APPEN ExternalProject_Add(${NAME} GIT_REPOSITORY ${URL} GIT_TAG ${TAG} - CMAKE_ARGS -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=${SHARED} ${ARGS} + CMAKE_ARGS -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=${SHARED} ${ARGS} ${DEPS_COMPILER_ARGS} CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH=${CMAKE_CURRENT_BINARY_DIR}/libs/${NAME} PREFIX project/${NAME} PATCH_COMMAND ${GIT_EXECUTABLE} clean --force -d -x && From 9092bbaa9e4d46f25b7f6c21301a36685d22d37e Mon Sep 17 00:00:00 2001 From: "GuillaumeG." Date: Sun, 1 Nov 2026 21:40:00 +0100 Subject: [PATCH 6/8] Fix macos build --- sources/private/fge_crypt.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/sources/private/fge_crypt.cpp b/sources/private/fge_crypt.cpp index 85a24217..2a16d485 100644 --- a/sources/private/fge_crypt.cpp +++ b/sources/private/fge_crypt.cpp @@ -78,13 +78,24 @@ bool CryptGenerateKeyAndCertificate(EVP_PKEY*& privateKey, X509*& certificate) X509_set_pubkey(certificate, privateKey); // Set certificate subject and issuer name - X509_NAME* name = X509_get_subject_name(certificate); + // Note: since OpenSSL 4, X509_get_subject_name() returns a const pointer, so the name can't be + // modified in place anymore. We build our own name and let X509_set_*_name() copy it. + X509_NAME* name = X509_NAME_new(); + if (name == nullptr) + { + std::cerr << "Failed to create X509 name" << std::endl; + EVP_PKEY_free(privateKey); + X509_free(certificate); + return false; + } X509_NAME_add_entry_by_txt(name, "C", MBSTRING_ASC, reinterpret_cast("CH"), -1, -1, 0); X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, reinterpret_cast("FGE"), -1, -1, 0); X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, reinterpret_cast("https://github.com/JonathSpirit/FastEngine.git"), -1, -1, 0); + X509_set_subject_name(certificate, name); X509_set_issuer_name(certificate, name); + X509_NAME_free(name); // Sign the certificate with the key if (X509_sign(certificate, privateKey, EVP_sha256()) == 0) From 9903e3736784b594803dc14c5294b4375fd11c70 Mon Sep 17 00:00:00 2001 From: "GuillaumeG." Date: Sun, 1 Nov 2026 21:40:27 +0100 Subject: [PATCH 7/8] sdl_mixer: avoid installing examples and tests --- deps/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 6e593a56..535987d3 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -136,7 +136,7 @@ AddCmakeDependency(SDL3_mixer https://github.com/libsdl-org/SDL_mixer.git release-3.2.4 ON - "-DSDL3MIXER_VENDORED=ON -DCMAKE_PREFIX_PATH=${CMAKE_CURRENT_BINARY_DIR}/libs/SDL3" + "-DSDL3MIXER_VENDORED=ON -DCMAKE_PREFIX_PATH=${CMAKE_CURRENT_BINARY_DIR}/libs/SDL3 -DSDLMIXER_TESTS_INSTALL=OFF -DSDLMIXER_EXAMPLES_INSTALL=OFF" "") add_dependencies(SDL3_mixer SDL3) From 897971fb084be927631d11925c92d8473800941b Mon Sep 17 00:00:00 2001 From: "GuillaumeG." Date: Sun, 1 Nov 2026 22:06:03 +0100 Subject: [PATCH 8/8] ObjChart: adding straight lines connection --- examples/objChart_010/main.cpp | 6 + extensions/README.md | 1 + .../FastEngine/extensions/C_objChart.hpp | 19 +++ .../FastEngine/extensions/C_objChart.inl | 112 ++++++++++++++++++ 4 files changed, 138 insertions(+) diff --git a/examples/objChart_010/main.cpp b/examples/objChart_010/main.cpp index e528a16a..1f46bd20 100644 --- a/examples/objChart_010/main.cpp +++ b/examples/objChart_010/main.cpp @@ -129,6 +129,9 @@ class MainScene : public fge::Scene "A: toggle animation ("} + (objChart->isAnimationEnabled() ? "on" : "off") + ")\n" + "L: toggle lines (" + + (objChart->isLinesShown() ? "on" : "off") + + ")\n" "R: add a point, D: remove the last point\n" "S: change point shape (" + shapeName + ")"); @@ -144,6 +147,9 @@ class MainScene : public fge::Scene case SDLK_A: objChart->enableAnimation(!objChart->isAnimationEnabled()); break; + case SDLK_L: + objChart->showLines(!objChart->isLinesShown()); + break; case SDLK_R: { double x = 0.0; diff --git a/extensions/README.md b/extensions/README.md index 664b975e..96cb2413 100644 --- a/extensions/README.md +++ b/extensions/README.md @@ -16,6 +16,7 @@ or `double` (`fge::ObjChartD`). - Linear X/Y axes, automatic ticks or fixed ranges - Optional grid and tick labels (when a font is set) +- Optional straight lines linking consecutive points (in index order), shown by default - Optional animation: a modified point gently moves toward its new position - Fixed size or `fge::DynamicSize` (e.g. fill a `fge::ObjWindow`) diff --git a/extensions/objChart/includes/FastEngine/extensions/C_objChart.hpp b/extensions/objChart/includes/FastEngine/extensions/C_objChart.hpp index a013cf0b..a4f1ff5a 100644 --- a/extensions/objChart/includes/FastEngine/extensions/C_objChart.hpp +++ b/extensions/objChart/includes/FastEngine/extensions/C_objChart.hpp @@ -38,6 +38,7 @@ #define FGE_OBJCHART_DEFAULT_SIZE_Y 200.0f #define FGE_OBJCHART_DEFAULT_TICK_COUNT 5 #define FGE_OBJCHART_DEFAULT_POINT_SIZE 6.0f +#define FGE_OBJCHART_DEFAULT_LINE_THICKNESS 2.0f #define FGE_OBJCHART_DEFAULT_CHARACTER_SIZE 12 #define FGE_OBJCHART_DEFAULT_ANIMATION_SPEED 6.0f #define FGE_OBJCHART_TICK_LENGTH 4.0f @@ -59,6 +60,8 @@ concept ObjChartValueType = std::same_as || std::same_as; * \ingroup extensions * \brief A point chart (scatter plot) with linear axes * + * Consecutive points (in index order) can be linked by straight lines (enabled by default, see showLines()). + * * The chart can be configured to store its data as float or double (see ObjChartF and ObjChartD). * Axes ranges can be computed automatically from the points, or fixed by the user. * @@ -236,6 +239,18 @@ class ObjChart : public fge::Object, public fge::Subscriber void showGrid(bool show); [[nodiscard]] bool isGridShown() const; + /** + * \brief Show or hide the straight lines linking consecutive points + * + * Points are linked in index order (not sorted by X), a line is drawn between point i and point i+1. + */ + void showLines(bool show); + [[nodiscard]] bool isLinesShown() const; + void setLineThickness(float thickness); + [[nodiscard]] float getLineThickness() const; + void setLineColor(fge::Color color); + [[nodiscard]] fge::Color getLineColor() const; + //Mapping /** @@ -278,6 +293,7 @@ class ObjChart : public fge::Object, public fge::Subscriber void ensureGeometryUpdate() const; void buildAxis(AxisData const& axis, bool isXAxis, fge::RectFloat const& plotArea) const; + void appendLine(fge::Vector2f a, fge::Vector2f b, fge::RectFloat const& plotArea) const; [[nodiscard]] static T computeStep(T length, std::size_t tickCount); @@ -306,6 +322,9 @@ class ObjChart : public fge::Object, public fge::Subscriber fge::Color g_colorGrid{220, 220, 220}; fge::Color g_colorLabel{40, 40, 40}; bool g_showGrid{true}; + bool g_showLines{true}; + float g_lineThickness{FGE_OBJCHART_DEFAULT_LINE_THICKNESS}; + fge::Color g_colorLine{30, 110, 220}; mutable bool g_needGeometryUpdate{true}; mutable fge::vulkan::VertexBuffer g_frameVertices; diff --git a/extensions/objChart/includes/FastEngine/extensions/C_objChart.inl b/extensions/objChart/includes/FastEngine/extensions/C_objChart.inl index 0b3740b9..094b3950 100644 --- a/extensions/objChart/includes/FastEngine/extensions/C_objChart.inl +++ b/extensions/objChart/includes/FastEngine/extensions/C_objChart.inl @@ -505,6 +505,40 @@ bool ObjChart::isGridShown() const return this->g_showGrid; } +template +void ObjChart::showLines(bool show) +{ + this->g_showLines = show; + this->g_needGeometryUpdate = true; +} +template +bool ObjChart::isLinesShown() const +{ + return this->g_showLines; +} +template +void ObjChart::setLineThickness(float thickness) +{ + this->g_lineThickness = std::max(thickness, 0.0f); + this->g_needGeometryUpdate = true; +} +template +float ObjChart::getLineThickness() const +{ + return this->g_lineThickness; +} +template +void ObjChart::setLineColor(fge::Color color) +{ + this->g_colorLine = color; + this->g_needGeometryUpdate = true; +} +template +fge::Color ObjChart::getLineColor() const +{ + return this->g_colorLine; +} + template fge::Vector2f ObjChart::mapValueToLocal(Point const& value) const { @@ -688,6 +722,16 @@ void ObjChart::ensureGeometryUpdate() const {plotArea._width + FGE_OBJCHART_LINE_THICKNESS, FGE_OBJCHART_LINE_THICKNESS}, this->g_colorAxis); + //Lines, appended before the points so the points are drawn on top + if (this->g_showLines && this->g_lineThickness > 0.0f) + { + for (std::size_t i = 1; i < this->g_points.size(); ++i) + { + this->appendLine(this->mapValueToLocal(this->g_points[i - 1]._displayed), + this->mapValueToLocal(this->g_points[i]._displayed), plotArea); + } + } + //Points if (this->g_pointSize <= 0.0f) { @@ -826,6 +870,74 @@ void ObjChart::buildAxis(AxisData const& axis, bool isXAxis, fge::RectFloat c labels.resize(labelCount); } +template +void ObjChart::appendLine(fge::Vector2f a, fge::Vector2f b, fge::RectFloat const& plotArea) const +{ + if (!std::isfinite(a.x) || !std::isfinite(a.y) || !std::isfinite(b.x) || !std::isfinite(b.y)) + { + return; + } + + //Clip the segment to the plot area (Liang-Barsky), points outside the ranges must not draw over the labels + float const dx = b.x - a.x; + float const dy = b.y - a.y; + float const p[4] = {-dx, dx, -dy, dy}; + float const q[4] = {a.x - plotArea._x, plotArea._x + plotArea._width - a.x, a.y - plotArea._y, + plotArea._y + plotArea._height - a.y}; + + float t0 = 0.0f; + float t1 = 1.0f; + for (std::size_t i = 0; i < 4; ++i) + { + if (p[i] == 0.0f) + { //Parallel to this border + if (q[i] < 0.0f) + { + return; + } + continue; + } + + float const r = q[i] / p[i]; + if (p[i] < 0.0f) + { + if (r > t1) + { + return; + } + t0 = std::max(t0, r); + } + else + { + if (r < t0) + { + return; + } + t1 = std::min(t1, r); + } + } + + fge::Vector2f const start{a.x + t0 * dx, a.y + t0 * dy}; + fge::Vector2f const end{a.x + t1 * dx, a.y + t1 * dy}; + + //Build the segment as a quad of the line thickness + float const length = std::hypot(end.x - start.x, end.y - start.y); + if (length <= 1e-4f) + { + return; + } + + float const halfThickness = this->g_lineThickness / 2.0f; + fge::Vector2f const normal{-(end.y - start.y) / length * halfThickness, (end.x - start.x) / length * halfThickness}; + + fge::Vector2f const startA{start.x + normal.x, start.y + normal.y}; + fge::Vector2f const startB{start.x - normal.x, start.y - normal.y}; + fge::Vector2f const endA{end.x + normal.x, end.y + normal.y}; + fge::Vector2f const endB{end.x - normal.x, end.y - normal.y}; + + this->g_pointVertices.appendTriangle(startA, endA, endB, this->g_colorLine); + this->g_pointVertices.appendTriangle(startA, endB, startB, this->g_colorLine); +} template T ObjChart::computeStep(T length, std::size_t tickCount)