From ddfbe32cea03be0f016497ec0dcdd8c11d77a2b1 Mon Sep 17 00:00:00 2001 From: kbz_8 Date: Wed, 13 May 2026 14:29:51 +0200 Subject: [PATCH 1/4] Indev (#228) Co-authored-by: kbJeff-8 --- third_party/kvf.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/third_party/kvf.h b/third_party/kvf.h index 0af50cf..0eef858 100755 --- a/third_party/kvf.h +++ b/third_party/kvf.h @@ -3176,7 +3176,7 @@ void kvfGPipelineBuilderEnableAdditiveBlending(KvfGraphicsPipelineBuilder* build builder->color_blend_attachment_state.dstColorBlendFactor = VK_BLEND_FACTOR_ONE; builder->color_blend_attachment_state.colorBlendOp = VK_BLEND_OP_ADD; builder->color_blend_attachment_state.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; - builder->color_blend_attachment_state.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; + builder->color_blend_attachment_state.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; builder->color_blend_attachment_state.alphaBlendOp = VK_BLEND_OP_ADD; } @@ -3189,7 +3189,7 @@ void kvfGPipelineBuilderEnableAlphaBlending(KvfGraphicsPipelineBuilder* builder) builder->color_blend_attachment_state.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; builder->color_blend_attachment_state.colorBlendOp = VK_BLEND_OP_ADD; builder->color_blend_attachment_state.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; - builder->color_blend_attachment_state.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; + builder->color_blend_attachment_state.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; builder->color_blend_attachment_state.alphaBlendOp = VK_BLEND_OP_ADD; } From 2d0c02ae7e8470e9af5124e1c1e526fd23c181f9 Mon Sep 17 00:00:00 2001 From: Daemo <97889325+DaemonicGh@users.noreply.github.com> Date: Fri, 7 Aug 2026 00:18:52 +0200 Subject: [PATCH 2/4] Fix freeze when resizing a window (#239) Fixed an annoying bug where the application would freeze when resizing the window This pull request made it so the SDL window's resizing event now broadcasts a new event specifically asking the swapchain to recreate itself, which will in turn broadcast the original event when done. It also removed the Event enum specific values, as they were irrelevant. --- runtime/Includes/Core/Enums.h | 7 ++++--- runtime/Sources/Platform/Inputs.cpp | 6 +++--- runtime/Sources/Renderer/Swapchain.cpp | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/runtime/Includes/Core/Enums.h b/runtime/Includes/Core/Enums.h index ef15390..4934b1a 100644 --- a/runtime/Includes/Core/Enums.h +++ b/runtime/Includes/Core/Enums.h @@ -19,9 +19,10 @@ namespace mlx enum class Event { - ResizeEventCode = 56, - FrameBeginEventCode = 57, - FatalErrorEventCode = 168, + ResizeEventCode = 1, + SwapchainResizeEventCode, + FrameBeginEventCode, + FatalErrorEventCode, EndEnum }; diff --git a/runtime/Sources/Platform/Inputs.cpp b/runtime/Sources/Platform/Inputs.cpp index bf86a1c..f41c248 100644 --- a/runtime/Sources/Platform/Inputs.cpp +++ b/runtime/Sources/Platform/Inputs.cpp @@ -8,9 +8,9 @@ namespace mlx { namespace Internal { - struct ResizeEventBroadcast : public EventBase + struct SwapchainResizeEventBroadcast : public EventBase { - Event What() const override { return Event::ResizeEventCode; } + Event What() const override { return Event::SwapchainResizeEventCode; } }; } @@ -23,7 +23,7 @@ namespace mlx if(!m_events_hooks.contains(window_id) || m_events_hooks[window_id][event].empty()) return; if(event == MLX_WINDOW_EVENT && code == 8) - EventBus::SendBroadcast(Internal::ResizeEventBroadcast{}); + EventBus::SendBroadcast(Internal::SwapchainResizeEventBroadcast{}); for(const auto& hook : m_events_hooks[window_id][event]) { if(hook.fn) diff --git a/runtime/Sources/Renderer/Swapchain.cpp b/runtime/Sources/Renderer/Swapchain.cpp index 9c08607..6b2cfe3 100644 --- a/runtime/Sources/Renderer/Swapchain.cpp +++ b/runtime/Sources/Renderer/Swapchain.cpp @@ -15,7 +15,7 @@ namespace mlx Event What() const override { return Event::ResizeEventCode; } }; } - + std::string VulkanFormatName(VkFormat format) { #define STRINGIFY(x) case x: return #x @@ -161,7 +161,7 @@ namespace mlx std::function functor = [this](const EventBase& event) { - if(event.What() == Event::ResizeEventCode && !m_resize) + if(event.What() == Event::SwapchainResizeEventCode && !m_resize) m_resize = true; }; EventBus::RegisterListener({ functor, "mlx_swapchain_" + std::to_string(reinterpret_cast(this)) }); From c9df651a14d7d9d6a45984a56c99fec33dbdfc85 Mon Sep 17 00:00:00 2001 From: Daemo <97889325+DaemonicGh@users.noreply.github.com> Date: Fri, 7 Aug 2026 12:31:13 +0200 Subject: [PATCH 3/4] Optimized region drawing and fixed incorrect region clipping (#240) Made small optimizations to Texture's SetRegion, SetLinearRegion, GetRegion and Clear. They might not be super effective depending on how the compiler already optimized them. The optimizations mainly focus on removing the multiplication when transforming a position to an index by using the row index rather than the y coordinate during iteration. Also fixed regions not drawing properly when clipping against the sides of a window and made GetRegion reject any region that isn't fully inside of the window (previously accepted regions that clipped outside of the right and bottom side, possibly leading to incorrect reads on the user end due to desynchronized region dimensions) --- runtime/Sources/Renderer/Image.cpp | 77 ++++++++++++++++-------------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/runtime/Sources/Renderer/Image.cpp b/runtime/Sources/Renderer/Image.cpp index 30c8ff8..f4766c8 100644 --- a/runtime/Sources/Renderer/Image.cpp +++ b/runtime/Sources/Renderer/Image.cpp @@ -226,25 +226,34 @@ namespace mlx void Texture::SetRegion(int x, int y, int w, int h, mlx_color* pixels) noexcept { MLX_PROFILE_FUNCTION(); - if(x < 0 || y < 0 || static_cast(x) >= m_width || static_cast(y) >= m_height) - return; - if(w < 0 || h < 0) + if(w < 0 || h < 0 || x < -w || y < -h + || x >= static_cast(m_width) || y >= static_cast(m_height)) return; if(!m_staging_buffer.has_value()) OpenCPUBuffer(); - for(std::uint32_t i = 0, moving_x = x, moving_y = y;; i++, moving_x++) + const int + start_x = std::max(x, 0), + start_y = std::max(y, 0), + start_row = start_y * m_width, + start_i = (start_y - y) * w + (start_x - x), + end_x = std::min(x + w, m_width), + end_y = std::min(y + h, m_height), + end_row = end_y * m_width, + incr = (x + w) - end_x + (start_x - x); + for(int i = start_i, dx = start_x, row = start_row;; i++, dx++) { - if(moving_x >= static_cast(x + w) || moving_x >= m_width) + if(dx >= end_x) { - moving_x = x; - moving_y++; - if(moving_y >= static_cast(y + h) || moving_y >= m_height) + i += incr; + dx = start_x; + row += m_width; + if(row >= end_row) break; } if constexpr(std::endian::native == std::endian::little) - m_staging_buffer->GetMap()[(moving_y * m_width) + moving_x] = ReverseColor(pixels[i]); + m_staging_buffer->GetMap()[row + dx] = ReverseColor(pixels[i]); else - m_staging_buffer->GetMap()[(moving_y * m_width) + moving_x] = pixels[i]; + m_staging_buffer->GetMap()[row + dx] = pixels[i]; } m_has_been_modified = true; } @@ -252,23 +261,25 @@ namespace mlx void Texture::SetLinearRegion(int x, int y, std::size_t len, mlx_color* pixels) noexcept { MLX_PROFILE_FUNCTION(); - if(x < 0 || y < 0 || static_cast(x) >= m_width || static_cast(y) >= m_height) + if(x >= static_cast(m_width) || y >= static_cast(m_height)) return; if(!m_staging_buffer.has_value()) OpenCPUBuffer(); + int + start = y * m_width + x, + dest_start = std::max(start, 0), + src_start = dest_start - start, + dest_end = std::min(start + len, m_width * m_height); if constexpr(std::endian::native == std::endian::little) { - for(std::size_t i = 0; i < len && (y * m_width) + x + i < m_width * m_height; i++) - m_staging_buffer->GetMap()[(y * m_width) + x + i] = ReverseColor(pixels[i]); + for(int i = dest_start, j = src_start; i < dest_end; i++, j++) + m_staging_buffer->GetMap()[i] = ReverseColor(pixels[j]); } else { - std::size_t len_guard; - if((y * m_width + x + len) < m_width * m_height) - len_guard = len; - else - len_guard = len - (m_width * m_height - (y * m_width + x + len)); - std::memcpy(&m_staging_buffer->GetMap()[(y * m_width) + x], pixels, len_guard); + std::memcpy( + &m_staging_buffer->GetMap()[dest_start], + &pixels[src_start], dest_end - dest_start); } m_has_been_modified = true; } @@ -289,23 +300,23 @@ namespace mlx void Texture::GetRegion(int x, int y, int w, int h, mlx_color* dst) noexcept { MLX_PROFILE_FUNCTION(); - if(x < 0 || y < 0 || static_cast(x) >= m_width || static_cast(y) >= m_height) + if(w < 0 || h < 0 || x < 0 || y < 0 || static_cast(x + w) >= m_width || static_cast(y + h) >= m_height) return; if(!m_staging_buffer.has_value()) OpenCPUBuffer(); - for(std::uint32_t i = 0, moving_x = x, moving_y = y;; i++, moving_x++) + for(std::uint32_t i = 0, dx = x, row = y * m_width;; i++, dx++) { - if(moving_x >= static_cast(x + w) || moving_x >= m_width) + if(dx >= m_width) { - moving_x = x; - moving_y++; - if(moving_y >= static_cast(y + h) || moving_y >= m_height) + dx = x; + row += m_width; + if(row >= m_height * m_width) break; } if constexpr(std::endian::native == std::endian::little) - dst[i] = ReverseColor(m_staging_buffer->GetMap()[(moving_y * m_width) + moving_x]); + dst[i] = ReverseColor(m_staging_buffer->GetMap()[row + dx]); else - dst[i] = m_staging_buffer->GetMap()[(moving_y * m_width) + moving_x]; + dst[i] = m_staging_buffer->GetMap()[row + dx]; } } @@ -320,16 +331,8 @@ namespace mlx processed_color.g = static_cast(color.g * 255.f); processed_color.b = static_cast(color.b * 255.f); processed_color.a = static_cast(color.a * 255.f); - if(processed_color.r == 0 && processed_color.g == 0 && processed_color.b == 0) - std::memset(m_staging_buffer->GetMap(), processed_color.a, m_staging_buffer->GetSize()); - else - { - for(std::size_t y = 0; y < m_height; y++) - { - for(std::size_t x = 0; x < m_width; x++) - m_staging_buffer->GetMap()[y * m_width + x] = processed_color; - } - } + for(std::size_t i = 0; i < m_width * m_height; i++) + m_staging_buffer->GetMap()[i] = processed_color; } } From 63fe3088bf2d50176d205a72a013c412ae8788da Mon Sep 17 00:00:00 2001 From: Daemo <97889325+DaemonicGh@users.noreply.github.com> Date: Sat, 8 Aug 2026 15:45:56 +0200 Subject: [PATCH 4/4] Fix inconsistencies with the FPS scheduler (#241) The current Fps manager will sometime skip the intended delay and start the next frame immediately, I'm not 100% sure why it happens, but I rewrote most of it to stay consistent and improved the overall precision by compensating for OS wake delay after sleep and finishing with a busy wait I also changed the behavior of mlx_set_fps_goal to allow zero/negative values and treat them as uncapped (like most graphics libraries) --- runtime/Includes/Core/Fps.h | 20 ++++++++------ runtime/Sources/Core/Application.cpp | 5 ++-- runtime/Sources/Core/Bridge.cpp | 13 +++++----- runtime/Sources/Core/Fps.cpp | 39 ++++++++++++++++------------ 4 files changed, 43 insertions(+), 34 deletions(-) diff --git a/runtime/Includes/Core/Fps.h b/runtime/Includes/Core/Fps.h index 6cebc77..05364f4 100644 --- a/runtime/Includes/Core/Fps.h +++ b/runtime/Includes/Core/Fps.h @@ -1,26 +1,30 @@ #ifndef __MLX_FPS__ #define __MLX_FPS__ +#include + namespace mlx { + typedef std::chrono::steady_clock fps_clock; + class FpsManager { public: FpsManager() = default; void Init(); - bool Update(); - inline void SetMaxFPS(std::uint32_t fps) noexcept { m_max_fps = fps; m_ns = 1000000000.0 / fps; } + void WaitUntilNextFrame(); + inline void SetMaxFPS(std::uint32_t fps) noexcept { m_target_delta = fps_clock::duration(std::chrono::seconds(1)) / fps;} ~FpsManager() = default; private: - double m_ns = 1000000000.0 / 1'337'000.0; - std::int64_t m_fps_before = 0; - std::int64_t m_fps_now = 0; - std::int64_t m_timer = 0; - std::uint32_t m_max_fps = 1'337'000; - std::uint32_t m_fps_elapsed_time = 0; + fps_clock::time_point m_current_time; + fps_clock::time_point m_target_time; + fps_clock::time_point m_last_time_record; + fps_clock::duration m_delta_time = fps_clock::duration().zero(); + fps_clock::duration m_target_delta = fps_clock::duration().zero(); + fps_clock::duration m_sleep_margin = std::chrono::microseconds(200); }; } diff --git a/runtime/Sources/Core/Application.cpp b/runtime/Sources/Core/Application.cpp index ef253b3..009c800 100644 --- a/runtime/Sources/Core/Application.cpp +++ b/runtime/Sources/Core/Application.cpp @@ -8,7 +8,7 @@ namespace mlx { - Application::Application() : p_mem_manager(std::make_unique()), p_sdl_manager(std::make_unique()), m_fps(), m_in() + Application::Application() : p_mem_manager(std::make_unique()), p_sdl_manager(std::make_unique()), m_fps(), m_in() { MLX_PROFILE_FUNCTION(); std::srand(std::time(nullptr)); @@ -33,8 +33,7 @@ namespace mlx while(m_in.IsRunning()) { - if(!m_fps.Update()) - continue; + m_fps.WaitUntilNextFrame(); m_in.FetchInputs(); diff --git a/runtime/Sources/Core/Bridge.cpp b/runtime/Sources/Core/Bridge.cpp index 76ee9f6..4123d6a 100644 --- a/runtime/Sources/Core/Bridge.cpp +++ b/runtime/Sources/Core/Bridge.cpp @@ -45,10 +45,9 @@ extern "C" void mlx_set_fps_goal(mlx_context mlx, int fps) { MLX_CHECK_APPLICATION_POINTER(mlx); - if(fps < 0) - mlx::Error("You cannot set a negative FPS cap (nice try)"); - else - mlx->app->SetFPSCap(static_cast(fps)); + if(fps <= 0) + fps = -1; + mlx->app->SetFPSCap(static_cast(fps)); } void mlx_destroy_context(mlx_context mlx) @@ -313,14 +312,14 @@ extern "C" mlx::Error("Font loader: filepath is NULL"); return; } - + std::filesystem::path file(filepath); if (std::strcmp(filepath, "default") != 0 && !std::filesystem::exists(file)) { mlx::Error("TTF loader: unable to find file '%'", filepath); return; } - + if(std::strcmp(filepath, "default") != 0) { if(file.extension() != ".ttf" && file.extension() != ".tte") @@ -350,7 +349,7 @@ extern "C" mlx::Error("Font loader: filepath is NULL"); return; } - + std::filesystem::path file(filepath); if (std::strcmp(filepath, "default") != 0 && !std::filesystem::exists(file)) { diff --git a/runtime/Sources/Core/Fps.cpp b/runtime/Sources/Core/Fps.cpp index a464f67..d97879c 100644 --- a/runtime/Sources/Core/Fps.cpp +++ b/runtime/Sources/Core/Fps.cpp @@ -1,30 +1,37 @@ #include #include +#ifndef __APPLE__ +#include +#endif + namespace mlx { void FpsManager::Init() { - m_timer = static_cast(std::chrono::duration_cast(std::chrono::high_resolution_clock::now().time_since_epoch()).count()); - m_fps_before = m_timer; - m_fps_now = m_timer; + m_current_time = fps_clock::now(); + m_target_time = m_current_time + m_target_delta; } - bool FpsManager::Update() + void FpsManager::WaitUntilNextFrame() { - using namespace std::chrono_literals; - m_fps_now = static_cast(std::chrono::duration_cast(std::chrono::high_resolution_clock::now().time_since_epoch()).count()); - - if(std::chrono::duration{m_fps_now - m_timer} >= 1s) - m_timer += m_fps_now; - - m_fps_elapsed_time = m_fps_now - m_fps_before; - if(m_fps_elapsed_time >= m_ns) + m_current_time = fps_clock::now(); + if(m_current_time < m_target_time) { - m_fps_before += m_ns; - return true; + std::this_thread::sleep_until(m_target_time - m_sleep_margin); + m_current_time = fps_clock::now(); + while (m_current_time < m_target_time) + { + #ifndef __APPLE__ + _mm_pause(); // reduces CPU usage on x86 without yielding + #endif + m_current_time = fps_clock::now(); + } } - std::this_thread::sleep_for(std::chrono::duration(m_ns - 1)); - return false; + else if (m_target_time < m_current_time - m_target_delta * 4) + m_target_time = m_current_time; + m_target_time += m_target_delta; + m_delta_time = m_current_time - m_last_time_record; + m_last_time_record = m_current_time; } }