Performance optimizations - #46

Open
jdolan wants to merge 19 commits into
mainfrom
performance-foundations
Open

Performance optimizations#46
jdolan wants to merge 19 commits into
mainfrom
performance-foundations

Conversation

@jdolan

Copy link
Copy Markdown
Owner

This pull request introduces a new HUD example application to the project, updates the build system and Xcode project files to support it, and includes several code improvements and bug fixes in the core library. The most significant changes are grouped below.


New Example Application: HUD


Core Library Improvements and Bug Fixes

  • Replaced direct assignments to the needsLayout property with calls to the setNeedsLayout method for better encapsulation and consistency in layout invalidation across multiple components (CollectionView, Control, Label, Option, PageView, ProgressBar). [1][2][3][4][5][6]
  • Improved ProgressBar value calculation to handle edge cases where max <= min, preventing divide-by-zero and ensuring correct progress display.
  • In Panel, after direct frame mutation, added a call to MVC_InvalidateRenderFrames() to ensure hit-testing uses up-to-date frame data.

Renderer Performance Optimization

  • Optimized Renderer::drawLines to use stack allocation for small vertex buffers, reducing heap allocations and improving rendering performance. [1][2]

jdolanand others added 15 commits September 1, 2026 21:13
Renders a representative game HUD (health, armor, ammo, crosshair,
countdown timer, chat log, toggling scoreboard) with each widget
updating on an interval, and times the style, layout, draw and
endFrame passes individually, printing a summary once per second.
Intended to measure CPU cost of driving a per-frame HUD with MVC
before and after performance changes. MVC_HUD_FRAMES=N exits after
N frames; MVC_HUD_HIDDEN=1 creates the window hidden.
Baseline (M-series macOS, MVC_HUD_FRAMES=1400 MVC_HUD_HIDDEN=1,
vsync ~120fps): idle frames style avg ~2-5us (max 233us), layout
avg ~0.5us (max 47us), draw avg ~8-38us (max 2.5ms), endFrame avg
~25-52us (max 226us); 6 draw calls idle, 26 with scoreboard shown.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduce propagating setters for the dirty flags: in addition to
setting the flag on the View, they mark needsLayoutSubviews or
needsApplyThemeSubviews on each ancestor, recording that a descendant
is dirty. The ancestor walk stops at the first already-marked View,
so repeated invalidations are amortized O(1).
Convert every in-tree flag write to the setters (View internals and
all widgets). No behavior change yet: the subtree flags are not read
until the traversal gating that follows.
Applications that assign needsLayout or needsApplyTheme directly
MUST migrate to the setters; direct writes will not propagate, and
the View may be skipped once applyThemeIfNeeded and layoutIfNeeded
are gated on the subtree flags.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both traversals previously recursed into every subview each frame,
making the per-frame cost O(tree) even when nothing was invalidated.
They now return immediately unless the View or a descendant is dirty,
making the steady-state cost O(dirty path).
The subtree flag is cleared before doing any work, so invalidations
that occur during the traversal itself (e.g. View::resize propagating
setNeedsLayout, or a widget marking a sibling mid-layout) survive to
the next frame rather than being lost.
Behavior change: a dirty View's subviews are no longer laid out
before the View itself. Previously layoutIfNeeded recursed into
children first and then re-arranged them via layoutWithConstraint,
laying out dirty children twice; children are now laid out once, by
their parent's layout pass, with a follow-up recursion catching any
descendant skipped by an overridden layoutSubviews.
Examples/HUD idle frames: style pass avg 2-5us -> 0.2us; layout
pass avg similar with maxima reduced. All tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both were recomputed from scratch on every call -- renderFrame is
O(depth), and clippingFrame recursed into every clipping ancestor's
clippingFrame, making it super-linear -- and they are called three
to six times per View per frame (Renderer::drawView, View::render,
subclass render methods), plus twice per View per mouse motion event.
Views now memoize both rects, stamped against a process-global render
frame generation. WindowController::renderTo bumps the generation via
MVC_InvalidateRenderFrames after layout and before drawing, so the
draw pass and subsequent hit-testing observe post-layout frames at
O(1) amortized per View. A zero generation (callers that never
invalidate, e.g. unit tests) disables cache reads entirely.
clippingFrame now intersects only the nearest clipping ancestor's
clippingFrame, which already folds in every outer clip; this is
equivalent to the previous every-ancestor loop without the redundant
super-linear work.
Frames mutated outside of layout (e.g. Panel dragging) are observed
by hit-testing at the next render rather than immediately;
MVC_InvalidateRenderFrames is exported for callers that need
same-batch precision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Text measurement went through Font::sizeCharacters on every call,
which strdups the string and invokes SDL_ttf per line; a container
measuring its children during layout re-measured every unchanged
Text descendant. The color escapes path is far more expensive still.
Cache the measured size on the Text, keyed by the Font's scale so
pixel-density changes re-measure without additional hooks, and
invalidate wherever the rendered texture is invalidated (setText,
setFont, color change, scale change, device reset) as well as in
awakeWithDictionary, whose text inlet bypasses setText.
Examples/HUD layout-pass maxima on widget-update frames drop from
~25-50us to ~5-10us. All tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pushDrawArrays now merges a record into the previous one when both
bind the same texture and scissor: vertices are appended contiguously,
so extending the prior record's vertexCount is equivalent and saves a
setScissor, bindFragmentSamplers and drawPrimitives per merged record
in endFrame. Blending order is preserved since only adjacent records
merge.
Vertices are appended with one capacity check and a direct array
write instead of a virtual Vector::add per vertex, and drawLines uses
a stack buffer for polylines up to 16 segments (drawLine and drawRect
always qualify) instead of a malloc/free per call -- previously every
bordered View allocated every frame.
Merging favors untextured geometry (backgrounds, borders, bevels
share the 1x1 white texture); distinct Text textures still cost one
draw each. All tests pass; Examples/HUD and Hello render identically.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collect each cache's value and validity into a single anonymous
struct member (renderFrameCache, clippingFrameCache on View;
naturalSizeCache on Text) rather than parallel loose fields, keeping
the value and the stamp or flag that guards it visibly paired.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code review found the self-first ordering introduced with traversal
gating regressed convergence: a descendant whose layout resizes it
marks its ancestors mid-pass, and with self-first ordering a clean
ancestor's dirty check has already run, deferring its re-arrangement
by one rendered frame per nesting level. Subviews-first restores the
original bottom-up single-pass convergence while keeping the gating.
Also complete setter adoption flagged by review: invalidateStyle's
enumerator now uses setNeedsApplyTheme rather than writing the flag
directly (the trailing self call becomes redundant), the layout unit
tests use setNeedsLayout, and ScrollBar's docs reference the setter.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
colorEscapes is a public, setter-less field that switches the
measurement path, and flipping it after the first measurement (the
documented usage) left the cache returning the escape-blind size
indefinitely. Include it in the cache key.
Addresses code review feedback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code review found the in-tree violators of the new invalidation
contract: Panel mutates its frame directly while dragging, leaving
hit-testing for the remainder of the event batch on the pre-move
cached clippingFrame, and Text resizes itself mid-draw on a pixel
density change but then read the renderFrame stamped earlier in the
same pass. Both now call MVC_InvalidateRenderFrames after mutating.
The HUD benchmark also never bumped the generation, so it measured
the frame-cache feature disabled; it now mirrors renderTo.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
progress and setValue computed value / (max - min), which reports
100% at zero progress for any non-zero min, and divides by zero when
max == min (reachable via bound inlets with no validation). Compute
(value - min) / (max - min), guarded to 0% when max <= min, and
derive setValue's fraction from progress rather than duplicating the
formula. Pre-existing defect surfaced by code review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirrors the ObjectivelyMVC-Hello command line tool target: HUD.c,
the same framework links and search paths, and a shared scheme, so
the benchmark can be run and profiled from Xcode.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Multiplies the scoreboard row count so frame cost can be measured
as a function of UI complexity; the default HUD is too small for
tree-size-dependent costs to dominate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The UI passes accounted for only a fraction of the process's CPU;
the acquire (RenderDevice::beginFrame through the clear pass) and
submit (RenderDevice::endFrame, which blocks on present with vsync)
columns attribute the remainder, distinguishing engine/driver floor
from MVC cost.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CopilotAI lite review requested due to automatic review settings September 2, 2026 02:10
Comment threadSources/ObjectivelyMVC/Renderer.c

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new Examples/HUD.c uses printf but does not include <stdio.h>, which can break builds under common C warning/error settings.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This pull request adds a new HUD example application intended for benchmarking, while also introducing render-frame caching/invalidation and several layout/theme invalidation improvements to reduce redundant work during rendering.

Changes:

  • Added a new HUD example app and integrated it into both Automake and Xcode (new scheme/target).
  • Introduced per-frame render/clipping frame caching with an explicit MVC_InvalidateRenderFrames() invalidation point after layout (and for direct frame mutation paths).
  • Optimized renderer batching (stack allocation for small line buffers; contiguous vertex appends and draw-call coalescing) and tightened layout invalidation via View::setNeedsLayout/View::setNeedsApplyTheme.
File summaries
FileDescription
Tests/ObjectivelyMVC/View.cUpdates tests to use View::setNeedsLayout instead of direct flag writes.
Sources/ObjectivelyMVC/WindowController.cInvalidates render-frame caches after layout, before draw.
Sources/ObjectivelyMVC/View.hAdds cached frame fields + subtree invalidation flags; exports MVC_InvalidateRenderFrames().
Sources/ObjectivelyMVC/View.cImplements caching/invalidation generation, subtree invalidation propagation, and faster clipping/render frame computation.
Sources/ObjectivelyMVC/TextView.cSwitches to setNeedsLayout on edit/text changes.
Sources/ObjectivelyMVC/Text.hAdds naturalSize cache state to Text.
Sources/ObjectivelyMVC/Text.cImplements/invalidates naturalSize caching; invalidates render frames on device-reset resize mid-draw.
Sources/ObjectivelyMVC/TabView.cUses setNeedsLayout on tab selection changes.
Sources/ObjectivelyMVC/TableView.cReplaces direct needsLayout writes with setNeedsLayout during layout/reload.
Sources/ObjectivelyMVC/Slider.cUses setNeedsLayout when value changes.
Sources/ObjectivelyMVC/Select.cUses setNeedsLayout after option mutations/selection.
Sources/ObjectivelyMVC/ScrollView.cUses setNeedsLayout for layout-affecting state changes.
Sources/ObjectivelyMVC/ScrollBar.hUpdates docs to reflect View::setNeedsLayout usage.
Sources/ObjectivelyMVC/ScrollBar.cUses setNeedsLayout after scroll interactions/state changes.
Sources/ObjectivelyMVC/Renderer.cReduces allocations for small line draws; appends vertices contiguously and coalesces compatible draw records.
Sources/ObjectivelyMVC/ProgressBar.cFixes progress calculation edge cases and uses setNeedsLayout.
Sources/ObjectivelyMVC/Panel.cInvalidates render frames after direct frame mutation to keep hit-testing accurate within the event batch.
Sources/ObjectivelyMVC/PageView.cUses setNeedsLayout when current page changes.
Sources/ObjectivelyMVC/Option.cUses setNeedsLayout when selection state changes.
Sources/ObjectivelyMVC/Label.cUses setNeedsLayout after dictionary binding.
Sources/ObjectivelyMVC/Control.cUses setNeedsLayout on state changes.
Sources/ObjectivelyMVC/CollectionView.cUses setNeedsLayout after reload.
ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-HUD.xcschemeAdds an Xcode shared scheme for the new HUD example.
ObjectivelyMVC.xcodeproj/project.pbxprojAdds the HUD target, source, and framework link settings to the Xcode project.
Examples/Makefile.amAdds HUD to Automake example programs and defines HUD_SOURCES.
Examples/HUD.cIntroduces the HUD benchmark app (builds view tree, runs timed frame passes, prints periodic stats).
Examples/.gitignoreIgnores the new HUD example binary.
Review details
  • Files reviewed: 27/27 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadExamples/HUD.c
jdolanand others added 4 commits September 1, 2026 22:24
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Replace the stack-or-malloc dual path with a fixed stack buffer,
emitting long polylines in batches: pushDrawArrays merges adjacent
records with equal texture and scissor, so a polyline of any length
still produces a single draw call, with no heap allocation for any
caller. In-tree callers pass at most four segments and take one
batch.
Addresses review feedback on #46.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jdolan
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Performance optimizations - #46

