Skip to content

Make the F2 UI usable on ultrawide screens - #20

Merged
Force67 merged 3 commits into
tiltedphoques:masterfrom
jotagalindo:fix/ultrawide-overlay
Nov 5, 2026
Merged

Force67 merged 3 commits into
tiltedphoques:masterfrom
jotagalindo:fix/ultrawide-overlay

Conversation

@jotagalindo

@jotagalindo jotagalindo commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Hi!

This comes from tiltedphoques/TiltedEvolution#945.

I play on a 5120x1440 monitor and the F2 menu was almost impossible to use: it showed up in pieces and clicks didn't respond, especially outdoors. At 1920x1080 it worked perfectly and used AI to make this fix, Claude specifically. I tested it myself in game.

What was happening

  • View upload: OverlayRenderHandlerD3D11 uploaded the whole view to the GPU on every CEF paint. At this resolution that is about 30 MB each time, and with low FPS several paints pile up per frame.
  • CEF waiting on Skyrim: CEF draws the UI on the GPU and copies each frame back to memory. Outdoors Skyrim keeps the GPU maxed out, so CEF ends up waiting and drops frames: cef_debug.log shows Client rejects proposal to capture frame.

What changes

  • OverlayRenderHandlerD3D11: uploads to the GPU only the area that changed, at most once per frame. It keeps its name and interface, so TiltedEvolution only needs the submodule bump.
  • CEF starts with disable-gpu and disable-gpu-compositing (in OverlayApp): the UI is simple HTML and CSS, so drawing it on the CPU is cheap. This applies to anything that uses OverlayApp.
  • View size: taken from the swap chain. Before, it came from the bound render target, which can be 512x512 while loading.
  • Cursor: scaled by height instead of width; before, it was huge on ultrawide screens.

Testing: the same code, as part of tiltedphoques/TiltedEvolution#945, was built on windows-2022 and tested in game at 5120x1440 indoors and outdoors, on a 2080 Super. This TiltedUI version builds with TiltedEvolution on windows-2022 (build);

Before and after (5120x1440, outdoors in Whiterun)

Before (current dev): the menu shows up half drawn, parts of it come and go, and the cursor is huge.

f2-ultrawide-before-fix.mp4

After (this PR): the connect dialog and the settings show up complete right away, and the cursor has its normal size.

f2-ultrawide-after-fix.mp4

Thanks a lot for everything, and all feedback is welcome!

jotagalindo and others added 2 commits November 5, 2026 13:47
The D3D11 handler copied and uploaded the whole CEF view on every paint,
through a deferred context. At 5120x1440 that is about 30 MB per paint, and
paints pile up in each game frame when the game runs below the UI's 60 fps,
so the menu barely rendered and ignored clicks, worst outdoors.

