-
Notifications
You must be signed in to change notification settings - Fork 0
The settings window
src/ui/settings_host.cpp, src/ui/settings_window.cpp, src/app.cpp
The settings can be drawn over the picture or given a window of their own, which can then be moved to a second monitor or set beside the preview. The switch is under Settings → Display.
The second window shares one Direct3D device with the preview, and sharing a device turned out to be the whole story behind a stutter that reached the desktop's own mouse cursor. This page is mostly a post-mortem, because everything here was found by measurement and several of the intermediate fixes were wrong.
Ten tabs, grouped by what a setting affects rather than by what it technically is.
| Source | the card and the signal coming out of it |
| Picture | what is done to that signal for viewing |
| HDR | the whole subject, including what the outputs write |
| Audio | playback, delay, levels, microphone |
| Recording | everything that leaves CapView as a file or a camera |
| Encoder | ffmpeg, which encoder, and what it is told |
| Display | the window, the theme, the language, the overlays |
| Keys | |
| Profiles | |
| Updates |
That distinction did real work. The switches deciding whether a recording, a screenshot or the virtual camera keeps its full range used to sit under Display — three tabs away from any of the three things they govern.
Recording and Encoder are separate because they answer different questions. Where does the file go, how big, how often is a decision about the recording; which chip encodes it and how hard it works is a decision about the machine. One is set once and the other every session. ffmpeg lives with the encoder for the same reason: it is what the encoder runs on, not a property of the recording.
HDR earns a tab rather than a section, because it is not a display setting. Its source curve belongs to the card, its tone mapping to the screen, and three of its switches decide what the recorder, the screenshots and the virtual camera write. A subject that reaches across four tabs is a subject.
Which tab is open is state ImGui keeps per context, and the settings are
drawn into a different context depending on whether they live in their own
window. So SettingsWindow tracks it explicitly:
if (!tabRestored_) { // once per run, from the configuration
tabRestored_ = true;
activeTab_ = cfg().app.settingsTab;
wantTab_ = activeTab_;
}
ImGuiContext* nowContext = ImGui::GetCurrentContext();
if (nowContext != tabContext_) { // and on every context switch
tabContext_ = nowContext;
wantTab_ = activeTab_;
}Without the second half, switching to the separate window dropped you back on the Source tab every time.
The symptom was that having the settings in their own window made everything stutter — including the system mouse cursor, which is what said it was not an ordinary frame-rate problem.
The dialog's frame was drawn and presented from inside the preview's own frame. Presenting a second swap chain part-way through another window's frame flushes everything already queued for it.
It runs after the preview's present now.
The preview redrew on every wake-up of the message loop, and a second window on screen produces a steady stream of messages.
Measured: the whole pipeline — video shaders, colour work, readbacks for the recorder and the camera, a present — ran 235 times a second to show a source delivering 25.
Drawing is paced by the picture now: a new frame, a field falling due, or a floor of thirty a second so meters and toasts keep moving. See Latency for the loop.
This fix was wrong the first time. The first version paced against a fixed 33 ms clock, which beats against 25 fps and produced a slow visible pulse. Pacing has to be against the picture, not against a timer.
This is the one that actually cost the milliseconds.
SetMaximumFrameLatency(1) is what makes the preview's latency as short as it
is — and it applies to the device, not to a swap chain. With one frame of
queue and two swap chains, each present waited for the other's frame to retire.
Measured: the dialog's present cost 8–14 ms and the preview's 3 ms. At a queue of three, both are under a millisecond.
D3DContext::SetFrameLatency(UINT) raises the queue to three only while the
dialog is on screen, because the rest of the time the short queue is the point.
The dialog's swap chain also uses BufferCount = 3 and ALLOW_TEARING where
supported (swapchainFlags_ / presentFlags_).
Dragging a window puts Windows into a loop of its own that does not return until the mouse is released. CapView's own loop stops running and the preview stops with it — measured at 1.24 seconds frozen during a drag.
The only way back in is from a message the window receives while that loop is running.
The obvious answer, and it fails:
case WM_ENTERSIZEMOVE:
::SetTimer(hwnd, 1, 16, nullptr);WM_TIMER is the lowest priority message there is, and Windows only
generates one when the queue is otherwise empty. During a drag the queue never
is. Measured: at an interval of 8 ms it fired seven times in a second.
The timer is still set, but only as a floor for when the mouse is held still.
case WM_MOVING:
case WM_SIZING:
self->PumpModalFrame();
break; // and on to DefWindowProc, which does the actual movingWindows sends these continuously while the window is being dragged or resized —
once per mouse movement — and unlike WM_TIMER they are real messages that
cannot be starved by the flood of mouse input causing the problem in the first
place.
Measured after: 125 pumps a second against the timer's seven.
if (lastModalTick_ != 0 && now - lastModalTick_ < 30) return;Rendering on every WM_MOVING puts a whole frame — shaders, readbacks, two
presents — between each mouse movement and the window following it, which trades
one kind of stutter for another. That was the second wrong fix: the preview ran
beautifully and the window lagged behind the cursor.
Thirty a second, not eighty. The preview has nothing more to show than the capture card delivers, and every frame drawn here is time the window is not following the mouse.
Measured during a drag: a preview frame costs 0.6 to 0.8 ms and the window keeps running at better than thirty a second.
inFrameCallback_ guards against reentrancy, since the frame callback can itself
pump messages.
Reported against 1.7: with the settings in their own window, the preview occasionally flickers black in places. Only with the analogue, interlaced source.
Not yet reproduced here. Two attempts, both defeated by the instrument rather than by the bug: screen capture runs at about twenty frames a second against a sixty frame preview, so a single dropped frame is caught roughly one time in three, and on moving content a per-pixel "darker than both neighbours" test flags every moving edge in the picture.
Ruled out along the way, each by reading rather than by guessing:
-
Scissor rectangles left behind by ImGui. The renderer's rasteriser state
has
ScissorEnable = FALSE, so a stale rectangle cannot clip the video. -
Undefined back-buffer content under
FLIP_DISCARD.D3DContext::BeginFrameclears the whole surface every frame. -
Repeated
SetMaximumFrameLatencycalls.D3DContext::SetFrameLatencyalready returns early when the value has not changed.
One genuine defect was found on the same trail and is fixed: with the settings window open, the message queue never goes quiet, so the second field of every interlaced frame was being dropped — see Latency. Whether that was also the flicker is unknown; it is a different symptom, and claiming it as the cause without a reproduction would be guessing twice.
The next step, when a live interlaced source is available, is to instrument
RenderFrame rather than photograph the screen.
Dragging the settings window across the preview still hitches slightly. That one is not CapView's to fix.
The work is not the problem — 0.6 to 0.8 ms a frame, measured. Two overlapping windows have to be composed together by the desktop, and one of them is repainting thirty times a second. Dragging the dialog onto another monitor, or off the preview, makes it go away.
The window was created owned but without WS_EX_APPWINDOW, which is how Windows
is asked for a taskbar button. Without one, a window has nowhere to go when it is
minimised, and lands as a stub in a corner of the screen the way windows did
before there was a taskbar.
MakeWindowAssociation is deliberately not called — it is per-factory, not
per-window, and calling it for the second window would change the first one's
Alt+Enter behaviour.
Both windows share one ImFontAtlas — the glyphs are the same and one copy on
the GPU is enough.
But the DX11 backend stores the atlas's texture id in the atlas. Closing the second window released the texture the first one was still drawing with, and the whole interface went blank until restart.
The atlas is still shared; the texture is rebuilt on the way out with
ImGui_ImplDX11_CreateDeviceObjects().
Switching between embedded and separate destroys and rebuilds the window, so it came up wherever Windows decided each time.
SettingsHost::placement() reads GetWindowPlacement — the restored
rectangle, not the current one, so a window read while minimised or maximised is
not remembered at the wrong size. App::RememberSettingsWindow() copies it into
the configuration before Destroy(), and Create() checks the remembered
position against the virtual screen before using it:
// A hundred pixels of title bar has to remain reachable.
if (where.x + 100 > desk.left && where.x < desk.right - 100 &&
where.y >= desk.top && where.y < desk.bottom - 40) { x = where.x; y = where.y; }A window remembered on a monitor that is no longer there does not come up somewhere nobody can reach it.
Reasonable question, and the answer is no — the problems above were all Direct3D and Win32 problems, not ImGui ones.
ImGui does assume it is called once per frame from whatever thread owns the device, which is exactly the assumption that makes the modal-loop problem visible. But any immediate-mode or retained-mode UI sharing a device with a latency-tuned swap chain hits the same three issues: the present ordering, the device-wide frame latency, and Windows' modal loop. A different framework would have hidden the first, made no difference to the second, and hit the third identically.