Open
jdolan wants to merge 19 commits into
mainfrom
performance-foundations
Open

Performance optimizations#46
jdolan wants to merge 19 commits into
mainfrom
performance-foundations

Conversation

@jdolan

Copy link
Copy Markdown
Owner

This pull request introduces a new HUD example application to the project, updates the build system and Xcode project files to support it, and includes several code improvements and bug fixes in the core library. The most significant changes are grouped below.


New Example Application: HUD


Core Library Improvements and Bug Fixes

  • Replaced direct assignments to the needsLayout property with calls to the setNeedsLayout method for better encapsulation and consistency in layout invalidation across multiple components (CollectionView, Control, Label, Option, PageView, ProgressBar). [1][2][3][4][5][6]
  • Improved ProgressBar value calculation to handle edge cases where max <= min, preventing divide-by-zero and ensuring correct progress display.
  • In Panel, after direct frame mutation, added a call to MVC_InvalidateRenderFrames() to ensure hit-testing uses up-to-date frame data.

Renderer Performance Optimization

  • Optimized Renderer::drawLines to use stack allocation for small vertex buffers, reducing heap allocations and improving rendering performance. [1][2]

jdolanand others added 15 commits September 1, 2026 21:13
Renders a representative game HUD (health, armor, ammo, crosshair,
countdown timer, chat log, toggling scoreboard) with each widget
updating on an interval, and times the style, layout, draw and
endFrame passes individually, printing a summary once per second.
Intended to measure CPU cost of driving a per-frame HUD with MVC
before and after performance changes. MVC_HUD_FRAMES=N exits after
N frames; MVC_HUD_HIDDEN=1 creates the window hidden.
Baseline (M-series macOS, MVC_HUD_FRAMES=1400 MVC_HUD_HIDDEN=1,
vsync ~120fps): idle frames style avg ~2-5us (max 233us), layout
avg ~0.5us (max 47us), draw avg ~8-38us (max 2.5ms), endFrame avg
~25-52us (max 226us); 6 draw calls idle, 26 with scoreboard shown.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduce propagating setters for the dirty flags: in addition to
setting the flag on the View, they mark needsLayoutSubviews or
needsApplyThemeSubviews on each ancestor, recording that a descendant
is dirty. The ancestor walk stops at the first already-marked View,
so repeated invalidations are amortized O(1).
Convert every in-tree flag write to the setters (View internals and
all widgets). No behavior change yet: the subtree flags are not read
until the traversal gating that follows.
Applications that assign needsLayout or needsApplyTheme directly
MUST migrate to the setters; direct writes will not propagate, and
the View may be skipped once applyThemeIfNeeded and layoutIfNeeded
are gated on the subtree flags.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both traversals previously recursed into every subview each frame,
making the per-frame cost O(tree) even when nothing was invalidated.
They now return immediately unless the View or a descendant is dirty,
making the steady-state cost O(dirty path).
The subtree flag is cleared before doing any work, so invalidations
that occur during the traversal itself (e.g. View::resize propagating
setNeedsLayout, or a widget marking a sibling mid-layout) survive to
the next frame rather than being lost.
Behavior change: a dirty View's subviews are no longer laid out
before the View itself. Previously layoutIfNeeded recursed into
children first and then re-arranged them via layoutWithConstraint,
laying out dirty children twice; children are now laid out once, by
their parent's layout pass, with a follow-up recursion catching any
descendant skipped by an overridden layoutSubviews.
Examples/HUD idle frames: style pass avg 2-5us -> 0.2us; layout
pass avg similar with maxima reduced. All tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both were recomputed from scratch on every call -- renderFrame is
O(depth), and clippingFrame recursed into every clipping ancestor's
clippingFrame, making it super-linear -- and they are called three
to six times per View per frame (Renderer::drawView, View::render,
subclass render methods), plus twice per View per mouse motion event.
Views now memoize both rects, stamped against a process-global render
frame generation. WindowController::renderTo bumps the generation via
MVC_InvalidateRenderFrames after layout and before drawing, so the
draw pass and subsequent hit-testing observe post-layout frames at
O(1) amortized per View. A zero generation (callers that never
invalidate, e.g. unit tests) disables cache reads entirely.
clippingFrame now intersects only the nearest clipping ancestor's
clippingFrame, which already folds in every outer clip; this is
equivalent to the previous every-ancestor loop without the redundant
super-linear work.
Frames mutated outside of layout (e.g. Panel dragging) are observed
by hit-testing at the next render rather than immediately;
MVC_InvalidateRenderFrames is exported for callers that need
same-batch precision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Text measurement went through Font::sizeCharacters on every call,
which strdups the string and invokes SDL_ttf per line; a container
measuring its children during layout re-measured every unchanged
Text descendant. The color escapes path is far more expensive still.
Cache the measured size on the Text, keyed by the Font's scale so
pixel-density changes re-measure without additional hooks, and
invalidate wherever the rendered texture is invalidated (setText,
setFont, color change, scale change, device reset) as well as in
awakeWithDictionary, whose text inlet bypasses setText.
Examples/HUD layout-pass maxima on widget-update frames drop from
~25-50us to ~5-10us. All tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pushDrawArrays now merges a record into the previous one when both
bind the same texture and scissor: vertices are appended contiguously,
so extending the prior record's vertexCount is equivalent and saves a
setScissor, bindFragmentSamplers and drawPrimitives per merged record
in endFrame. Blending order is preserved since only adjacent records
merge.
Vertices are appended with one capacity check and a direct array
write instead of a virtual Vector::add per vertex, and drawLines uses
a stack buffer for polylines up to 16 segments (drawLine and drawRect
always qualify) instead of a malloc/free per call -- previously every
bordered View allocated every frame.
Merging favors untextured geometry (backgrounds, borders, bevels
share the 1x1 white texture); distinct Text textures still cost one
draw each. All tests pass; Examples/HUD and Hello render identically.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collect each cache's value and validity into a single anonymous
struct member (renderFrameCache, clippingFrameCache on View;
naturalSizeCache on Text) rather than parallel loose fields, keeping
the value and the stamp or flag that guards it visibly paired.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code review found the self-first ordering introduced with traversal
gating regressed convergence: a descendant whose layout resizes it
marks its ancestors mid-pass, and with self-first ordering a clean
ancestor's dirty check has already run, deferring its re-arrangement
by one rendered frame per nesting level. Subviews-first restores the
original bottom-up single-pass convergence while keeping the gating.
Also complete setter adoption flagged by review: invalidateStyle's
enumerator now uses setNeedsApplyTheme rather than writing the flag
directly (the trailing self call becomes redundant), the layout unit
tests use setNeedsLayout, and ScrollBar's docs reference the setter.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
colorEscapes is a public, setter-less field that switches the
measurement path, and flipping it after the first measurement (the
documented usage) left the cache returning the escape-blind size
indefinitely. Include it in the cache key.
Addresses code review feedback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code review found the in-tree violators of the new invalidation
contract: Panel mutates its frame directly while dragging, leaving
hit-testing for the remainder of the event batch on the pre-move
cached clippingFrame, and Text resizes itself mid-draw on a pixel
density change but then read the renderFrame stamped earlier in the
same pass. Both now call MVC_InvalidateRenderFrames after mutating.
The HUD benchmark also never bumped the generation, so it measured
the frame-cache feature disabled; it now mirrors renderTo.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
progress and setValue computed value / (max - min), which reports
100% at zero progress for any non-zero min, and divides by zero when
max == min (reachable via bound inlets with no validation). Compute
(value - min) / (max - min), guarded to 0% when max <= min, and
derive setValue's fraction from progress rather than duplicating the
formula. Pre-existing defect surfaced by code review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirrors the ObjectivelyMVC-Hello command line tool target: HUD.c,
the same framework links and search paths, and a shared scheme, so
the benchmark can be run and profiled from Xcode.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Multiplies the scoreboard row count so frame cost can be measured
as a function of UI complexity; the default HUD is too small for
tree-size-dependent costs to dominate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The UI passes accounted for only a fraction of the process's CPU;
the acquire (RenderDevice::beginFrame through the clear pass) and
submit (RenderDevice::endFrame, which blocks on present with vsync)
columns attribute the remainder, distinguishing engine/driver floor
from MVC cost.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CopilotAI lite review requested due to automatic review settings September 2, 2026 02:10
Comment threadSources/ObjectivelyMVC/Renderer.c

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new Examples/HUD.c uses printf but does not include <stdio.h>, which can break builds under common C warning/error settings.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This pull request adds a new HUD example application intended for benchmarking, while also introducing render-frame caching/invalidation and several layout/theme invalidation improvements to reduce redundant work during rendering.

Changes:

  • Added a new HUD example app and integrated it into both Automake and Xcode (new scheme/target).
  • Introduced per-frame render/clipping frame caching with an explicit MVC_InvalidateRenderFrames() invalidation point after layout (and for direct frame mutation paths).
  • Optimized renderer batching (stack allocation for small line buffers; contiguous vertex appends and draw-call coalescing) and tightened layout invalidation via View::setNeedsLayout/View::setNeedsApplyTheme.
File summaries
FileDescription
Tests/ObjectivelyMVC/View.cUpdates tests to use View::setNeedsLayout instead of direct flag writes.
Sources/ObjectivelyMVC/WindowController.cInvalidates render-frame caches after layout, before draw.
Sources/ObjectivelyMVC/View.hAdds cached frame fields + subtree invalidation flags; exports MVC_InvalidateRenderFrames().
Sources/ObjectivelyMVC/View.cImplements caching/invalidation generation, subtree invalidation propagation, and faster clipping/render frame computation.
Sources/ObjectivelyMVC/TextView.cSwitches to setNeedsLayout on edit/text changes.
Sources/ObjectivelyMVC/Text.hAdds naturalSize cache state to Text.
Sources/ObjectivelyMVC/Text.cImplements/invalidates naturalSize caching; invalidates render frames on device-reset resize mid-draw.
Sources/ObjectivelyMVC/TabView.cUses setNeedsLayout on tab selection changes.
Sources/ObjectivelyMVC/TableView.cReplaces direct needsLayout writes with setNeedsLayout during layout/reload.
Sources/ObjectivelyMVC/Slider.cUses setNeedsLayout when value changes.
Sources/ObjectivelyMVC/Select.cUses setNeedsLayout after option mutations/selection.
Sources/ObjectivelyMVC/ScrollView.cUses setNeedsLayout for layout-affecting state changes.
Sources/ObjectivelyMVC/ScrollBar.hUpdates docs to reflect View::setNeedsLayout usage.
Sources/ObjectivelyMVC/ScrollBar.cUses setNeedsLayout after scroll interactions/state changes.
Sources/ObjectivelyMVC/Renderer.cReduces allocations for small line draws; appends vertices contiguously and coalesces compatible draw records.
Sources/ObjectivelyMVC/ProgressBar.cFixes progress calculation edge cases and uses setNeedsLayout.
Sources/ObjectivelyMVC/Panel.cInvalidates render frames after direct frame mutation to keep hit-testing accurate within the event batch.
Sources/ObjectivelyMVC/PageView.cUses setNeedsLayout when current page changes.
Sources/ObjectivelyMVC/Option.cUses setNeedsLayout when selection state changes.
Sources/ObjectivelyMVC/Label.cUses setNeedsLayout after dictionary binding.
Sources/ObjectivelyMVC/Control.cUses setNeedsLayout on state changes.
Sources/ObjectivelyMVC/CollectionView.cUses setNeedsLayout after reload.
ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-HUD.xcschemeAdds an Xcode shared scheme for the new HUD example.
ObjectivelyMVC.xcodeproj/project.pbxprojAdds the HUD target, source, and framework link settings to the Xcode project.
Examples/Makefile.amAdds HUD to Automake example programs and defines HUD_SOURCES.
Examples/HUD.cIntroduces the HUD benchmark app (builds view tree, runs timed frame passes, prints periodic stats).
Examples/.gitignoreIgnores the new HUD example binary.
Review details
  • Files reviewed: 27/27 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadExamples/HUD.c
jdolanand others added 4 commits September 1, 2026 22:24
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Replace the stack-or-malloc dual path with a fixed stack buffer,
emitting long polylines in batches: pushDrawArrays merges adjacent
records with equal texture and scissor, so a polyline of any length
still produces a single draw call, with no heap allocation for any
caller. In-tree callers pass at most four segments and take one
batch.
Addresses review feedback on #46.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jdolan
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Performance optimizations - #46