CEF paints are now merged into a CPU copy of the view, and the render thread
uploads only the repainted area with UpdateSubresource, at most once per frame.
The view is sized after the swap chain's back buffer instead of the render
target bound when the UI is drawn, which was 512x512 for a moment while
loading and made CEF lay out and repaint the whole UI twice. The cursor is
scaled by the screen height instead of the width, so it no longer grows on
wide screens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With only the upload fixed, the UI still showed up half drawn outdoors at
5120x1440. cef_debug.log shows CEF dropping UI frames ("Client rejects
proposal to capture frame") right when the UI changes: CEF rasterizes on
the GPU and copies each frame back to memory for OnPaint, and that work
waits behind the game's frames when the game keeps the GPU busy.

CEF now starts with disable-gpu and disable-gpu-compositing, so the UI
(plain HTML and CSS) is drawn on the CPU straight into the memory OnPaint
reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jotagalindo
jotagalindo marked this pull request as ready for review November 5, 2026 13:23

@Force67 Force67 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

  • Out-of-bounds read when the view shrinks: a dirty rect pending at the old size survives the resize (UploadDirtyPixels skips while pixel and texture sizes differ), then the size-change branch in OnPaint unions it with (0, 0, newW, newH). The box ends up larger than m_pixels and the texture, so UpdateSubresource reads past the vector. Set m_hasDirtyPixels = false before AddDirtyRect in that branch, and maybe clamp the box in UploadDirtyPixels too.
  • disable-gpu / disable-gpu-compositing hit every OverlayApp user. Software raster of full repaints at 5120x1440 costs CPU, so please also check 1080p on a weaker CPU. Opt-in per app might be safer.
  • The comment saying the back buffer matches the window client area isn't always true (the ini resolution can differ, mouse coords come from ScreenToClient). Using the swap chain is still better, just soften the comment.
  • m_pixelLock is held during a full 30 MB UpdateSubresource, which stalls OnPaint. Fine for now.
  • GetRenderTargetSize() no longer reads the render target, rename it.
  • Comments are long, the ones in OverlayApp.cpp and on the class could be a lot shorter.

- When the view shrinks, drop the dirty area of the old size before marking
  the new view dirty, and clamp the uploaded box to the view, so
  UpdateSubresource never reads past the pixel copy.
- OverlayApp no longer adds disable-gpu and disable-gpu-compositing. An app
  that wants CPU drawing adds them in its own OnBeforeCommandLineProcessing.
- GetRenderTargetSize() is now UpdateViewSize(), since it reads the swap
  chain and updates the view size.
- Shorter comments, and no claim that the back buffer always matches the
  window's client area.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@miredirex

miredirex commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

@jotagalindo do you happen to know what exactly fixed the stutter (low fps)? If it's the "disable" flags, then yes it's worth keeping

Dirty rects are a good fix too (#3 can be closed once this is merged)

@jotagalindo

jotagalindo commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor Author

@miredirex both helped, in different ways:

  • Dirty rects: before, the whole view was uploaded on every paint, about 30 MB at 5120x1440. With only this change the F2 menu got better, but outdoors (in Whiterun) it still showed up half drawn.
  • The disable-gpu / disable-gpu-compositing flags is what finished the fix. cef_debug.log showed Client rejects proposal to capture frame right when the UI changed, so CEF was dropping frames while the game kept the GPU maxed out. With the flags that message went away and the menu shows up complete right away.

I didn't measure FPS for each change separately; what I checked was that the menu showed up complete and responded to clicks. I can measure it with and without the flags if that helps.

@jotagalindo jotagalindo left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@Force67 Thanks for the review. Everything is addressed in 15210c2:

  • Out-of-bounds read when the view shrinks: on a size change, the dirty area of the old size is dropped before the new view is marked dirty, and the box in UploadDirtyPixels is also always clamped to the view.
  • Back buffer comment: softened; it no longer says it always matches the window's client area.
  • GetRenderTargetSize() is now UpdateViewSize().
  • Comments: shortened, including the one on the class. The one in OverlayApp.cpp is gone.
  • m_pixelLock during UpdateSubresource: left as is, as you said.

I will write more in next comments.

@jotagalindo

Copy link
Copy Markdown
Contributor Author

@miredirex About the measurements, I made another build in my TiltedEvolution fork to measure. It has this commit plus two things that are only there for testing: a counter of the UI frames CEF hands to the game each second, and a file that, when present, removes the flags. That way both modes use the same client and the same server (release).

  • PC: i9-9900K (8 cores, 16 threads) and RTX 2080 Super.
  • CPU sampled every 2 s from each process's CPU time, as % of the whole CPU. TPProcess are CEF's processes.
  • In every session: Whiterun, Breezehome and outdoors, moving through the menu.
5120x1440, with flags 5120x1440, without flags 1080p, 4 threads, with flags
TPProcess CPU while the UI changes (avg / max) 1.0% / 2.2% 2.6% / 6.7% 0.9% / 1.6% (3.6% / 6.4% of the 4 threads)
TPProcess CPU while the UI is idle 0.0% 0.9% 0.1%
Frames dropped by CEF (Client rejects proposal to capture frame) 0 191 0
UI frames per second, max 38 9.5 37
  • I don't have a weaker CPU:, so I simulated it on my i9-9900K by limiting the game and all of CEF's processes to 4 threads (4 cores without HyperThreading). It isn't the same as a really slow CPU, since those cores still run at the 9900K's clock (up to 5 GHz). In the next few days I'll be playing with a friend who has an older PC, and I'll try to measure how it goes for him too.
  • With 4 threads, the game's CPU didn't go up with the menu open (10.0% vs 9.5% with the UI idle).

With these numbers, drawing on the CPU costs little even with 4 threads, and less than on the GPU, probably (i think) because on the GPU CEF has to copy every frame back to memory.

So the flags are what fix the stutter. Dirty rects lower the cost of uploading the view, but on their own they aren't enough.

@jotagalindo

Copy link
Copy Markdown
Contributor Author

@Force67 @miredirex with these numbers, how would you prefer the flags? I think we have two options

  • A. Opt-in (what the PR does now): OverlayApp doesn't add them, and each app turns them on in its own OnBeforeCommandLineProcessing. TiltedEvolution would do it with an OverlayApp subclass, which is already written in Make the F2 UI usable on ultrawide screens TiltedEvolution#945. I could reopen that PR with just that change plus the submodule bump.
  • B. On by default in TiltedUI (as this PR had it before this review): OverlayApp adds them for everyone again, and TiltedEvolution only needs the submodule bump.

The measurements answer the CPU cost question. Not forcing it on everyone who uses OverlayApp is more of a design decision, so I'll leave it up to you. The effect in game is the same either way it's the same two flags, only where they're added changes.

@jotagalindo
jotagalindo requested a review from Force67 November 5, 2026 16:40
@miredirex

miredirex commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

I think we have two options

I don't think this matters too much, really. Whatever is simpler code wise

I could reopen that PR with just that change plus the submodule bump

That's totally fine too

@jotagalindo

Copy link
Copy Markdown
Contributor Author

@miredirex thanks! Then I'll go with option A, in TiltedUI the flags are opt-in as @Force67 suggested and TiltedEvolution turns them on with its own subclass. I've already reopened tiltedphoques/TiltedEvolution#945 for that, and its submodule is already prepared.

@Force67 Force67 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

jotagalindo added a commit to jotagalindo/TiltedEvolution that referenced this pull request Nov 5, 2026
The view upload fix now lives in TiltedUI (tiltedphoques/TiltedUI#20), so the
client goes back to OverlayRenderHandlerD3D11 and only opts in to CPU drawing
with CpuRenderedOverlayApp. TiltedUI points to the head of that PR, which ends
up in TiltedUI's master when the PR is merged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Force67
Force67 merged commit c7789de into tiltedphoques:master Nov 5, 2026
0 of 3 checks passed
jotagalindo added a commit to jotagalindo/TiltedEvolution that referenced this pull request Nov 5, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
miredirex pushed a commit to tiltedphoques/TiltedEvolution that referenced this pull request Nov 5, 2026
* Make the F2 UI usable on ultrawide screens

TiltedUI's D3D11 overlay handler copied and uploaded the whole CEF view on
every paint, through a deferred context. At 5120x1440 that is about 30 MB per
paint, and paints pile up in each game frame when the game runs below the UI's
60 fps, so the menu barely rendered and ignored clicks, worst outdoors.

Details in tiltedphoques/TiltedUI#20
---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
absol89 pushed a commit to absol89/TiltedEvolutionScriptFixes that referenced this pull request Nov 5, 2026
* Make the F2 UI usable on ultrawide screens

TiltedUI's D3D11 overlay handler copied and uploaded the whole CEF view on
every paint, through a deferred context. At 5120x1440 that is about 30 MB per
paint, and paints pile up in each game frame when the game runs below the UI's
60 fps, so the menu barely rendered and ignored clicks, worst outdoors.

Details in tiltedphoques/TiltedUI#20
---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit ed46728)
Sign up for free to 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.

3 participants