A vertical ScrollView that extends into the bottom inset (like a navigation bar) adds
that much space after its contents, so the end can be scrolled out from under it.
It only did that when the view reached the very bottom of the screen, and then added
the whole inset. Since 58fa522d48 inset the content area of dialog screens on
screens over 600 dp, a phone like the S21 Ultra (617 dp wide) got a 1.7 dp inset,
which turned the extra space off. Add exactly the part of the view that lies in the
inset instead, and skip the bottom shadow in that case.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Dialog screens (tabbed, two-pane, simple and the others built on
UIBaseDialogScreen) center their content at up to 1400 wide, instead of
stretching settings rows and buttons across very wide windows.
On large screens, those screens and the main screen also get an inset on all
sides, since controls right at the edge are hard to see there. It grows with
the screen's smaller dimension, from nothing at 600 to the full inset at
1000, and is always full on TVs. The game, popups, and the screens that
position things to match the game's screen (display layout, touch control
layout and tests, custom button and visual mapping) are left as they were.
The root's bounds are computed by UIScreen::GetContentBounds, since
UI::Margins can't hold widths this large.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ChoiceGroups no longer have hardcoded margins. A new ChoiceGroupList holds them,
with padding and spacing that give the same look as before. Settings tabs, developer
tools, the two-pane settings columns, the pause menu and the rest put their groups in
one, and a few screens adjust its padding and spacing to their layout.
Also add the two groups in the remote disc streaming settings that were created but
never added, and make the settings search look inside groups: it only checked a tab's
direct children, so with ChoiceGroups a filter hid everything.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Grid children are given LinearLayoutParams almost everywhere, so their margins are
taken from those as well as from GridLayoutParams, which gains margins.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SetContentGravity centers the children, or moves them to the end, when they don't
fill the layout.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Lay out children across the layout direction within the layout's bounds rather than
its measured size, which differ when a parent clamps it. And count weighted
children's margins when growing to fit them.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Nothing used it, and it didn't work: it never set its own size, centered vertically
using the width, and didn't lay out its children.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Centering gravity centered in the whole cell, ignoring the margins, so a view with
uneven margins ended up off center.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The child was measured inside its margins, then had them subtracted from its bounds
again, and the leading margin along the scroll direction was never applied, so the
end of the content couldn't be scrolled to. Also include the margins when wrapping
the content, and page horizontally by the width rather than the x position.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MeasureBySpec compared the spec against the view's previous measurement instead of
its fixed size, so a fixed-size view larger than the space offered wasn't clamped
unless an earlier measure happened to be larger.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LinearLayout had a public padding member that only DisplayLayoutScreen set. Make it
private with a setter, and fix the measuring: the vertical retry used the horizontal
padding, the AT_MOST retries and the weighted children ignored it, and growing to fit
weighted children counted it twice.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PathBrowser lists on one thread, so going up from the network folder
waited for its listing to finish (up to 8 seconds) before the drive list
showed. Pass GetFilesInDir a cancel flag that PathBrowser sets whenever
the path changes, or on destruction, and check it while waiting.
Also don't probe the drives in the drive list for game folders.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The drive list gets a Network entry, the path //, which lists the
servers in the shell's network folder (the old WNet enumeration finds
nothing on a modern network). Going up from \\server now lands there
instead of in the drive list.
Network listings run on a thread of their own with a deadline, since
they can block for a long time and can't be cancelled: the network
folder keeps its enumerator open for half a minute after it has found
everything, and NetShareEnum on a server that doesn't answer hangs too.
We take what has turned up after a quiet second, or after 8 seconds at
the most.
GameBrowser skips probing the servers for game folders, which would
contact each one from the UI thread.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A RoundCorners mask picks the corners to round. A square corner is drawn
from the corner image's edge profiles, split along the diagonal, so its
antialiasing matches the straight edges and it needs no extra draw call.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
It stays for a second after the last activity, then fades out over half
a second. Any scrolling, touch, bob drag, or the mouse over the bob's
strip brings it back within a tenth of a second. It also shows briefly
when the view appears, so it's clear that it scrolls.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
UIAtlas generates an antialiased white corner for each radius in
ROUND_RECT_RADII (8, 16, 24, 32 UI units) and each style (filled, and
thin/medium/thick outlines from ROUND_RECT_THICKNESS) at the atlas DPI.
DrawBuffer::DrawRoundRect mirrors the corner into all four corners and
stretches the corner's last texel row/column along the edges, so a
round rect is 9 quads (8 for outlines).
Other radii use the nearest corner, scaled. The LogoScreen temporarily
draws some for testing.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Navigating up from a share like \\QUAD\PSP ISO lands on \\QUAD, which
FindFirstFile can't list, so it showed as an empty folder. Enumerate the
server's disk shares with NetShareEnum instead, skipping admin shares.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CommonWindows.h defines WIN32_LEAN_AND_MEAN, which leaves out what the
Direct2D/DirectWrite headers in draw_text_uwp.h need. Include
<windows.h> with just NOMINMAX instead, and keep std::max out of
DropShadow.h so it doesn't depend on which order windows.h comes in.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Text and the SVG button images are mixed on screen, so they now share
UIDropShadowParams (3 UI units of blur, 0.66 intensity, no offset, the
atlas's existing look) instead of the text having its own.
draw_text.cpp included <windows.h> directly, whose min/max macros broke
the std::max calls added here. Include CommonWindows.h instead.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CompositeDropShadow in Common/Render/DropShadow.cpp blurs a premultiplied
RGBA image's alpha (with an optional offset) and composites the image
over it. The atlas's AddDropShadow and TextDrawer::AddDropShadow are now
thin wrappers that pick the parameters.
The blur scatters only the non-transparent texels, which makes atlas
shadows 3-7x faster than the old gather-with-clamping version. Atlas
output is unchanged apart from rounding (alpha differs by at most 1).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The backend rasterizes coverage (or color, for emoji) as usual, then a
shared CPU pass pads the bitmap, blurs an offset copy of the coverage
and composites the text over it as RGBA8888. Blur and offset scale with
the font size and with DPI. The flag is part of the string cache key,
and the text is still aligned by its own size, with the quad grown to
fit the shadow.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
nanosvg walks every scanline of the target for each visible shape, so
rasterizing the whole SVG and cropping the images out of it cost
shapes * canvas area. Each image is now rasterized into a buffer the size
of its bounding box, from a list of copies of its own shapes, so the
images can be rasterized on the thread pool.
Atlas generation on a Redmi 9A (32-bit) goes from about 165 ms to about
95 ms, and on a desktop from about 10 ms to 6 ms. Parsing the SVG is now
the largest part. The atlas layout is unchanged; a few edge pixels differ
by at most 2/255 from the different subpixel offsets.
Also copy whole rows in Image::copyfrom, and fix a leak of the SVG file
data when parsing fails.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Key ups go to every screen, so keys don't get stuck, and that included
events that are a press and a release at once, like iOS's back swipe. So
with settings open from the pause menu, a back swipe made the pause
screen try to close too. It isn't on top, so finishDialog refused, but
the screen had already stopped taking input, and once settings closed it
was left on top, ignoring everything.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Metal: when a layer has no drawable to give (it can take a second to
say so), don't ask again in the same frame for every pass to it.
- Swapchains other than the primary are created, resized and destroyed
between frames (Metal asserts it), and destroyed with the last
reference to them (ReleaseAssertLast, Metal and Vulkan).
- BindFramebufferSamplesAsTexture asserts it isn't given a swapchain,
like BindFramebufferAsTexture.
- An offscreen Metal swapchain can't be resized, asserted.
- VulkanRenderManager asserts that no additional swapchain outlives it.
- Native_RemoveOutputAndWait asserts it isn't called on the thread that
handles the requests, where it would always time out.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Only out of date and a lost surface recreated one, so a TV whose mode
changed without a resize (MoltenVK reports SUBOPTIMAL when the layer's
size no longer matches) kept a stretched picture. Like the main
swapchain, it's now recreated after a few SUBOPTIMAL presents in a row.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Graphics > "External display" on iOS and Android. "Show the game" (the
default) is the TV mode, "Mirror the screen" leaves the TV to the system.
A change applies right away. It's disabled on backends that can't draw to
a second swapchain.
On Android 12 and up, a second screen that's part of the device (like a
dual-screen handheld's) isn't used, so the game doesn't move there by
itself.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A presentation display (HDMI or USB-C video out, some wireless displays,
or the simulated ones in Developer options) gets a Presentation with a
SurfaceView, whose surface becomes a Game output, when the backend is
Vulkan. Otherwise Android keeps mirroring.
The surface mustn't be touched after surfaceDestroyed returns, so the
output is removed with the new Native_RemoveOutputAndWait(), which
returns once the render thread has let go of it (or right away if the
graphics are down). The UI drawn on an output now leaves out the main
swapchain's pre-rotation, as the game already did.
"In menus on %1" gets the device name from Settings.Global.DEVICE_NAME.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
g_display lags behind surfaceChanged until the render thread applies the
resize, and a recreate after a suboptimal present can run in between.
The check then swapped a correct extent for a frame. Android now sets the
draw size callback to the latest size from backbufferResize, and the
check uses that.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A swapchain pass closes its step label and opens one in presentCmd, but
only if it gets an image. With the main window minimized it doesn't, and
the end of the step closed a label that wasn't open, in presentCmd if an
extra swapchain's pass had moved recording there.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
e32e558aab dropped it, assuming the stale extent came from our own resize
race. A Redmi 9A (PowerVR, Android 11) shows it doesn't: after a rotation,
vkGetPhysicalDeviceSurfaceCapabilitiesKHR returns the new transform with the
old extent, even when queried after surfaceChanged and ApplyResize have
given us the new size, and the wrong swapchain then stays until the next
rotation.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Swapchains other than the main one are VKRSwapchains, owned by the
render manager, each with its own surface, image views, framebuffers
and depth buffer, in the main swapchain's format so the backbuffer render
pass and pipelines work for all of them. A render step with no
framebuffer can name one.
They don't pace the frame: acquiring doesn't wait, and the pass is
skipped if there's no image. The frame's last submit waits on and
signals the semaphores of every swapchain acquired that frame, and each
is presented after the main one. When one goes out of date, it's
recreated at the start of the next frame.
The main swapchain's path is unchanged; frametests on Vulkan match.
VulkanContext::CreateSurface is the old per-platform surface code, now
also used for the extra ones.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
We queried VkPhysicalDevicePortabilitySubsetFeaturesKHR but never enabled
it at device creation, so to the validation layers we were using image
view swizzles (and sampler LOD bias) without having them. Enable whatever
the device offers.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Through the Khronos loader (like the Vulkan SDK's, needed for the
validation layers), MoltenVK is only listed if the instance enables
VK_KHR_portability_enumeration. Without it, instance creation failed with
VK_ERROR_INCOMPATIBLE_DRIVER and we fell back to OpenGL. Loading MoltenVK
directly, as the app normally does, works as before.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
UIContext::BeginFrame sets the scissor, which on Vulkan needs a bound
render target, and sets it to the primary's size. In portrait, that cut
the TV's menu message off past the phone's width.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>