Open
jdolan wants to merge 19 commits into
mainfrom
performance-foundations
Open

Performance optimizations#46
jdolan wants to merge 19 commits into
mainfrom
performance-foundations

Conversation

@jdolan

Copy link
Copy Markdown
Owner

This pull request introduces a new HUD example application to the project, updates the build system and Xcode project files to support it, and includes several code improvements and bug fixes in the core library. The most significant changes are grouped below.


New Example Application: HUD


Core Library Improvements and Bug Fixes

  • Replaced direct assignments to the needsLayout property with calls to the setNeedsLayout method for better encapsulation and consistency in layout invalidation across multiple components (CollectionView, Control, Label, Option, PageView, ProgressBar). [1][2][3][4][5][6]
  • Improved ProgressBar value calculation to handle edge cases where max <= min, preventing divide-by-zero and ensuring correct progress display.
  • In Panel, after direct frame mutation, added a call to MVC_InvalidateRenderFrames() to ensure hit-testing uses up-to-date frame data.

Renderer Performance Optimization

  • Optimized Renderer::drawLines to use stack allocation for small vertex buffers, reducing heap allocations and improving rendering performance. [1][2]

jdolanand others added 15 commits September 1, 2026 21:13
Renders a representative game HUD (health, armor, ammo, crosshair,
countdown timer, chat log, toggling scoreboard) with each widget
updating on an interval, and times the style, layout, draw and
endFrame passes individually, printing a summary once per second.
Intended to measure CPU cost of driving a per-frame HUD with MVC
before and after performance changes. MVC_HUD_FRAMES=N exits after
N frames; MVC_HUD_HIDDEN=1 creates the window hidden.
Baseline (M-series macOS, MVC_HUD_FRAMES=1400 MVC_HUD_HIDDEN=1,
vsync ~120fps): idle frames style avg ~2-5us (max 233us), layout
avg ~0.5us (max 47us), draw avg ~8-38us (max 2.5ms), endFrame avg
~25-52us (max 226us); 6 draw calls idle, 26 with scoreboard shown.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduce propagating setters for the dirty flags: in addition to
setting the flag on the View, they mark needsLayoutSubviews or
needsApplyThemeSubviews on each ancestor, recording that a descendant
is dirty. The ancestor walk stops at the first already-marked View,
so repeated invalidations are amortized O(1).
Convert every in-tree flag write to the setters (View internals and
all widgets). No behavior change yet: the subtree flags are not read
until the traversal gating that follows.
Applications that assign needsLayout or needsApplyTheme directly
MUST migrate to the setters; direct writes will not propagate, and
the View may be skipped once applyThemeIfNeeded and layoutIfNeeded
are gated on the subtree flags.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both traversals previously recursed into every subview each frame,
making the per-frame cost O(tree) even when nothing was invalidated.
They now return immediately unless the View or a descendant is dirty,
making the steady-state cost O(dirty path).
The subtree flag is cleared before doing any work, so invalidations
that occur during the traversal itself (e.g. View::resize propagating
setNeedsLayout, or a widget marking a sibling mid-layout) survive to
the next frame rather than being lost.
Behavior change: a dirty View's subviews are no longer laid out
before the View itself. Previously layoutIfNeeded recursed into
children first and then re-arranged them via layoutWithConstraint,
laying out dirty children twice; children are now laid out once, by
their parent's layout pass, with a follow-up recursion catching any
descendant skipped by an overridden layoutSubviews.
Examples/HUD idle frames: style pass avg 2-5us -> 0.2us; layout
pass avg similar with maxima reduced. All tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both were recomputed from scratch on every call -- renderFrame is
O(depth), and clippingFrame recursed into every clipping ancestor's
clippingFrame, making it super-linear -- and they are called three
to six times per View per frame (Renderer::drawView, View::render,
subclass render methods), plus twice per View per mouse motion event.
Views now memoize both rects, stamped against a process-global render
frame generation. WindowController::renderTo bumps the generation via
MVC_InvalidateRenderFrames after layout and before drawing, so the
draw pass and subsequent hit-testing observe post-layout frames at
O(1) amortized per View. A zero generation (callers that never
invalidate, e.g. unit tests) disables cache reads entirely.
clippingFrame now intersects only the nearest clipping ancestor's
clippingFrame, which already folds in every outer clip; this is
equivalent to the previous every-ancestor loop without the redundant
super-linear work.
Frames mutated outside of layout (e.g. Panel dragging) are observed
by hit-testing at the next render rather than immediately;
MVC_InvalidateRenderFrames is exported for callers that need
same-batch precision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Text measurement went through Font::sizeCharacters on every call,
which strdups the string and invokes SDL_ttf per line; a container
measuring its children during layout re-measured every unchanged
Text descendant. The color escapes path is far more expensive still.
Cache the measured size on the Text, keyed by the Font's scale so
pixel-density changes re-measure without additional hooks, and
invalidate wherever the rendered texture is invalidated (setText,
setFont, color change, scale change, device reset) as well as in
awakeWithDictionary, whose text inlet bypasses setText.
Examples/HUD layout-pass maxima on widget-update frames drop from
~25-50us to ~5-10us. All tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pushDrawArrays now merges a record into the previous one when both
bind the same texture and scissor: vertices are appended contiguously,
so extending the prior record's vertexCount is equivalent and saves a
setScissor, bindFragmentSamplers and drawPrimitives per merged record
in endFrame. Blending order is preserved since only adjacent records
merge.
Vertices are appended with one capacity check and a direct array
write instead of a virtual Vector::add per vertex, and drawLines uses
a stack buffer for polylines up to 16 segments (drawLine and drawRect
always qualify) instead of a malloc/free per call -- previously every
bordered View allocated every frame.
Merging favors untextured geometry (backgrounds, borders, bevels
share the 1x1 white texture); distinct Text textures still cost one
draw each. All tests pass; Examples/HUD and Hello render identically.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collect each cache's value and validity into a single anonymous
struct member (renderFrameCache, clippingFrameCache on View;
naturalSizeCache on Text) rather than parallel loose fields, keeping
the value and the stamp or flag that guards it visibly paired.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code review found the self-first ordering introduced with traversal
gating regressed convergence: a descendant whose layout resizes it
marks its ancestors mid-pass, and with self-first ordering a clean
ancestor's dirty check has already run, deferring its re-arrangement
by one rendered frame per nesting level. Subviews-first restores the
original bottom-up single-pass convergence while keeping the gating.
Also complete setter adoption flagged by review: invalidateStyle's
enumerator now uses setNeedsApplyTheme rather than writing the flag
directly (the trailing self call becomes redundant), the layout unit
tests use setNeedsLayout, and ScrollBar's docs reference the setter.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
colorEscapes is a public, setter-less field that switches the
measurement path, and flipping it after the first measurement (the
documented usage) left the cache returning the escape-blind size
indefinitely. Include it in the cache key.
Addresses code review feedback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code review found the in-tree violators of the new invalidation
contract: Panel mutates its frame directly while dragging, leaving
hit-testing for the remainder of the event batch on the pre-move
cached clippingFrame, and Text resizes itself mid-draw on a pixel
density change but then read the renderFrame stamped earlier in the
same pass. Both now call MVC_InvalidateRenderFrames after mutating.
The HUD benchmark also never bumped the generation, so it measured
the frame-cache feature disabled; it now mirrors renderTo.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
progress and setValue computed value / (max - min), which reports
100% at zero progress for any non-zero min, and divides by zero when
max == min (reachable via bound inlets with no validation). Compute
(value - min) / (max - min), guarded to 0% when max <= min, and
derive setValue's fraction from progress rather than duplicating the
formula. Pre-existing defect surfaced by code review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirrors the ObjectivelyMVC-Hello command line tool target: HUD.c,
the same framework links and search paths, and a shared scheme, so
the benchmark can be run and profiled from Xcode.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Multiplies the scoreboard row count so frame cost can be measured
as a function of UI complexity; the default HUD is too small for
tree-size-dependent costs to dominate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The UI passes accounted for only a fraction of the process's CPU;
the acquire (RenderDevice::beginFrame through the clear pass) and
submit (RenderDevice::endFrame, which blocks on present with vsync)
columns attribute the remainder, distinguishing engine/driver floor
from MVC cost.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CopilotAI lite review requested due to automatic review settings September 2, 2026 02:10
Comment threadSources/ObjectivelyMVC/Renderer.c

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new Examples/HUD.c uses printf but does not include <stdio.h>, which can break builds under common C warning/error settings.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This pull request adds a new HUD example application intended for benchmarking, while also introducing render-frame caching/invalidation and several layout/theme invalidation improvements to reduce redundant work during rendering.

Changes:

  • Added a new HUD example app and integrated it into both Automake and Xcode (new scheme/target).
  • Introduced per-frame render/clipping frame caching with an explicit MVC_InvalidateRenderFrames() invalidation point after layout (and for direct frame mutation paths).
  • Optimized renderer batching (stack allocation for small line buffers; contiguous vertex appends and draw-call coalescing) and tightened layout invalidation via View::setNeedsLayout/View::setNeedsApplyTheme.
File summaries
FileDescription
Tests/ObjectivelyMVC/View.cUpdates tests to use View::setNeedsLayout instead of direct flag writes.
Sources/ObjectivelyMVC/WindowController.cInvalidates render-frame caches after layout, before draw.
Sources/ObjectivelyMVC/View.hAdds cached frame fields + subtree invalidation flags; exports MVC_InvalidateRenderFrames().
Sources/ObjectivelyMVC/View.cImplements caching/invalidation generation, subtree invalidation propagation, and faster clipping/render frame computation.
Sources/ObjectivelyMVC/TextView.cSwitches to setNeedsLayout on edit/text changes.
Sources/ObjectivelyMVC/Text.hAdds naturalSize cache state to Text.
Sources/ObjectivelyMVC/Text.cImplements/invalidates naturalSize caching; invalidates render frames on device-reset resize mid-draw.
Sources/ObjectivelyMVC/TabView.cUses setNeedsLayout on tab selection changes.
Sources/ObjectivelyMVC/TableView.cReplaces direct needsLayout writes with setNeedsLayout during layout/reload.
Sources/ObjectivelyMVC/Slider.cUses setNeedsLayout when value changes.
Sources/ObjectivelyMVC/Select.cUses setNeedsLayout after option mutations/selection.
Sources/ObjectivelyMVC/ScrollView.cUses setNeedsLayout for layout-affecting state changes.
Sources/ObjectivelyMVC/ScrollBar.hUpdates docs to reflect View::setNeedsLayout usage.
Sources/ObjectivelyMVC/ScrollBar.cUses setNeedsLayout after scroll interactions/state changes.
Sources/ObjectivelyMVC/Renderer.cReduces allocations for small line draws; appends vertices contiguously and coalesces compatible draw records.
Sources/ObjectivelyMVC/ProgressBar.cFixes progress calculation edge cases and uses setNeedsLayout.
Sources/ObjectivelyMVC/Panel.cInvalidates render frames after direct frame mutation to keep hit-testing accurate within the event batch.
Sources/ObjectivelyMVC/PageView.cUses setNeedsLayout when current page changes.
Sources/ObjectivelyMVC/Option.cUses setNeedsLayout when selection state changes.
Sources/ObjectivelyMVC/Label.cUses setNeedsLayout after dictionary binding.
Sources/ObjectivelyMVC/Control.cUses setNeedsLayout on state changes.
Sources/ObjectivelyMVC/CollectionView.cUses setNeedsLayout after reload.
ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-HUD.xcschemeAdds an Xcode shared scheme for the new HUD example.
ObjectivelyMVC.xcodeproj/project.pbxprojAdds the HUD target, source, and framework link settings to the Xcode project.
Examples/Makefile.amAdds HUD to Automake example programs and defines HUD_SOURCES.
Examples/HUD.cIntroduces the HUD benchmark app (builds view tree, runs timed frame passes, prints periodic stats).
Examples/.gitignoreIgnores the new HUD example binary.
Review details
  • Files reviewed: 27/27 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadExamples/HUD.c
jdolanand others added 4 commits September 1, 2026 22:24
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Replace the stack-or-malloc dual path with a fixed stack buffer,
emitting long polylines in batches: pushDrawArrays merges adjacent
records with equal texture and scissor, so a polyline of any length
still produces a single draw call, with no heap allocation for any
caller. In-tree callers pass at most four segments and take one
batch.
Addresses review feedback on #46.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jdolan
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Performance optimizations - #46

Open
jdolan wants to merge 19 commits into
mainfrom
performance-foundations
Open

Performance optimizations#46
jdolan wants to merge 19 commits into
mainfrom
performance-foundations

Conversation

@jdolan

Copy link
Copy Markdown
Owner

This pull request introduces a new HUD example application to the project, updates the build system and Xcode project files to support it, and includes several code improvements and bug fixes in the core library. The most significant changes are grouped below.


New Example Application: HUD


Core Library Improvements and Bug Fixes

  • Replaced direct assignments to the needsLayout property with calls to the setNeedsLayout method for better encapsulation and consistency in layout invalidation across multiple components (CollectionView, Control, Label, Option, PageView, ProgressBar). [1][2][3][4][5][6]
  • Improved ProgressBar value calculation to handle edge cases where max <= min, preventing divide-by-zero and ensuring correct progress display.
  • In Panel, after direct frame mutation, added a call to MVC_InvalidateRenderFrames() to ensure hit-testing uses up-to-date frame data.

Renderer Performance Optimization

  • Optimized Renderer::drawLines to use stack allocation for small vertex buffers, reducing heap allocations and improving rendering performance. [1][2]

jdolanand others added 15 commits September 1, 2026 21:13
Renders a representative game HUD (health, armor, ammo, crosshair,
countdown timer, chat log, toggling scoreboard) with each widget
updating on an interval, and times the style, layout, draw and
endFrame passes individually, printing a summary once per second.
Intended to measure CPU cost of driving a per-frame HUD with MVC
before and after performance changes. MVC_HUD_FRAMES=N exits after
N frames; MVC_HUD_HIDDEN=1 creates the window hidden.
Baseline (M-series macOS, MVC_HUD_FRAMES=1400 MVC_HUD_HIDDEN=1,
vsync ~120fps): idle frames style avg ~2-5us (max 233us), layout
avg ~0.5us (max 47us), draw avg ~8-38us (max 2.5ms), endFrame avg
~25-52us (max 226us); 6 draw calls idle, 26 with scoreboard shown.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduce propagating setters for the dirty flags: in addition to
setting the flag on the View, they mark needsLayoutSubviews or
needsApplyThemeSubviews on each ancestor, recording that a descendant
is dirty. The ancestor walk stops at the first already-marked View,
so repeated invalidations are amortized O(1).
Convert every in-tree flag write to the setters (View internals and
all widgets). No behavior change yet: the subtree flags are not read
until the traversal gating that follows.
Applications that assign needsLayout or needsApplyTheme directly
MUST migrate to the setters; direct writes will not propagate, and
the View may be skipped once applyThemeIfNeeded and layoutIfNeeded
are gated on the subtree flags.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both traversals previously recursed into every subview each frame,
making the per-frame cost O(tree) even when nothing was invalidated.
They now return immediately unless the View or a descendant is dirty,
making the steady-state cost O(dirty path).
The subtree flag is cleared before doing any work, so invalidations
that occur during the traversal itself (e.g. View::resize propagating
setNeedsLayout, or a widget marking a sibling mid-layout) survive to
the next frame rather than being lost.
Behavior change: a dirty View's subviews are no longer laid out
before the View itself. Previously layoutIfNeeded recursed into
children first and then re-arranged them via layoutWithConstraint,
laying out dirty children twice; children are now laid out once, by
their parent's layout pass, with a follow-up recursion catching any
descendant skipped by an overridden layoutSubviews.
Examples/HUD idle frames: style pass avg 2-5us -> 0.2us; layout
pass avg similar with maxima reduced. All tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both were recomputed from scratch on every call -- renderFrame is
O(depth), and clippingFrame recursed into every clipping ancestor's
clippingFrame, making it super-linear -- and they are called three
to six times per View per frame (Renderer::drawView, View::render,
subclass render methods), plus twice per View per mouse motion event.
Views now memoize both rects, stamped against a process-global render
frame generation. WindowController::renderTo bumps the generation via
MVC_InvalidateRenderFrames after layout and before drawing, so the
draw pass and subsequent hit-testing observe post-layout frames at
O(1) amortized per View. A zero generation (callers that never
invalidate, e.g. unit tests) disables cache reads entirely.
clippingFrame now intersects only the nearest clipping ancestor's
clippingFrame, which already folds in every outer clip; this is
equivalent to the previous every-ancestor loop without the redundant
super-linear work.
Frames mutated outside of layout (e.g. Panel dragging) are observed
by hit-testing at the next render rather than immediately;
MVC_InvalidateRenderFrames is exported for callers that need
same-batch precision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Text measurement went through Font::sizeCharacters on every call,
which strdups the string and invokes SDL_ttf per line; a container
measuring its children during layout re-measured every unchanged
Text descendant. The color escapes path is far more expensive still.
Cache the measured size on the Text, keyed by the Font's scale so
pixel-density changes re-measure without additional hooks, and
invalidate wherever the rendered texture is invalidated (setText,
setFont, color change, scale change, device reset) as well as in
awakeWithDictionary, whose text inlet bypasses setText.
Examples/HUD layout-pass maxima on widget-update frames drop from
~25-50us to ~5-10us. All tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pushDrawArrays now merges a record into the previous one when both
bind the same texture and scissor: vertices are appended contiguously,
so extending the prior record's vertexCount is equivalent and saves a
setScissor, bindFragmentSamplers and drawPrimitives per merged record
in endFrame. Blending order is preserved since only adjacent records
merge.
Vertices are appended with one capacity check and a direct array
write instead of a virtual Vector::add per vertex, and drawLines uses
a stack buffer for polylines up to 16 segments (drawLine and drawRect
always qualify) instead of a malloc/free per call -- previously every
bordered View allocated every frame.
Merging favors untextured geometry (backgrounds, borders, bevels
share the 1x1 white texture); distinct Text textures still cost one
draw each. All tests pass; Examples/HUD and Hello render identically.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collect each cache's value and validity into a single anonymous
struct member (renderFrameCache, clippingFrameCache on View;
naturalSizeCache on Text) rather than parallel loose fields, keeping
the value and the stamp or flag that guards it visibly paired.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code review found the self-first ordering introduced with traversal
gating regressed convergence: a descendant whose layout resizes it
marks its ancestors mid-pass, and with self-first ordering a clean
ancestor's dirty check has already run, deferring its re-arrangement
by one rendered frame per nesting level. Subviews-first restores the
original bottom-up single-pass convergence while keeping the gating.
Also complete setter adoption flagged by review: invalidateStyle's
enumerator now uses setNeedsApplyTheme rather than writing the flag
directly (the trailing self call becomes redundant), the layout unit
tests use setNeedsLayout, and ScrollBar's docs reference the setter.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
colorEscapes is a public, setter-less field that switches the
measurement path, and flipping it after the first measurement (the
documented usage) left the cache returning the escape-blind size
indefinitely. Include it in the cache key.
Addresses code review feedback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code review found the in-tree violators of the new invalidation
contract: Panel mutates its frame directly while dragging, leaving
hit-testing for the remainder of the event batch on the pre-move
cached clippingFrame, and Text resizes itself mid-draw on a pixel
density change but then read the renderFrame stamped earlier in the
same pass. Both now call MVC_InvalidateRenderFrames after mutating.
The HUD benchmark also never bumped the generation, so it measured
the frame-cache feature disabled; it now mirrors renderTo.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
progress and setValue computed value / (max - min), which reports
100% at zero progress for any non-zero min, and divides by zero when
max == min (reachable via bound inlets with no validation). Compute
(value - min) / (max - min), guarded to 0% when max <= min, and
derive setValue's fraction from progress rather than duplicating the
formula. Pre-existing defect surfaced by code review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirrors the ObjectivelyMVC-Hello command line tool target: HUD.c,
the same framework links and search paths, and a shared scheme, so
the benchmark can be run and profiled from Xcode.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Multiplies the scoreboard row count so frame cost can be measured
as a function of UI complexity; the default HUD is too small for
tree-size-dependent costs to dominate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The UI passes accounted for only a fraction of the process's CPU;
the acquire (RenderDevice::beginFrame through the clear pass) and
submit (RenderDevice::endFrame, which blocks on present with vsync)
columns attribute the remainder, distinguishing engine/driver floor
from MVC cost.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CopilotAI lite review requested due to automatic review settings September 2, 2026 02:10
Comment threadSources/ObjectivelyMVC/Renderer.c

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new Examples/HUD.c uses printf but does not include <stdio.h>, which can break builds under common C warning/error settings.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This pull request adds a new HUD example application intended for benchmarking, while also introducing render-frame caching/invalidation and several layout/theme invalidation improvements to reduce redundant work during rendering.

Changes:

  • Added a new HUD example app and integrated it into both Automake and Xcode (new scheme/target).
  • Introduced per-frame render/clipping frame caching with an explicit MVC_InvalidateRenderFrames() invalidation point after layout (and for direct frame mutation paths).
  • Optimized renderer batching (stack allocation for small line buffers; contiguous vertex appends and draw-call coalescing) and tightened layout invalidation via View::setNeedsLayout/View::setNeedsApplyTheme.
File summaries
FileDescription
Tests/ObjectivelyMVC/View.cUpdates tests to use View::setNeedsLayout instead of direct flag writes.
Sources/ObjectivelyMVC/WindowController.cInvalidates render-frame caches after layout, before draw.
Sources/ObjectivelyMVC/View.hAdds cached frame fields + subtree invalidation flags; exports MVC_InvalidateRenderFrames().
Sources/ObjectivelyMVC/View.cImplements caching/invalidation generation, subtree invalidation propagation, and faster clipping/render frame computation.
Sources/ObjectivelyMVC/TextView.cSwitches to setNeedsLayout on edit/text changes.
Sources/ObjectivelyMVC/Text.hAdds naturalSize cache state to Text.
Sources/ObjectivelyMVC/Text.cImplements/invalidates naturalSize caching; invalidates render frames on device-reset resize mid-draw.
Sources/ObjectivelyMVC/TabView.cUses setNeedsLayout on tab selection changes.
Sources/ObjectivelyMVC/TableView.cReplaces direct needsLayout writes with setNeedsLayout during layout/reload.
Sources/ObjectivelyMVC/Slider.cUses setNeedsLayout when value changes.
Sources/ObjectivelyMVC/Select.cUses setNeedsLayout after option mutations/selection.
Sources/ObjectivelyMVC/ScrollView.cUses setNeedsLayout for layout-affecting state changes.
Sources/ObjectivelyMVC/ScrollBar.hUpdates docs to reflect View::setNeedsLayout usage.
Sources/ObjectivelyMVC/ScrollBar.cUses setNeedsLayout after scroll interactions/state changes.
Sources/ObjectivelyMVC/Renderer.cReduces allocations for small line draws; appends vertices contiguously and coalesces compatible draw records.
Sources/ObjectivelyMVC/ProgressBar.cFixes progress calculation edge cases and uses setNeedsLayout.
Sources/ObjectivelyMVC/Panel.cInvalidates render frames after direct frame mutation to keep hit-testing accurate within the event batch.
Sources/ObjectivelyMVC/PageView.cUses setNeedsLayout when current page changes.
Sources/ObjectivelyMVC/Option.cUses setNeedsLayout when selection state changes.
Sources/ObjectivelyMVC/Label.cUses setNeedsLayout after dictionary binding.
Sources/ObjectivelyMVC/Control.cUses setNeedsLayout on state changes.
Sources/ObjectivelyMVC/CollectionView.cUses setNeedsLayout after reload.
ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-HUD.xcschemeAdds an Xcode shared scheme for the new HUD example.
ObjectivelyMVC.xcodeproj/project.pbxprojAdds the HUD target, source, and framework link settings to the Xcode project.
Examples/Makefile.amAdds HUD to Automake example programs and defines HUD_SOURCES.
Examples/HUD.cIntroduces the HUD benchmark app (builds view tree, runs timed frame passes, prints periodic stats).
Examples/.gitignoreIgnores the new HUD example binary.
Review details
  • Files reviewed: 27/27 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadExamples/HUD.c
jdolanand others added 4 commits September 1, 2026 22:24
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Replace the stack-or-malloc dual path with a fixed stack buffer,
emitting long polylines in batches: pushDrawArrays merges adjacent
records with equal texture and scissor, so a polyline of any length
still produces a single draw call, with no heap allocation for any
caller. In-tree callers pass at most four segments and take one
batch.
Addresses review feedback on #46.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jdolan
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Performance optimizations - #46

Open
jdolan wants to merge 19 commits into
mainfrom
performance-foundations
Open

Performance optimizations#46
jdolan wants to merge 19 commits into
mainfrom
performance-foundations

Conversation

@jdolan

Copy link
Copy Markdown
Owner

This pull request introduces a new HUD example application to the project, updates the build system and Xcode project files to support it, and includes several code improvements and bug fixes in the core library. The most significant changes are grouped below.


New Example Application: HUD


Core Library Improvements and Bug Fixes

  • Replaced direct assignments to the needsLayout property with calls to the setNeedsLayout method for better encapsulation and consistency in layout invalidation across multiple components (CollectionView, Control, Label, Option, PageView, ProgressBar). [1][2][3][4][5][6]
  • Improved ProgressBar value calculation to handle edge cases where max <= min, preventing divide-by-zero and ensuring correct progress display.
  • In Panel, after direct frame mutation, added a call to MVC_InvalidateRenderFrames() to ensure hit-testing uses up-to-date frame data.

Renderer Performance Optimization

  • Optimized Renderer::drawLines to use stack allocation for small vertex buffers, reducing heap allocations and improving rendering performance. [1][2]

jdolanand others added 15 commits September 1, 2026 21:13
Renders a representative game HUD (health, armor, ammo, crosshair,
countdown timer, chat log, toggling scoreboard) with each widget
updating on an interval, and times the style, layout, draw and
endFrame passes individually, printing a summary once per second.
Intended to measure CPU cost of driving a per-frame HUD with MVC
before and after performance changes. MVC_HUD_FRAMES=N exits after
N frames; MVC_HUD_HIDDEN=1 creates the window hidden.
Baseline (M-series macOS, MVC_HUD_FRAMES=1400 MVC_HUD_HIDDEN=1,
vsync ~120fps): idle frames style avg ~2-5us (max 233us), layout
avg ~0.5us (max 47us), draw avg ~8-38us (max 2.5ms), endFrame avg
~25-52us (max 226us); 6 draw calls idle, 26 with scoreboard shown.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduce propagating setters for the dirty flags: in addition to
setting the flag on the View, they mark needsLayoutSubviews or
needsApplyThemeSubviews on each ancestor, recording that a descendant
is dirty. The ancestor walk stops at the first already-marked View,
so repeated invalidations are amortized O(1).
Convert every in-tree flag write to the setters (View internals and
all widgets). No behavior change yet: the subtree flags are not read
until the traversal gating that follows.
Applications that assign needsLayout or needsApplyTheme directly
MUST migrate to the setters; direct writes will not propagate, and
the View may be skipped once applyThemeIfNeeded and layoutIfNeeded
are gated on the subtree flags.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both traversals previously recursed into every subview each frame,
making the per-frame cost O(tree) even when nothing was invalidated.
They now return immediately unless the View or a descendant is dirty,
making the steady-state cost O(dirty path).
The subtree flag is cleared before doing any work, so invalidations
that occur during the traversal itself (e.g. View::resize propagating
setNeedsLayout, or a widget marking a sibling mid-layout) survive to
the next frame rather than being lost.
Behavior change: a dirty View's subviews are no longer laid out
before the View itself. Previously layoutIfNeeded recursed into
children first and then re-arranged them via layoutWithConstraint,
laying out dirty children twice; children are now laid out once, by
their parent's layout pass, with a follow-up recursion catching any
descendant skipped by an overridden layoutSubviews.
Examples/HUD idle frames: style pass avg 2-5us -> 0.2us; layout
pass avg similar with maxima reduced. All tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both were recomputed from scratch on every call -- renderFrame is
O(depth), and clippingFrame recursed into every clipping ancestor's
clippingFrame, making it super-linear -- and they are called three
to six times per View per frame (Renderer::drawView, View::render,
subclass render methods), plus twice per View per mouse motion event.
Views now memoize both rects, stamped against a process-global render
frame generation. WindowController::renderTo bumps the generation via
MVC_InvalidateRenderFrames after layout and before drawing, so the
draw pass and subsequent hit-testing observe post-layout frames at
O(1) amortized per View. A zero generation (callers that never
invalidate, e.g. unit tests) disables cache reads entirely.
clippingFrame now intersects only the nearest clipping ancestor's
clippingFrame, which already folds in every outer clip; this is
equivalent to the previous every-ancestor loop without the redundant
super-linear work.
Frames mutated outside of layout (e.g. Panel dragging) are observed
by hit-testing at the next render rather than immediately;
MVC_InvalidateRenderFrames is exported for callers that need
same-batch precision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Text measurement went through Font::sizeCharacters on every call,
which strdups the string and invokes SDL_ttf per line; a container
measuring its children during layout re-measured every unchanged
Text descendant. The color escapes path is far more expensive still.
Cache the measured size on the Text, keyed by the Font's scale so
pixel-density changes re-measure without additional hooks, and
invalidate wherever the rendered texture is invalidated (setText,
setFont, color change, scale change, device reset) as well as in
awakeWithDictionary, whose text inlet bypasses setText.
Examples/HUD layout-pass maxima on widget-update frames drop from
~25-50us to ~5-10us. All tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pushDrawArrays now merges a record into the previous one when both
bind the same texture and scissor: vertices are appended contiguously,
so extending the prior record's vertexCount is equivalent and saves a
setScissor, bindFragmentSamplers and drawPrimitives per merged record
in endFrame. Blending order is preserved since only adjacent records
merge.
Vertices are appended with one capacity check and a direct array
write instead of a virtual Vector::add per vertex, and drawLines uses
a stack buffer for polylines up to 16 segments (drawLine and drawRect
always qualify) instead of a malloc/free per call -- previously every
bordered View allocated every frame.
Merging favors untextured geometry (backgrounds, borders, bevels
share the 1x1 white texture); distinct Text textures still cost one
draw each. All tests pass; Examples/HUD and Hello render identically.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collect each cache's value and validity into a single anonymous
struct member (renderFrameCache, clippingFrameCache on View;
naturalSizeCache on Text) rather than parallel loose fields, keeping
the value and the stamp or flag that guards it visibly paired.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code review found the self-first ordering introduced with traversal
gating regressed convergence: a descendant whose layout resizes it
marks its ancestors mid-pass, and with self-first ordering a clean
ancestor's dirty check has already run, deferring its re-arrangement
by one rendered frame per nesting level. Subviews-first restores the
original bottom-up single-pass convergence while keeping the gating.
Also complete setter adoption flagged by review: invalidateStyle's
enumerator now uses setNeedsApplyTheme rather than writing the flag
directly (the trailing self call becomes redundant), the layout unit
tests use setNeedsLayout, and ScrollBar's docs reference the setter.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
colorEscapes is a public, setter-less field that switches the
measurement path, and flipping it after the first measurement (the
documented usage) left the cache returning the escape-blind size
indefinitely. Include it in the cache key.
Addresses code review feedback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code review found the in-tree violators of the new invalidation
contract: Panel mutates its frame directly while dragging, leaving
hit-testing for the remainder of the event batch on the pre-move
cached clippingFrame, and Text resizes itself mid-draw on a pixel
density change but then read the renderFrame stamped earlier in the
same pass. Both now call MVC_InvalidateRenderFrames after mutating.
The HUD benchmark also never bumped the generation, so it measured
the frame-cache feature disabled; it now mirrors renderTo.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
progress and setValue computed value / (max - min), which reports
100% at zero progress for any non-zero min, and divides by zero when
max == min (reachable via bound inlets with no validation). Compute
(value - min) / (max - min), guarded to 0% when max <= min, and
derive setValue's fraction from progress rather than duplicating the
formula. Pre-existing defect surfaced by code review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirrors the ObjectivelyMVC-Hello command line tool target: HUD.c,
the same framework links and search paths, and a shared scheme, so
the benchmark can be run and profiled from Xcode.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Multiplies the scoreboard row count so frame cost can be measured
as a function of UI complexity; the default HUD is too small for
tree-size-dependent costs to dominate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The UI passes accounted for only a fraction of the process's CPU;
the acquire (RenderDevice::beginFrame through the clear pass) and
submit (RenderDevice::endFrame, which blocks on present with vsync)
columns attribute the remainder, distinguishing engine/driver floor
from MVC cost.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CopilotAI lite review requested due to automatic review settings September 2, 2026 02:10
Comment threadSources/ObjectivelyMVC/Renderer.c

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new Examples/HUD.c uses printf but does not include <stdio.h>, which can break builds under common C warning/error settings.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This pull request adds a new HUD example application intended for benchmarking, while also introducing render-frame caching/invalidation and several layout/theme invalidation improvements to reduce redundant work during rendering.

Changes:

  • Added a new HUD example app and integrated it into both Automake and Xcode (new scheme/target).
  • Introduced per-frame render/clipping frame caching with an explicit MVC_InvalidateRenderFrames() invalidation point after layout (and for direct frame mutation paths).
  • Optimized renderer batching (stack allocation for small line buffers; contiguous vertex appends and draw-call coalescing) and tightened layout invalidation via View::setNeedsLayout/View::setNeedsApplyTheme.
File summaries
FileDescription
Tests/ObjectivelyMVC/View.cUpdates tests to use View::setNeedsLayout instead of direct flag writes.
Sources/ObjectivelyMVC/WindowController.cInvalidates render-frame caches after layout, before draw.
Sources/ObjectivelyMVC/View.hAdds cached frame fields + subtree invalidation flags; exports MVC_InvalidateRenderFrames().
Sources/ObjectivelyMVC/View.cImplements caching/invalidation generation, subtree invalidation propagation, and faster clipping/render frame computation.
Sources/ObjectivelyMVC/TextView.cSwitches to setNeedsLayout on edit/text changes.
Sources/ObjectivelyMVC/Text.hAdds naturalSize cache state to Text.
Sources/ObjectivelyMVC/Text.cImplements/invalidates naturalSize caching; invalidates render frames on device-reset resize mid-draw.
Sources/ObjectivelyMVC/TabView.cUses setNeedsLayout on tab selection changes.
Sources/ObjectivelyMVC/TableView.cReplaces direct needsLayout writes with setNeedsLayout during layout/reload.
Sources/ObjectivelyMVC/Slider.cUses setNeedsLayout when value changes.
Sources/ObjectivelyMVC/Select.cUses setNeedsLayout after option mutations/selection.
Sources/ObjectivelyMVC/ScrollView.cUses setNeedsLayout for layout-affecting state changes.
Sources/ObjectivelyMVC/ScrollBar.hUpdates docs to reflect View::setNeedsLayout usage.
Sources/ObjectivelyMVC/ScrollBar.cUses setNeedsLayout after scroll interactions/state changes.
Sources/ObjectivelyMVC/Renderer.cReduces allocations for small line draws; appends vertices contiguously and coalesces compatible draw records.
Sources/ObjectivelyMVC/ProgressBar.cFixes progress calculation edge cases and uses setNeedsLayout.
Sources/ObjectivelyMVC/Panel.cInvalidates render frames after direct frame mutation to keep hit-testing accurate within the event batch.
Sources/ObjectivelyMVC/PageView.cUses setNeedsLayout when current page changes.
Sources/ObjectivelyMVC/Option.cUses setNeedsLayout when selection state changes.
Sources/ObjectivelyMVC/Label.cUses setNeedsLayout after dictionary binding.
Sources/ObjectivelyMVC/Control.cUses setNeedsLayout on state changes.
Sources/ObjectivelyMVC/CollectionView.cUses setNeedsLayout after reload.
ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-HUD.xcschemeAdds an Xcode shared scheme for the new HUD example.
ObjectivelyMVC.xcodeproj/project.pbxprojAdds the HUD target, source, and framework link settings to the Xcode project.
Examples/Makefile.amAdds HUD to Automake example programs and defines HUD_SOURCES.
Examples/HUD.cIntroduces the HUD benchmark app (builds view tree, runs timed frame passes, prints periodic stats).
Examples/.gitignoreIgnores the new HUD example binary.
Review details
  • Files reviewed: 27/27 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadExamples/HUD.c
jdolanand others added 4 commits September 1, 2026 22:24
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Replace the stack-or-malloc dual path with a fixed stack buffer,
emitting long polylines in batches: pushDrawArrays merges adjacent
records with equal texture and scissor, so a polyline of any length
still produces a single draw call, with no heap allocation for any
caller. In-tree callers pass at most four segments and take one
batch.
Addresses review feedback on #46.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jdolan
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Performance optimizations - #46

Open
jdolan wants to merge 19 commits into
mainfrom
performance-foundations
Open

Performance optimizations#46
jdolan wants to merge 19 commits into
mainfrom
performance-foundations

Conversation

@jdolan

Copy link
Copy Markdown
Owner

This pull request introduces a new HUD example application to the project, updates the build system and Xcode project files to support it, and includes several code improvements and bug fixes in the core library. The most significant changes are grouped below.


New Example Application: HUD


Core Library Improvements and Bug Fixes

  • Replaced direct assignments to the needsLayout property with calls to the setNeedsLayout method for better encapsulation and consistency in layout invalidation across multiple components (CollectionView, Control, Label, Option, PageView, ProgressBar). [1][2][3][4][5][6]
  • Improved ProgressBar value calculation to handle edge cases where max <= min, preventing divide-by-zero and ensuring correct progress display.
  • In Panel, after direct frame mutation, added a call to MVC_InvalidateRenderFrames() to ensure hit-testing uses up-to-date frame data.

Renderer Performance Optimization

  • Optimized Renderer::drawLines to use stack allocation for small vertex buffers, reducing heap allocations and improving rendering performance. [1][2]

jdolanand others added 15 commits September 1, 2026 21:13
Renders a representative game HUD (health, armor, ammo, crosshair,
countdown timer, chat log, toggling scoreboard) with each widget
updating on an interval, and times the style, layout, draw and
endFrame passes individually, printing a summary once per second.
Intended to measure CPU cost of driving a per-frame HUD with MVC
before and after performance changes. MVC_HUD_FRAMES=N exits after
N frames; MVC_HUD_HIDDEN=1 creates the window hidden.
Baseline (M-series macOS, MVC_HUD_FRAMES=1400 MVC_HUD_HIDDEN=1,
vsync ~120fps): idle frames style avg ~2-5us (max 233us), layout
avg ~0.5us (max 47us), draw avg ~8-38us (max 2.5ms), endFrame avg
~25-52us (max 226us); 6 draw calls idle, 26 with scoreboard shown.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduce propagating setters for the dirty flags: in addition to
setting the flag on the View, they mark needsLayoutSubviews or
needsApplyThemeSubviews on each ancestor, recording that a descendant
is dirty. The ancestor walk stops at the first already-marked View,
so repeated invalidations are amortized O(1).
Convert every in-tree flag write to the setters (View internals and
all widgets). No behavior change yet: the subtree flags are not read
until the traversal gating that follows.
Applications that assign needsLayout or needsApplyTheme directly
MUST migrate to the setters; direct writes will not propagate, and
the View may be skipped once applyThemeIfNeeded and layoutIfNeeded
are gated on the subtree flags.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both traversals previously recursed into every subview each frame,
making the per-frame cost O(tree) even when nothing was invalidated.
They now return immediately unless the View or a descendant is dirty,
making the steady-state cost O(dirty path).
The subtree flag is cleared before doing any work, so invalidations
that occur during the traversal itself (e.g. View::resize propagating
setNeedsLayout, or a widget marking a sibling mid-layout) survive to
the next frame rather than being lost.
Behavior change: a dirty View's subviews are no longer laid out
before the View itself. Previously layoutIfNeeded recursed into
children first and then re-arranged them via layoutWithConstraint,
laying out dirty children twice; children are now laid out once, by
their parent's layout pass, with a follow-up recursion catching any
descendant skipped by an overridden layoutSubviews.
Examples/HUD idle frames: style pass avg 2-5us -> 0.2us; layout
pass avg similar with maxima reduced. All tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both were recomputed from scratch on every call -- renderFrame is
O(depth), and clippingFrame recursed into every clipping ancestor's
clippingFrame, making it super-linear -- and they are called three
to six times per View per frame (Renderer::drawView, View::render,
subclass render methods), plus twice per View per mouse motion event.
Views now memoize both rects, stamped against a process-global render
frame generation. WindowController::renderTo bumps the generation via
MVC_InvalidateRenderFrames after layout and before drawing, so the
draw pass and subsequent hit-testing observe post-layout frames at
O(1) amortized per View. A zero generation (callers that never
invalidate, e.g. unit tests) disables cache reads entirely.
clippingFrame now intersects only the nearest clipping ancestor's
clippingFrame, which already folds in every outer clip; this is
equivalent to the previous every-ancestor loop without the redundant
super-linear work.
Frames mutated outside of layout (e.g. Panel dragging) are observed
by hit-testing at the next render rather than immediately;
MVC_InvalidateRenderFrames is exported for callers that need
same-batch precision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Text measurement went through Font::sizeCharacters on every call,
which strdups the string and invokes SDL_ttf per line; a container
measuring its children during layout re-measured every unchanged
Text descendant. The color escapes path is far more expensive still.
Cache the measured size on the Text, keyed by the Font's scale so
pixel-density changes re-measure without additional hooks, and
invalidate wherever the rendered texture is invalidated (setText,
setFont, color change, scale change, device reset) as well as in
awakeWithDictionary, whose text inlet bypasses setText.
Examples/HUD layout-pass maxima on widget-update frames drop from
~25-50us to ~5-10us. All tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pushDrawArrays now merges a record into the previous one when both
bind the same texture and scissor: vertices are appended contiguously,
so extending the prior record's vertexCount is equivalent and saves a
setScissor, bindFragmentSamplers and drawPrimitives per merged record
in endFrame. Blending order is preserved since only adjacent records
merge.
Vertices are appended with one capacity check and a direct array
write instead of a virtual Vector::add per vertex, and drawLines uses
a stack buffer for polylines up to 16 segments (drawLine and drawRect
always qualify) instead of a malloc/free per call -- previously every
bordered View allocated every frame.
Merging favors untextured geometry (backgrounds, borders, bevels
share the 1x1 white texture); distinct Text textures still cost one
draw each. All tests pass; Examples/HUD and Hello render identically.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collect each cache's value and validity into a single anonymous
struct member (renderFrameCache, clippingFrameCache on View;
naturalSizeCache on Text) rather than parallel loose fields, keeping
the value and the stamp or flag that guards it visibly paired.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code review found the self-first ordering introduced with traversal
gating regressed convergence: a descendant whose layout resizes it
marks its ancestors mid-pass, and with self-first ordering a clean
ancestor's dirty check has already run, deferring its re-arrangement
by one rendered frame per nesting level. Subviews-first restores the
original bottom-up single-pass convergence while keeping the gating.
Also complete setter adoption flagged by review: invalidateStyle's
enumerator now uses setNeedsApplyTheme rather than writing the flag
directly (the trailing self call becomes redundant), the layout unit
tests use setNeedsLayout, and ScrollBar's docs reference the setter.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
colorEscapes is a public, setter-less field that switches the
measurement path, and flipping it after the first measurement (the
documented usage) left the cache returning the escape-blind size
indefinitely. Include it in the cache key.
Addresses code review feedback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code review found the in-tree violators of the new invalidation
contract: Panel mutates its frame directly while dragging, leaving
hit-testing for the remainder of the event batch on the pre-move
cached clippingFrame, and Text resizes itself mid-draw on a pixel
density change but then read the renderFrame stamped earlier in the
same pass. Both now call MVC_InvalidateRenderFrames after mutating.
The HUD benchmark also never bumped the generation, so it measured
the frame-cache feature disabled; it now mirrors renderTo.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
progress and setValue computed value / (max - min), which reports
100% at zero progress for any non-zero min, and divides by zero when
max == min (reachable via bound inlets with no validation). Compute
(value - min) / (max - min), guarded to 0% when max <= min, and
derive setValue's fraction from progress rather than duplicating the
formula. Pre-existing defect surfaced by code review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirrors the ObjectivelyMVC-Hello command line tool target: HUD.c,
the same framework links and search paths, and a shared scheme, so
the benchmark can be run and profiled from Xcode.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Multiplies the scoreboard row count so frame cost can be measured
as a function of UI complexity; the default HUD is too small for
tree-size-dependent costs to dominate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The UI passes accounted for only a fraction of the process's CPU;
the acquire (RenderDevice::beginFrame through the clear pass) and
submit (RenderDevice::endFrame, which blocks on present with vsync)
columns attribute the remainder, distinguishing engine/driver floor
from MVC cost.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CopilotAI lite review requested due to automatic review settings September 2, 2026 02:10
Comment threadSources/ObjectivelyMVC/Renderer.c

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new Examples/HUD.c uses printf but does not include <stdio.h>, which can break builds under common C warning/error settings.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This pull request adds a new HUD example application intended for benchmarking, while also introducing render-frame caching/invalidation and several layout/theme invalidation improvements to reduce redundant work during rendering.

Changes:

  • Added a new HUD example app and integrated it into both Automake and Xcode (new scheme/target).
  • Introduced per-frame render/clipping frame caching with an explicit MVC_InvalidateRenderFrames() invalidation point after layout (and for direct frame mutation paths).
  • Optimized renderer batching (stack allocation for small line buffers; contiguous vertex appends and draw-call coalescing) and tightened layout invalidation via View::setNeedsLayout/View::setNeedsApplyTheme.
File summaries
FileDescription
Tests/ObjectivelyMVC/View.cUpdates tests to use View::setNeedsLayout instead of direct flag writes.
Sources/ObjectivelyMVC/WindowController.cInvalidates render-frame caches after layout, before draw.
Sources/ObjectivelyMVC/View.hAdds cached frame fields + subtree invalidation flags; exports MVC_InvalidateRenderFrames().
Sources/ObjectivelyMVC/View.cImplements caching/invalidation generation, subtree invalidation propagation, and faster clipping/render frame computation.
Sources/ObjectivelyMVC/TextView.cSwitches to setNeedsLayout on edit/text changes.
Sources/ObjectivelyMVC/Text.hAdds naturalSize cache state to Text.
Sources/ObjectivelyMVC/Text.cImplements/invalidates naturalSize caching; invalidates render frames on device-reset resize mid-draw.
Sources/ObjectivelyMVC/TabView.cUses setNeedsLayout on tab selection changes.
Sources/ObjectivelyMVC/TableView.cReplaces direct needsLayout writes with setNeedsLayout during layout/reload.
Sources/ObjectivelyMVC/Slider.cUses setNeedsLayout when value changes.
Sources/ObjectivelyMVC/Select.cUses setNeedsLayout after option mutations/selection.
Sources/ObjectivelyMVC/ScrollView.cUses setNeedsLayout for layout-affecting state changes.
Sources/ObjectivelyMVC/ScrollBar.hUpdates docs to reflect View::setNeedsLayout usage.
Sources/ObjectivelyMVC/ScrollBar.cUses setNeedsLayout after scroll interactions/state changes.
Sources/ObjectivelyMVC/Renderer.cReduces allocations for small line draws; appends vertices contiguously and coalesces compatible draw records.
Sources/ObjectivelyMVC/ProgressBar.cFixes progress calculation edge cases and uses setNeedsLayout.
Sources/ObjectivelyMVC/Panel.cInvalidates render frames after direct frame mutation to keep hit-testing accurate within the event batch.
Sources/ObjectivelyMVC/PageView.cUses setNeedsLayout when current page changes.
Sources/ObjectivelyMVC/Option.cUses setNeedsLayout when selection state changes.
Sources/ObjectivelyMVC/Label.cUses setNeedsLayout after dictionary binding.
Sources/ObjectivelyMVC/Control.cUses setNeedsLayout on state changes.
Sources/ObjectivelyMVC/CollectionView.cUses setNeedsLayout after reload.
ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-HUD.xcschemeAdds an Xcode shared scheme for the new HUD example.
ObjectivelyMVC.xcodeproj/project.pbxprojAdds the HUD target, source, and framework link settings to the Xcode project.
Examples/Makefile.amAdds HUD to Automake example programs and defines HUD_SOURCES.
Examples/HUD.cIntroduces the HUD benchmark app (builds view tree, runs timed frame passes, prints periodic stats).
Examples/.gitignoreIgnores the new HUD example binary.
Review details
  • Files reviewed: 27/27 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadExamples/HUD.c
jdolanand others added 4 commits September 1, 2026 22:24
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Replace the stack-or-malloc dual path with a fixed stack buffer,
emitting long polylines in batches: pushDrawArrays merges adjacent
records with equal texture and scissor, so a polyline of any length
still produces a single draw call, with no heap allocation for any
caller. In-tree callers pass at most four segments and take one
batch.
Addresses review feedback on #46.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jdolan
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Performance optimizations - #46

Open
jdolan wants to merge 19 commits into
mainfrom
performance-foundations
Open

Performance optimizations#46
jdolan wants to merge 19 commits into
mainfrom
performance-foundations

Conversation

@jdolan

Copy link
Copy Markdown
Owner

This pull request introduces a new HUD example application to the project, updates the build system and Xcode project files to support it, and includes several code improvements and bug fixes in the core library. The most significant changes are grouped below.


New Example Application: HUD


Core Library Improvements and Bug Fixes

  • Replaced direct assignments to the needsLayout property with calls to the setNeedsLayout method for better encapsulation and consistency in layout invalidation across multiple components (CollectionView, Control, Label, Option, PageView, ProgressBar). [1][2][3][4][5][6]
  • Improved ProgressBar value calculation to handle edge cases where max <= min, preventing divide-by-zero and ensuring correct progress display.
  • In Panel, after direct frame mutation, added a call to MVC_InvalidateRenderFrames() to ensure hit-testing uses up-to-date frame data.

Renderer Performance Optimization

  • Optimized Renderer::drawLines to use stack allocation for small vertex buffers, reducing heap allocations and improving rendering performance. [1][2]

jdolanand others added 15 commits September 1, 2026 21:13
Renders a representative game HUD (health, armor, ammo, crosshair,
countdown timer, chat log, toggling scoreboard) with each widget
updating on an interval, and times the style, layout, draw and
endFrame passes individually, printing a summary once per second.
Intended to measure CPU cost of driving a per-frame HUD with MVC
before and after performance changes. MVC_HUD_FRAMES=N exits after
N frames; MVC_HUD_HIDDEN=1 creates the window hidden.
Baseline (M-series macOS, MVC_HUD_FRAMES=1400 MVC_HUD_HIDDEN=1,
vsync ~120fps): idle frames style avg ~2-5us (max 233us), layout
avg ~0.5us (max 47us), draw avg ~8-38us (max 2.5ms), endFrame avg
~25-52us (max 226us); 6 draw calls idle, 26 with scoreboard shown.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduce propagating setters for the dirty flags: in addition to
setting the flag on the View, they mark needsLayoutSubviews or
needsApplyThemeSubviews on each ancestor, recording that a descendant
is dirty. The ancestor walk stops at the first already-marked View,
so repeated invalidations are amortized O(1).
Convert every in-tree flag write to the setters (View internals and
all widgets). No behavior change yet: the subtree flags are not read
until the traversal gating that follows.
Applications that assign needsLayout or needsApplyTheme directly
MUST migrate to the setters; direct writes will not propagate, and
the View may be skipped once applyThemeIfNeeded and layoutIfNeeded
are gated on the subtree flags.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both traversals previously recursed into every subview each frame,
making the per-frame cost O(tree) even when nothing was invalidated.
They now return immediately unless the View or a descendant is dirty,
making the steady-state cost O(dirty path).
The subtree flag is cleared before doing any work, so invalidations
that occur during the traversal itself (e.g. View::resize propagating
setNeedsLayout, or a widget marking a sibling mid-layout) survive to
the next frame rather than being lost.
Behavior change: a dirty View's subviews are no longer laid out
before the View itself. Previously layoutIfNeeded recursed into
children first and then re-arranged them via layoutWithConstraint,
laying out dirty children twice; children are now laid out once, by
their parent's layout pass, with a follow-up recursion catching any
descendant skipped by an overridden layoutSubviews.
Examples/HUD idle frames: style pass avg 2-5us -> 0.2us; layout
pass avg similar with maxima reduced. All tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both were recomputed from scratch on every call -- renderFrame is
O(depth), and clippingFrame recursed into every clipping ancestor's
clippingFrame, making it super-linear -- and they are called three
to six times per View per frame (Renderer::drawView, View::render,
subclass render methods), plus twice per View per mouse motion event.
Views now memoize both rects, stamped against a process-global render
frame generation. WindowController::renderTo bumps the generation via
MVC_InvalidateRenderFrames after layout and before drawing, so the
draw pass and subsequent hit-testing observe post-layout frames at
O(1) amortized per View. A zero generation (callers that never
invalidate, e.g. unit tests) disables cache reads entirely.
clippingFrame now intersects only the nearest clipping ancestor's
clippingFrame, which already folds in every outer clip; this is
equivalent to the previous every-ancestor loop without the redundant
super-linear work.
Frames mutated outside of layout (e.g. Panel dragging) are observed
by hit-testing at the next render rather than immediately;
MVC_InvalidateRenderFrames is exported for callers that need
same-batch precision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Text measurement went through Font::sizeCharacters on every call,
which strdups the string and invokes SDL_ttf per line; a container
measuring its children during layout re-measured every unchanged
Text descendant. The color escapes path is far more expensive still.
Cache the measured size on the Text, keyed by the Font's scale so
pixel-density changes re-measure without additional hooks, and
invalidate wherever the rendered texture is invalidated (setText,
setFont, color change, scale change, device reset) as well as in
awakeWithDictionary, whose text inlet bypasses setText.
Examples/HUD layout-pass maxima on widget-update frames drop from
~25-50us to ~5-10us. All tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pushDrawArrays now merges a record into the previous one when both
bind the same texture and scissor: vertices are appended contiguously,
so extending the prior record's vertexCount is equivalent and saves a
setScissor, bindFragmentSamplers and drawPrimitives per merged record
in endFrame. Blending order is preserved since only adjacent records
merge.
Vertices are appended with one capacity check and a direct array
write instead of a virtual Vector::add per vertex, and drawLines uses
a stack buffer for polylines up to 16 segments (drawLine and drawRect
always qualify) instead of a malloc/free per call -- previously every
bordered View allocated every frame.
Merging favors untextured geometry (backgrounds, borders, bevels
share the 1x1 white texture); distinct Text textures still cost one
draw each. All tests pass; Examples/HUD and Hello render identically.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collect each cache's value and validity into a single anonymous
struct member (renderFrameCache, clippingFrameCache on View;
naturalSizeCache on Text) rather than parallel loose fields, keeping
the value and the stamp or flag that guards it visibly paired.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code review found the self-first ordering introduced with traversal
gating regressed convergence: a descendant whose layout resizes it
marks its ancestors mid-pass, and with self-first ordering a clean
ancestor's dirty check has already run, deferring its re-arrangement
by one rendered frame per nesting level. Subviews-first restores the
original bottom-up single-pass convergence while keeping the gating.
Also complete setter adoption flagged by review: invalidateStyle's
enumerator now uses setNeedsApplyTheme rather than writing the flag
directly (the trailing self call becomes redundant), the layout unit
tests use setNeedsLayout, and ScrollBar's docs reference the setter.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
colorEscapes is a public, setter-less field that switches the
measurement path, and flipping it after the first measurement (the
documented usage) left the cache returning the escape-blind size
indefinitely. Include it in the cache key.
Addresses code review feedback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code review found the in-tree violators of the new invalidation
contract: Panel mutates its frame directly while dragging, leaving
hit-testing for the remainder of the event batch on the pre-move
cached clippingFrame, and Text resizes itself mid-draw on a pixel
density change but then read the renderFrame stamped earlier in the
same pass. Both now call MVC_InvalidateRenderFrames after mutating.
The HUD benchmark also never bumped the generation, so it measured
the frame-cache feature disabled; it now mirrors renderTo.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
progress and setValue computed value / (max - min), which reports
100% at zero progress for any non-zero min, and divides by zero when
max == min (reachable via bound inlets with no validation). Compute
(value - min) / (max - min), guarded to 0% when max <= min, and
derive setValue's fraction from progress rather than duplicating the
formula. Pre-existing defect surfaced by code review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirrors the ObjectivelyMVC-Hello command line tool target: HUD.c,
the same framework links and search paths, and a shared scheme, so
the benchmark can be run and profiled from Xcode.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Multiplies the scoreboard row count so frame cost can be measured
as a function of UI complexity; the default HUD is too small for
tree-size-dependent costs to dominate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The UI passes accounted for only a fraction of the process's CPU;
the acquire (RenderDevice::beginFrame through the clear pass) and
submit (RenderDevice::endFrame, which blocks on present with vsync)
columns attribute the remainder, distinguishing engine/driver floor
from MVC cost.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CopilotAI lite review requested due to automatic review settings September 2, 2026 02:10
Comment threadSources/ObjectivelyMVC/Renderer.c

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new Examples/HUD.c uses printf but does not include <stdio.h>, which can break builds under common C warning/error settings.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This pull request adds a new HUD example application intended for benchmarking, while also introducing render-frame caching/invalidation and several layout/theme invalidation improvements to reduce redundant work during rendering.

Changes:

  • Added a new HUD example app and integrated it into both Automake and Xcode (new scheme/target).
  • Introduced per-frame render/clipping frame caching with an explicit MVC_InvalidateRenderFrames() invalidation point after layout (and for direct frame mutation paths).
  • Optimized renderer batching (stack allocation for small line buffers; contiguous vertex appends and draw-call coalescing) and tightened layout invalidation via View::setNeedsLayout/View::setNeedsApplyTheme.
File summaries
FileDescription
Tests/ObjectivelyMVC/View.cUpdates tests to use View::setNeedsLayout instead of direct flag writes.
Sources/ObjectivelyMVC/WindowController.cInvalidates render-frame caches after layout, before draw.
Sources/ObjectivelyMVC/View.hAdds cached frame fields + subtree invalidation flags; exports MVC_InvalidateRenderFrames().
Sources/ObjectivelyMVC/View.cImplements caching/invalidation generation, subtree invalidation propagation, and faster clipping/render frame computation.
Sources/ObjectivelyMVC/TextView.cSwitches to setNeedsLayout on edit/text changes.
Sources/ObjectivelyMVC/Text.hAdds naturalSize cache state to Text.
Sources/ObjectivelyMVC/Text.cImplements/invalidates naturalSize caching; invalidates render frames on device-reset resize mid-draw.
Sources/ObjectivelyMVC/TabView.cUses setNeedsLayout on tab selection changes.
Sources/ObjectivelyMVC/TableView.cReplaces direct needsLayout writes with setNeedsLayout during layout/reload.
Sources/ObjectivelyMVC/Slider.cUses setNeedsLayout when value changes.
Sources/ObjectivelyMVC/Select.cUses setNeedsLayout after option mutations/selection.
Sources/ObjectivelyMVC/ScrollView.cUses setNeedsLayout for layout-affecting state changes.
Sources/ObjectivelyMVC/ScrollBar.hUpdates docs to reflect View::setNeedsLayout usage.
Sources/ObjectivelyMVC/ScrollBar.cUses setNeedsLayout after scroll interactions/state changes.
Sources/ObjectivelyMVC/Renderer.cReduces allocations for small line draws; appends vertices contiguously and coalesces compatible draw records.
Sources/ObjectivelyMVC/ProgressBar.cFixes progress calculation edge cases and uses setNeedsLayout.
Sources/ObjectivelyMVC/Panel.cInvalidates render frames after direct frame mutation to keep hit-testing accurate within the event batch.
Sources/ObjectivelyMVC/PageView.cUses setNeedsLayout when current page changes.
Sources/ObjectivelyMVC/Option.cUses setNeedsLayout when selection state changes.
Sources/ObjectivelyMVC/Label.cUses setNeedsLayout after dictionary binding.
Sources/ObjectivelyMVC/Control.cUses setNeedsLayout on state changes.
Sources/ObjectivelyMVC/CollectionView.cUses setNeedsLayout after reload.
ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-HUD.xcschemeAdds an Xcode shared scheme for the new HUD example.
ObjectivelyMVC.xcodeproj/project.pbxprojAdds the HUD target, source, and framework link settings to the Xcode project.
Examples/Makefile.amAdds HUD to Automake example programs and defines HUD_SOURCES.
Examples/HUD.cIntroduces the HUD benchmark app (builds view tree, runs timed frame passes, prints periodic stats).
Examples/.gitignoreIgnores the new HUD example binary.
Review details
  • Files reviewed: 27/27 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadExamples/HUD.c
jdolanand others added 4 commits September 1, 2026 22:24
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Replace the stack-or-malloc dual path with a fixed stack buffer,
emitting long polylines in batches: pushDrawArrays merges adjacent
records with equal texture and scissor, so a polyline of any length
still produces a single draw call, with no heap allocation for any
caller. In-tree callers pass at most four segments and take one
batch.
Addresses review feedback on #46.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jdolan
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Performance optimizations - #46

Open
jdolan wants to merge 19 commits into
mainfrom
performance-foundations
Open

Performance optimizations#46
jdolan wants to merge 19 commits into
mainfrom
performance-foundations

Conversation

@jdolan

Copy link
Copy Markdown
Owner

This pull request introduces a new HUD example application to the project, updates the build system and Xcode project files to support it, and includes several code improvements and bug fixes in the core library. The most significant changes are grouped below.


New Example Application: HUD


Core Library Improvements and Bug Fixes

  • Replaced direct assignments to the needsLayout property with calls to the setNeedsLayout method for better encapsulation and consistency in layout invalidation across multiple components (CollectionView, Control, Label, Option, PageView, ProgressBar). [1][2][3][4][5][6]
  • Improved ProgressBar value calculation to handle edge cases where max <= min, preventing divide-by-zero and ensuring correct progress display.
  • In Panel, after direct frame mutation, added a call to MVC_InvalidateRenderFrames() to ensure hit-testing uses up-to-date frame data.

Renderer Performance Optimization

  • Optimized Renderer::drawLines to use stack allocation for small vertex buffers, reducing heap allocations and improving rendering performance. [1][2]

jdolanand others added 15 commits September 1, 2026 21:13
Renders a representative game HUD (health, armor, ammo, crosshair,
countdown timer, chat log, toggling scoreboard) with each widget
updating on an interval, and times the style, layout, draw and
endFrame passes individually, printing a summary once per second.
Intended to measure CPU cost of driving a per-frame HUD with MVC
before and after performance changes. MVC_HUD_FRAMES=N exits after
N frames; MVC_HUD_HIDDEN=1 creates the window hidden.
Baseline (M-series macOS, MVC_HUD_FRAMES=1400 MVC_HUD_HIDDEN=1,
vsync ~120fps): idle frames style avg ~2-5us (max 233us), layout
avg ~0.5us (max 47us), draw avg ~8-38us (max 2.5ms), endFrame avg
~25-52us (max 226us); 6 draw calls idle, 26 with scoreboard shown.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduce propagating setters for the dirty flags: in addition to
setting the flag on the View, they mark needsLayoutSubviews or
needsApplyThemeSubviews on each ancestor, recording that a descendant
is dirty. The ancestor walk stops at the first already-marked View,
so repeated invalidations are amortized O(1).
Convert every in-tree flag write to the setters (View internals and
all widgets). No behavior change yet: the subtree flags are not read
until the traversal gating that follows.
Applications that assign needsLayout or needsApplyTheme directly
MUST migrate to the setters; direct writes will not propagate, and
the View may be skipped once applyThemeIfNeeded and layoutIfNeeded
are gated on the subtree flags.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both traversals previously recursed into every subview each frame,
making the per-frame cost O(tree) even when nothing was invalidated.
They now return immediately unless the View or a descendant is dirty,
making the steady-state cost O(dirty path).
The subtree flag is cleared before doing any work, so invalidations
that occur during the traversal itself (e.g. View::resize propagating
setNeedsLayout, or a widget marking a sibling mid-layout) survive to
the next frame rather than being lost.
Behavior change: a dirty View's subviews are no longer laid out
before the View itself. Previously layoutIfNeeded recursed into
children first and then re-arranged them via layoutWithConstraint,
laying out dirty children twice; children are now laid out once, by
their parent's layout pass, with a follow-up recursion catching any
descendant skipped by an overridden layoutSubviews.
Examples/HUD idle frames: style pass avg 2-5us -> 0.2us; layout
pass avg similar with maxima reduced. All tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both were recomputed from scratch on every call -- renderFrame is
O(depth), and clippingFrame recursed into every clipping ancestor's
clippingFrame, making it super-linear -- and they are called three
to six times per View per frame (Renderer::drawView, View::render,
subclass render methods), plus twice per View per mouse motion event.
Views now memoize both rects, stamped against a process-global render
frame generation. WindowController::renderTo bumps the generation via
MVC_InvalidateRenderFrames after layout and before drawing, so the
draw pass and subsequent hit-testing observe post-layout frames at
O(1) amortized per View. A zero generation (callers that never
invalidate, e.g. unit tests) disables cache reads entirely.
clippingFrame now intersects only the nearest clipping ancestor's
clippingFrame, which already folds in every outer clip; this is
equivalent to the previous every-ancestor loop without the redundant
super-linear work.
Frames mutated outside of layout (e.g. Panel dragging) are observed
by hit-testing at the next render rather than immediately;
MVC_InvalidateRenderFrames is exported for callers that need
same-batch precision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Text measurement went through Font::sizeCharacters on every call,
which strdups the string and invokes SDL_ttf per line; a container
measuring its children during layout re-measured every unchanged
Text descendant. The color escapes path is far more expensive still.
Cache the measured size on the Text, keyed by the Font's scale so
pixel-density changes re-measure without additional hooks, and
invalidate wherever the rendered texture is invalidated (setText,
setFont, color change, scale change, device reset) as well as in
awakeWithDictionary, whose text inlet bypasses setText.
Examples/HUD layout-pass maxima on widget-update frames drop from
~25-50us to ~5-10us. All tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pushDrawArrays now merges a record into the previous one when both
bind the same texture and scissor: vertices are appended contiguously,
so extending the prior record's vertexCount is equivalent and saves a
setScissor, bindFragmentSamplers and drawPrimitives per merged record
in endFrame. Blending order is preserved since only adjacent records
merge.
Vertices are appended with one capacity check and a direct array
write instead of a virtual Vector::add per vertex, and drawLines uses
a stack buffer for polylines up to 16 segments (drawLine and drawRect
always qualify) instead of a malloc/free per call -- previously every
bordered View allocated every frame.
Merging favors untextured geometry (backgrounds, borders, bevels
share the 1x1 white texture); distinct Text textures still cost one
draw each. All tests pass; Examples/HUD and Hello render identically.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collect each cache's value and validity into a single anonymous
struct member (renderFrameCache, clippingFrameCache on View;
naturalSizeCache on Text) rather than parallel loose fields, keeping
the value and the stamp or flag that guards it visibly paired.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code review found the self-first ordering introduced with traversal
gating regressed convergence: a descendant whose layout resizes it
marks its ancestors mid-pass, and with self-first ordering a clean
ancestor's dirty check has already run, deferring its re-arrangement
by one rendered frame per nesting level. Subviews-first restores the
original bottom-up single-pass convergence while keeping the gating.
Also complete setter adoption flagged by review: invalidateStyle's
enumerator now uses setNeedsApplyTheme rather than writing the flag
directly (the trailing self call becomes redundant), the layout unit
tests use setNeedsLayout, and ScrollBar's docs reference the setter.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
colorEscapes is a public, setter-less field that switches the
measurement path, and flipping it after the first measurement (the
documented usage) left the cache returning the escape-blind size
indefinitely. Include it in the cache key.
Addresses code review feedback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code review found the in-tree violators of the new invalidation
contract: Panel mutates its frame directly while dragging, leaving
hit-testing for the remainder of the event batch on the pre-move
cached clippingFrame, and Text resizes itself mid-draw on a pixel
density change but then read the renderFrame stamped earlier in the
same pass. Both now call MVC_InvalidateRenderFrames after mutating.
The HUD benchmark also never bumped the generation, so it measured
the frame-cache feature disabled; it now mirrors renderTo.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
progress and setValue computed value / (max - min), which reports
100% at zero progress for any non-zero min, and divides by zero when
max == min (reachable via bound inlets with no validation). Compute
(value - min) / (max - min), guarded to 0% when max <= min, and
derive setValue's fraction from progress rather than duplicating the
formula. Pre-existing defect surfaced by code review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirrors the ObjectivelyMVC-Hello command line tool target: HUD.c,
the same framework links and search paths, and a shared scheme, so
the benchmark can be run and profiled from Xcode.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Multiplies the scoreboard row count so frame cost can be measured
as a function of UI complexity; the default HUD is too small for
tree-size-dependent costs to dominate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The UI passes accounted for only a fraction of the process's CPU;
the acquire (RenderDevice::beginFrame through the clear pass) and
submit (RenderDevice::endFrame, which blocks on present with vsync)
columns attribute the remainder, distinguishing engine/driver floor
from MVC cost.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CopilotAI lite review requested due to automatic review settings September 2, 2026 02:10
Comment threadSources/ObjectivelyMVC/Renderer.c

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new Examples/HUD.c uses printf but does not include <stdio.h>, which can break builds under common C warning/error settings.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This pull request adds a new HUD example application intended for benchmarking, while also introducing render-frame caching/invalidation and several layout/theme invalidation improvements to reduce redundant work during rendering.

Changes:

  • Added a new HUD example app and integrated it into both Automake and Xcode (new scheme/target).
  • Introduced per-frame render/clipping frame caching with an explicit MVC_InvalidateRenderFrames() invalidation point after layout (and for direct frame mutation paths).
  • Optimized renderer batching (stack allocation for small line buffers; contiguous vertex appends and draw-call coalescing) and tightened layout invalidation via View::setNeedsLayout/View::setNeedsApplyTheme.
File summaries
FileDescription
Tests/ObjectivelyMVC/View.cUpdates tests to use View::setNeedsLayout instead of direct flag writes.
Sources/ObjectivelyMVC/WindowController.cInvalidates render-frame caches after layout, before draw.
Sources/ObjectivelyMVC/View.hAdds cached frame fields + subtree invalidation flags; exports MVC_InvalidateRenderFrames().
Sources/ObjectivelyMVC/View.cImplements caching/invalidation generation, subtree invalidation propagation, and faster clipping/render frame computation.
Sources/ObjectivelyMVC/TextView.cSwitches to setNeedsLayout on edit/text changes.
Sources/ObjectivelyMVC/Text.hAdds naturalSize cache state to Text.
Sources/ObjectivelyMVC/Text.cImplements/invalidates naturalSize caching; invalidates render frames on device-reset resize mid-draw.
Sources/ObjectivelyMVC/TabView.cUses setNeedsLayout on tab selection changes.
Sources/ObjectivelyMVC/TableView.cReplaces direct needsLayout writes with setNeedsLayout during layout/reload.
Sources/ObjectivelyMVC/Slider.cUses setNeedsLayout when value changes.
Sources/ObjectivelyMVC/Select.cUses setNeedsLayout after option mutations/selection.
Sources/ObjectivelyMVC/ScrollView.cUses setNeedsLayout for layout-affecting state changes.
Sources/ObjectivelyMVC/ScrollBar.hUpdates docs to reflect View::setNeedsLayout usage.
Sources/ObjectivelyMVC/ScrollBar.cUses setNeedsLayout after scroll interactions/state changes.
Sources/ObjectivelyMVC/Renderer.cReduces allocations for small line draws; appends vertices contiguously and coalesces compatible draw records.
Sources/ObjectivelyMVC/ProgressBar.cFixes progress calculation edge cases and uses setNeedsLayout.
Sources/ObjectivelyMVC/Panel.cInvalidates render frames after direct frame mutation to keep hit-testing accurate within the event batch.
Sources/ObjectivelyMVC/PageView.cUses setNeedsLayout when current page changes.
Sources/ObjectivelyMVC/Option.cUses setNeedsLayout when selection state changes.
Sources/ObjectivelyMVC/Label.cUses setNeedsLayout after dictionary binding.
Sources/ObjectivelyMVC/Control.cUses setNeedsLayout on state changes.
Sources/ObjectivelyMVC/CollectionView.cUses setNeedsLayout after reload.
ObjectivelyMVC.xcodeproj/xcshareddata/xcschemes/ObjectivelyMVC-HUD.xcschemeAdds an Xcode shared scheme for the new HUD example.
ObjectivelyMVC.xcodeproj/project.pbxprojAdds the HUD target, source, and framework link settings to the Xcode project.
Examples/Makefile.amAdds HUD to Automake example programs and defines HUD_SOURCES.
Examples/HUD.cIntroduces the HUD benchmark app (builds view tree, runs timed frame passes, prints periodic stats).
Examples/.gitignoreIgnores the new HUD example binary.
Review details
  • Files reviewed: 27/27 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadExamples/HUD.c
jdolanand others added 4 commits September 1, 2026 22:24
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Replace the stack-or-malloc dual path with a fixed stack buffer,
emitting long polylines in batches: pushDrawArrays merges adjacent
records with equal texture and scissor, so a polyline of any length
still produces a single draw call, with no heap allocation for any
caller. In-tree callers pass at most four segments and take one
batch.
Addresses review feedback on #46.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jdolan