Skip to content

feat: a mod or plugin can give the game's own screens a look of its own (interface themes) - #3278

Open
Luc-nbr wants to merge 15 commits into
openOMSI-org:mainfrom
Luc-nbr:feat/plugin-ui-theme
Open

Luc-nbr wants to merge 15 commits into
openOMSI-org:mainfrom
Luc-nbr:feat/plugin-ui-theme

Conversation

@Luc-nbr

@Luc-nbr Luc-nbr commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

What changes for the player?

Options › Interface has a new row, "Theme pack". A mod or a plugin can bring a theme that gives the game's own screens a look of their own:

  • the colours of the pause menu, its settings windows and lists, the photo mode, the developer tools and the plugins' panels and notifications;
  • the rail and the panels as frosted glass over the game's picture. Buttons and action cards stay solid, so they still read as controls;
  • the logo at the top of the pause menu: an icon with a name, or a wordmark across the rail;
  • the loading screen's colours and logo. The logo can itself be the loading bar: a ring or a line in it is drawn along as the map loads, and parts of it pop up (or flash) as the loading reaches them.

The choice is kept for the next start. openOMSI's own look is the first entry and the default; without a theme, nothing changes (see the before/after screenshots).

How to test

Make a folder Themes/Night/ in the game folder or the content folder with this theme.json:

{
  "name": "Night",
  "colors": { "accent": "#2a75f7", "background": "#0a142e", "surface": "#141a26", "ink": "#e8ebf2" },
  "radius": 12,
  "glass": true,
  "loading": { "logo": "logo.svg", "progress": "stroke" },
  "logo": "logo.svg"
}

and this logo.svg beside it:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 200">
  <path id="progress" d="M100 20 A80 80 0 1 0 180 100 L380 100" fill="none" stroke="#2a75f7" stroke-width="16" stroke-linecap="round"/>
  <rect id="box" data-at="0.3" x="70" y="70" width="60" height="60" rx="8" fill="#eef1f5"/>
  <circle id="lamp" data-at="0.3" data-delay="0.4" data-blink="" cx="100" cy="100" r="14" fill="#ffd54a"/>
  <circle id="stop" data-at="0.8" cx="330" cy="100" r="22" fill="#ffffff"/>
</svg>
  1. Start the game (any map and bus), press Esc and go to Options › Interface › Theme pack. The list has "openOMSI" and "Night". Choose "Night".
  2. Press Esc. The rail is a rounded pane of frosted glass over the picture, inset from the window's edges, with the logo across its top (it is twice as wide as high, so it is drawn as a wordmark) and "openOMSI " under it. Resume is blue; End the session and the driver's chip are solid. With no category chosen, the picture beside the rail is only a little dimmed.
  3. Choose a category: the picture behind the page is blurred under the navy background, and the action cards are solid navy with a thin light edge. Open Options: the settings panels are glass, and their switches, sliders and drop-downs are solid.
  4. Leave the session and start another map. The loading screen is navy, and the logo's ring and line are drawn along as the map loads, over a faint track of the whole path. At 30 % the box pops up, and the lamp flashes twice a moment later. At 80 % the stop pops up. The percentage and the tips are still at the bottom, but there is no striped bar.
  5. Host a LAN game, which does not pause, and open the menu: the glass shows the moving picture.
  6. Choose "openOMSI" again and restart: the game looks as before.
  7. Break theme.json (e.g. "radius": 80) and start the game: game.log says interface theme "Night": radius: 0 to 24 is expected - openOMSI's own look, and the game looks as before.

For a plugin: a theme.json in plugins/<name>/ makes a theme of that name, and omsi.ui.set_theme("<name>") switches to it (omsi.ui.themes() lists them, omsi.ui.theme() says which one is in force).

Tried in the game on Windows 11 with an RTX 4070 SUPER (DX12): the release build of this branch and of main (8119ca6), Berlin-Spandau with the MAN SD200 and 30 AI vehicles, 1600 × 900. Each run had a fresh HOME and OMSI_CONTENT. The Omsi-Hub theme was the test theme (glass, the logo as the loading bar, a wordmark), and the screens were driven by OMSI_INPUT (key Escape, key Right, menu options). Switching and restarting, checked in three runs:

  • a plugin's ui.set_theme("omsihub") saves ui_theme=omsihub;
  • after a restart the theme is in force before the loading screen, and switching back with ui.set_theme("") saves ui_theme=openomsi;
  • after the next restart the game is in openOMSI's own look (last screenshot).

(These runs were made before the key was renamed for #3309: the setting was then ui_theme, now ui_theme_pack; nothing else in that path changed.)

Doing that found and fixed three things: the window, made before the renderer, could not be copied from, so the glass was not blurred; the glass cost too much when it was taken every frame (see Performance); and a loading logo whose view box does not start at 0 popped its marks in a little off their place.

Automated:

  • cargo test -p omsi-ui: palette fallbacks, hex colours, per-thread palettes; a path's length, a logo drawn along it, its marks popping up and flashing, also with a moved view box.
  • cargo test -p omsi-plugin and cargo test -p omsi-plugin-ui: a plugin asks for a theme the game has, and the API manifest.
  • cargo test -p omsi-app: a theme file read whole, a wrong one saying why and nothing read outside its folder, themes found in Themes and plugins with the first id winning, the setting's values, the translated row.
  • cargo test -p omsi-render and cargo test -p omsi-launcher-core settings.

Screenshots

In the game, Berlin-Spandau, 1600 × 900. Left main, middle this branch with openOMSI's own look (the same as main), right this branch with the Omsi-Hub theme.

main this branch, openOMSI's look this branch, Omsi-Hub theme
Loading
Pause menu
A category
Options

After switching back to openOMSI's look and restarting:

What does this change, and why?

Mods have always changed how OMSI looks, but the game's own screens could not be changed: their colours are constants in the code. With interface themes, a mod or a plugin can give the menus and the loading screen a look that fits it, without touching the code, and the player chooses it. Omsi-Hub, an add-on that runs on openOMSI through the plugin API, is one user: its plugin brings a theme in its own colours, with its logo as the loading bar. Nothing in the change is specific to it. The full format is in docs/THEMES.md.

The loading screen comes before any plugin runs, so a theme is a file that the game reads itself (Themes/<id>/theme.json, or theme.json in a plugin's folder), not a call a plugin makes.

API (omsi.ui.version is now 7; #3277 took 6):

omsi.ui.themes()           --> { { id = "", name = "openOMSI" }, { id = "night", name = "Night" }, ... }
omsi.ui.theme()            --> "" (openOMSI's own look) or the id in force
omsi.ui.set_theme("night") --> true, or false and the reason; kept as the player's setting
  • omsi-ui:
    • The constants of theme became a Palette in force (theme::th(), theme::set), with Palette::OPENOMSI as before and with_palette for the tests. A theme's colours left out follow the one they go with (sidebar the background, text the ink and so on).
    • theme::corner(r) is r with openOMSI's look and follows a theme's radius. The reworked pause menu's corners (12, 14, 18 ...) go through it.
    • Glass describes the glass. kit::Ui::surface draws a surface solid, or as glass: the backdrop (Ui::backdrop) behind it through image_part, the panel's colour at the glass's opacity, a sheen and a light edge. panel uses it.
    • icons has path_length, progress_svg (a path drawn along by a dash, over a faint track), marks, staged_svg and Stage (elements with data-at popping up or flashing), and svg_size.
  • omsi-render: SurfaceState::picture gives the texture the frame is drawn into, where the window allows COPY_SRC; new_with and adopt ask for it when the adapter offers it. The sRGB stand-in allows it too.
  • omsi-app:
    • themes finds, reads and validates themes and keeps the one in force (sync, called every frame, which only reads anew when the setting changes). Settings::ui_theme_pack is the setting, and Options › Interface has its row "Theme pack" (in the 29 languages of that page).
    • The loading screen's drawing is ui/loading.rs, out of ui.rs.
    • The glass: Shell::capture_backdrop copies the frame the scene has just drawn and blurs it on the graphics card in three small passes (a quarter of the window, then a 16th, then an 8th). It is done when the menu opens, then at most 30 times a second while the game goes on behind it (glass_due), and not again while the game is paused. Nothing is read back and nothing waits for the GPU.
    • The pause screens draw the rail and the panels with surface; the buttons, the driver's chip and the action cards stay solid. A wide theme logo goes across the top of the rail.
    • The loading screen takes the theme's colours, logo and progress, and times the logo's marks by its clock. The headset's menu (ui/menu.rs) takes the theme's accent and panel colours, but no glass.
  • omsi-plugin: UiState keeps the themes and a plugin's wish (ui/theme.rs), with ui.themes, ui.theme and ui.set_theme. omsi-app hands the wish to the settings after the plugins' frame. The navigator's board (Dock, set_dock, dock) moved to ui/dock.rs, beside ui/menu.rs, to keep ui.rs under its size.
  • omsi-plugin-ui: the panels' accent and the notifications' card follow the theme; the panels' colours are look.rs.
  • omsi-launcher-core: ui_theme_pack is read and written with the settings file.

With #3309 (light and dark, open): #3309 makes ui_theme the light or dark interface theme, with the row "Interface theme". This PR's setting is ui_theme_pack and its row "Theme pack", so the two keys and rows no longer clash, whichever is merged first. A theme pack is a look a mod or plugin brings; the light or dark theme is openOMSI's own look. Whichever PR lands second will need to fit its palette into the other's (both replace the same colour constants): a theme pack replaces openOMSI's colours, light or dark.

  • docs/THEMES.md is new. docs/PLUGINS.md has a section "An interface theme", and its reference and plugin-api.json are regenerated.

What could break?

With openOMSI's own look, every colour is the constant it was, and the screenshots of main and this branch are the same. The screens now read the colours through th() instead of constants: a lock and a copy a call.

A theme is data only: nothing outside its folder is read, a file is at most 4 MB, a picture at most 4096 px, and a content folder another player's game sent in a LAN session brings no theme. A broken theme leaves openOMSI's look with a line in game.log.

The glass needs the window's picture to allow a copy. Where it does not, the panels are see-through without the blur. I tried DX12 only; Vulkan and Metal report COPY_SRC for their windows too, but I have not run them. I could not try the triple screens, the cockpit display or a phone. In VR, the menu keeps its own drawing with the theme's colours and no glass. Plugin panels already drawn keep their old colours until their content changes, after a theme is switched while driving.

Unrelated to this change, but found while testing: a local release build of main crashes at start-up with the renderer pipelines' threads overflowing their stack when dxcompiler.dll is not next to the program (wgpu then compiles with FXC). The released packages are not affected; with DXC beside the program, both builds run.

Performance

The same scene for every run: Berlin-Spandau, the MAN SD200 in the driver's view, 30 AI vehicles, a LAN session hosted (so the menu does not pause the game), the pause menu opened at 40 s and left open until the end at 180 s. Vsync off, no frame limit, 1600 × 900, the summary of OMSI_PROFILE_JSON (after its 15 s warm-up), and the process's private memory read at 2 min.

This branch, Omsi-Hub glass theme This branch, openOMSI's look main (8119ca6)
Average FPS 140.4 145.1 (139.6 in an earlier run) 136.1
Median frame (p50, ms) 6.70 6.46 6.72
Slowest 1 % of frames (p99, ms) 13.07 12.51 16.22
Slowest frame (ms) 95.5 88.2 103.5
Frames over twice the median 168 167 447
Memory after 2 min (MB, private) 2884 2940 2868

Machine: Windows 11, NVIDIA GeForce RTX 4070 SUPER (DX12), 1600 × 900, the default settings of a fresh home (Vanilla+, 4× MSAA, SSAO, 2048 shadows). One run each; the frame times move a few per cent from run to run on this machine (openOMSI's own look: 139.6 and 145.1 FPS in two runs).

With the glass taken every frame, it cost 1.1 ms a frame (121 FPS, p50 7.76 ms). Taken at most 30 times a second, it costs about 0.24 ms (p50), and its slowest 1 % and its slow frames are as with openOMSI's own look: no stall from the glass.

Checklist

  • cargo test --workspace and cargo build --release pass: omsi-ui (46), omsi-render, omsi-plugin, omsi-plugin-ui, omsi-app (398) and the launcher's settings tests pass locally, cargo build --release -p omsi-app builds, and scripts/size-budget.sh and scripts/arch-guard.sh pass. The full workspace is left to CI.
  • The pull request contains a single change
  • Before/after screenshots are in the description if anything changes on screen
  • A new feature has its performance table filled in
  • New or changed interface texts are in crates/omsi-app/locales/app.yml ("Interface theme" and its description, in the 29 languages of the Interface page)
  • Online play, saves, settings and mods made for the previous release still work
  • No original OMSI 2 content and no file of a megabyte or more in the commits

A mod or a plugin can now give the game's own screens a look of their own, chosen in
Options › Interface › Interface theme (docs/THEMES.md). A theme is a folder with a theme.json
(Themes/<id>/ of a content folder, or a plugin's own folder):

- the palette: omsi_ui::theme's constants are now a Palette in force (theme::th(), set by
  themes::sync from the setting ui_theme), read by the widget kit, the pause menu, its settings
  windows and lists, the photo mode, the developer tools, the permission dialog, the headset's
  menu and the plugins' panels and notifications; colours a theme leaves out follow the ones
  they go with;
- glass: Ui::surface draws a panel as frosted glass - the game's picture behind the pause menu,
  rendered small when the menu opens (and every 1.5 s while a LAN game goes on), blurred on the
  CPU, under the panel's colour, with a light edge and a sheen; the rail becomes a floating pane;
- the loading screen: the theme's colours and logo, which can be the loading bar itself - its
  path id="progress" drawn along (icons::path_length, icons::progress_svg) or the logo filled
  from the left as far as the loading is;
- the pause menu's logo and name.

openOMSI's own look is unchanged and stays the default. Plugins see the themes with ui.themes and
ui.theme and can switch with ui.set_theme (omsi.ui.version 6; the API docs regenerated).
…eaches them

An element of an SVG loading logo with an id and data-at (0 to 1 along the progress) stays
hidden until the loading is that far, then pops up from its middle with a little overshoot;
with data-blink it flashes twice (a light), data-delay waits a moment after (icons::marks,
icons::staged_svg, Stage::of). The loading screen times each mark by its own clock and draws a
new picture of the logo only when a mark's stage changes (in twentieths of its size).
@Luc-nbr Luc-nbr changed the title feat(ui): interface themes for the game's own screens feat: a mod or plugin can give the game's own screens a look of its own (interface themes) Oct 25, 2026
"Interface theme" and its description in app.yml, in the 29 languages the Interface page's
other rows have, with a test that the row is drawn in the player's language.
@Luc-nbr
Luc-nbr marked this pull request as ready for review October 25, 2026 10:20
@turbo-devv

Copy link
Copy Markdown
Collaborator

Thanks @Luc-nbr. This now conflicts with main: the pause menu was reworked (rounder corners, pause_hidden, the veil on category pages) and #3277 took omsi.ui.version 6, so the theme work should move to 7 (code, docs/PLUGINS.md, the changelog). Could you merge main and re-check the themed pause pages against the new look? Once it merges cleanly the checks run again and we can look at it.

@isaacsa2 isaacsa2 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.

The theme format and the default-look fallback are a useful start. I can't approve 4782a89 yet.

I agree with turbo-devv's merge-main/API-version follow-up. Beyond that, the description still says the feature has not been tried in game, screenshots are pending and performance is unmeasured, although the PR is ready for review. Please add before/after captures of the default look and a themed pause/settings/loading screen, and fill the performance comparison on the same map and machine.

Please include an unpaused LAN session with the glass menu open: frame_glass_backdrop() calls render_to_image, reads the picture back and blurs it on the CPU every 1.5 seconds. Average FPS alone won't show the stalls that path may add; include frame-time spikes/p99 and memory, alongside the default theme. Also check switching back to openOMSI and restarting with the saved selection. I haven't measured a stall myself; this is the unvalidated execution path in the diff. Fresh platform CI is still needed after the conflicts are resolved.

… the glass on the GPU

- The reworked pause menu (rounder corners, pause_hidden, the veil on category pages) takes the
  theme: its corners through theme::corner(r), which is r with openOMSI's look and follows a
  theme's radius; with no category chosen the glass theme dims the picture only a little behind
  the rail, a category page, a list or a settings window has the picture blurred under the
  background.
- openOMSI-org#3277 took omsi.ui.version 6: the themes are 7 (code, docs/PLUGINS.md, the changelog), the API
  docs regenerated.
- The glass's picture is no longer rendered a second time and read back: the frame the scene has
  just drawn is copied on the graphics card (SurfaceState::picture: the window's texture, or the
  sRGB stand-in, where it allows COPY_SRC) and blurred there in three small passes
  (Shell::capture_backdrop), every frame the menu is open. Nothing waits for the GPU and nothing
  runs on the CPU per pixel; the CPU blur and frame_glass_backdrop are gone.
…heme's wordmark on the rail

Tried in the game (Berlin-Spandau, DX12):
- The window is made before the renderer (SurfaceState::early), so its picture could not be
  copied from and the glass showed the picture unblurred: adopt() now asks for COPY_SRC once
  the renderer's adapter says the window allows it.
- The glass's picture is taken when the menu opens, then at most 30 times a second while the
  game goes on behind it (Shell::glass_due), and not again while it is paused: with it taken every
  frame the glass cost 1.1 ms a frame in a LAN session, now 0.24 ms (p50; p99 and the slow
  frames as with openOMSI's own look).
- What is clicked stays solid on the glass - the session's buttons (End the session with a
  hairline of the red), the driver's chip, the action cards on the home and category pages -
  so that it reads as a control; the glass is the rail's and the panels'.
- A theme's logo that is a wordmark (1.6 times as wide as high or more) is drawn across the top
  of the rail in place of the icon and the name, openOMSI's version under it.
…view box does not start at 0

usvg puts the view box on a group under the root, so the box of a mark is the canvas's: its
middle is taken back through the element's parent's transform (its own undone), and a stop of
the Omsi-Hub logo popped in a little above its line. Tested with a moved view box: the middle in
the SVG's units, and a mark half grown has its ink centred where the whole one has.
# Conflicts:
#	crates/omsi-app/src/settings.rs
@Luc-nbr

Luc-nbr commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Thanks both, it's updated.

@turbo-devv: main is merged, the themes are omsi.ui.version 7 now (code, docs/PLUGINS.md, the changelog), and the themed pause pages follow the reworked look. The new rounder corners go through theme::corner(r), which keeps them exactly as on main for openOMSI's own look. pause_hidden and the veils work as on main, with a glass theme blurring the picture behind a category page instead of covering it.

@isaacsa2: I've now run it in the game, and the description has before/after screenshots (main, this branch with openOMSI's look, this branch with a glass theme: loading, pause menu, a category page, options) and the performance table. The table comes from an unpaused LAN session with the glass menu open, against openOMSI's own look and main, on the same map and machine, with p50/p99, slow frames and memory. The path you pointed at is gone: the glass no longer calls render_to_image or reads anything back. It copies the frame the scene has just drawn and blurs it on the GPU, when the menu opens, then at most 30 times a second while the game runs behind it, and once while it is paused. That costs about 0.24 ms at the median, and the p99, the slow frames and the memory are the same as with the default look. Switching back to openOMSI and restarting with the saved selection is checked too (three runs, last screenshot).

Running it in the game also turned up three bugs, now fixed: the window could not be copied from (no blur), the glass every frame cost 1.1 ms, and a loading logo with a moved view box popped its marks in slightly off their place.

It merges cleanly now, so CI can run again.

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Test builds for d2d51be

Platform Download
Android (arm64) android.zip
Linux x64 (game and dedicated server) linux-x64.zip
macOS (Apple silicon) macos-arm64.zip
Windows x64 (game and dedicated server) windows-x64.zip
  • Unpack the download into a new folder, not over your openOMSI. The launcher shows the version as ...-pr3278, and a test build is never updated. It shares your settings and OMSI 2 folder; mods you installed into your openOMSI folder are only seen when you unpack it into a copy of that folder.
  • Android: the download holds an APK that installs as a separate app, openOMSI PR feat: a mod or plugin can give the game's own screens a look of its own (interface themes) #3278, beside the release, and shares its openOMSI folder.
  • What to try is under How to test in the description. Please comment with what you found and the version the launcher shows.
  • The links work for 14 days after the build.

@isaacsa2 isaacsa2 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.

Rechecked 22b7b50. Thanks for the in-game captures, restart checks and unpaused LAN measurements. The GPU-copy/blur path replaces the readback I flagged earlier, so that evidence request is addressed.

There are still concrete blockers. PR checks / files fails on three file sizes: omsi-app/src/ui.rs is 1,852 lines against 1,810, omsi-plugin-ui/src/lib.rs is 1,510 against 1,500, and omsi-plugin/src/ui.rs is 1,507 against 1,500. Please extract cohesive helpers/modules rather than raise the limits. Log: https://github.com/openOMSI-org/openOMSI/actions/runs/40224043363/job/121261539726

The current Release also fails workspace tests: process_permissions::once_waits_without_blocking_frames_then_prompts_again_and_deny_stays_denied on Windows, and process_wait::timer_can_wait_and_be_cancelled_by_unloading on Linux. I have not established whether those are environmental; please diagnose them or provide the matching base comparison, and get the required checks green. The branch also conflicts with current main again. Coordinate the ui_theme setting with #3309 during that update, since both PRs introduce that key with different meanings. I reviewed the follow-up and CI; I have not run the themed renderer locally.

…three files under their size

- main merged again (settings.rs: nav_classic_2d beside the theme pack).
- openOMSI-org#3309 (open) makes `ui_theme` the light or dark interface theme, with the row "Interface
  theme". This PR's setting is now `ui_theme_pack`, its row "Theme pack" (in the 29 languages
  of the Interface page): a theme pack is a look a mod or plugin brings, the light or dark theme
  is openOMSI's own; the two keys no longer clash, whichever is merged first.
- Under the size budget by moving cohesive parts into modules, not by raising limits:
  - omsi-app: the loading screen's drawing (the wordmark, the striped bar, an interface theme's
    logo and marks) is `ui/loading.rs` (ui.rs 1852 -> 1671 lines, its budget 1810);
  - omsi-plugin-ui: the panels' colours (the card, the accent, the menus' greys) are `look.rs`
    (lib.rs 1510 -> 1486);
  - omsi-plugin: the navigator's board (`Dock`, `set_dock`, `dock`) is `ui/dock.rs`, as the pause
    menu's lines (`ui/menu.rs`) and the themes (`ui/theme.rs`) are (ui.rs 1507 -> 1482).
  scripts/size-budget.sh and scripts/arch-guard.sh pass.
Luc-nbr added a commit to Omsi-Hub/Omsi-Hub-Addon that referenced this pull request Oct 26, 2026
…erface › Theme pack (openOMSI-org/openOMSI#3278 keeps its key apart from the light and dark interface theme of #3309)
@Luc-nbr

Luc-nbr commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @isaacsa2, all three are addressed in 93914b6.

File sizes: I moved cohesive parts out instead of raising limits:

  • the loading screen's drawing is omsi-app/src/ui/loading.rs (ui.rs 1852 → 1671);
  • the panels' colours are omsi-plugin-ui/src/look.rs (lib.rs 1510 → 1486);
  • the navigator's board (Dock, set_dock, dock) is omsi-plugin/src/ui/dock.rs, next to ui/menu.rs and ui/theme.rs (ui.rs 1507 → 1482).

scripts/size-budget.sh and scripts/arch-guard.sh pass locally.

#3309: this PR's key is now ui_theme_pack and its row is "Theme pack", so #3309 keeps ui_theme and "Interface theme" for light/dark. A theme pack is a look a mod or plugin brings; light/dark is openOMSI's own look. Both PRs replace the same colour constants with a runtime palette, so whichever lands second has to fit into the other. I'm happy to do that on this side if #3309 goes first.

The two test failures: neither test touches anything this PR changes. Both pass 5/5 locally (Windows) on 93914b6, and the same workflow is green on main and the other PRs since then.

  • process_wait::timer_can_wait_and_be_cancelled_by_unloading (Linux): ParseIntError { kind: Empty } at line 201. The child writes its pid with std::fs::write, which creates the file before writing the number. The test reads it as soon as pid_file.exists(), so it can read an empty file. That race is in the test; writing to a temp file and renaming it, or waiting for content, would close it. I can open a separate PR for that.
  • process_permissions::once_waits_without_blocking_frames_then_prompts_again_and_deny_stays_denied (Windows): it timed out after 45 s with messages=[], so the child process never reported back while the game kept running (17547 frames). That looks like the slow child start-up on Windows runners that test(plugin): allow slow Windows child launch in consent tests #3275 already allowed for.

CI is running again on the new commit.

Signed-off-by: Isaac S. <107325320+isaacsa2@users.noreply.github.com>
@isaacsa2 isaacsa2 mentioned this pull request Oct 26, 2026
5 of 7 tasks
Adapt square.rs from main to the theme branch’s runtime accent function. This fixes E0432 after merging main without changing the tile layout or behaviour.

Signed-off-by: Isaac S. <107325320+isaacsa2@users.noreply.github.com>

@isaacsa2 isaacsa2 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.

Rechecked the follow-up and updated this branch with main. The settings conflict keeps both ui_theme_pack and cab_wheel_rotation=0. The fresh build exposed E0432 in omsi-plugin-ui/src/square.rs: main's new quick-menu tiles still imported ACCENT, while this branch uses accent(). I fixed those two references in 99f12f6; the tile layout and behaviour are unchanged. The module extraction and separate ui_theme_pack key address my earlier size/key objections. Theme pack is selectable in Options > Interface and players can return to openOMSI's own look, so its optional glass cost is not an FPS blocker under the current requirement. Approval is still pending the platform/workspace checks on 99f12f6. The supplied DX12 captures and measurements do not establish Vulkan/Metal or VR behaviour, and I have not run the game locally.

isaacsa2
isaacsa2 previously approved these changes Oct 27, 2026

@isaacsa2 isaacsa2 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.

Rechecked 99f12f6. The current Release, PR checks, CodeQL and secret scan now pass; size budget, crate layers and whitespace checks also pass locally. The module extractions, separate ui_theme_pack setting and quick-menu accent fix resolve my earlier build/integration objections.

The expensive CPU readback path is gone. Glass uses the existing frame on the GPU, is refreshed at a bounded rate and is optional through Options > Interface > Theme pack, with openOMSI's own look available again. The supplied DX12 captures and measurements address the validation I requested. Approving this head.

#3309 touches the same palette paths, so whichever lands second needs to merge the updated main and rerun its tests; keeping both setting names alone is not enough. I reviewed the code and CI, not a new local gameplay run. Vulkan, Metal and VR behavior is not established by the DX12 measurements.

@thibautgnt thibautgnt 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.

Thanks a lot @Luc-nbr, this is a carefully built feature: the theme file is validated field by field with clear log lines, openOMSI's own look stays the default and pixel-identical, and the glass moved to a bounded-rate GPU copy with real p99 numbers behind it. Two things before it can go in:

  1. An SVG logo can still read files outside the theme's folder (crates/omsi-ui/src/icons.rs:30, :46, :55, :182). read_logo checks the logo's own path (crates/omsi-app/src/themes.rs:232), but the SVG is then parsed with usvg::Options::default(). With no resources_dir, usvg's default image resolver opens any <image href="..."> as a plain path, absolute or relative to the working directory, and reads it whole with std::fs::read (no 4 MB cap). So a theme with <image href="C:/Users/me/Pictures/x.png"/> or href="/some/other/file.svg" reads files outside its folder, and on Linux href="/dev/zero" makes the game read forever and run out of memory every time it starts, because the setting is saved. The file is read again on every parse: four parses at load, then one for each new picture of the loading logo. Fix: one shared Options for theme logos with image_href_resolver.resolve_string returning None (keep resolve_data if you want data: images, which stay inside the 4 MB file), used by rasterize_rgba, svg_size, path_length and marks. Please add a test with an <image href> pointing outside the folder.

  2. Conflict with #3309 (unify game theme), merged on main just now. git merge-tree against current main conflicts in 14 files: omsi-ui/src/theme.rs, omsi-ui/src/kit.rs, omsi-plugin-ui/src/{lib,nav,square,widgets}.rs, omsi-app/src/shell/pause.rs, shell/pause/navigation_ui.rs, shell/pause/quick_actions_ui.rs, devtools/panel.rs, photo/panel.rs, game_lists/dropdowns.rs, settings.rs and omsi-launcher-core/src/lib.rs. It is not only text: the two PRs give the same colours two different mechanisms. On main, Ui carries theme: Theme (the UPPERCASE fields) picked by set_theme(light), and light: bool is passed down to the plugin panels (ThemeColors::new(light)), the quick menu and the shell. Here, everything reads a global th() Palette built on the dark palette. If the merge just keeps both, a theme pack shows its colours only where th() is read, and the light theme only where self.theme is read. Please settle on one source of colours, and decide what a pack does on top of Light: the simplest is with_colors starting from palette(light) instead of Palette::OPENOMSI, with radius and glass on top. Then rerun the omsi-ui, omsi-plugin-ui and omsi-app tests and the size budget on the merged result.

Non-blocking notes:

  • crates/omsi-render/src/surface.rs:87 and :140 add COPY_SRC to the window's swapchain for every player who can have it, with or without a glass theme. On DX12 your numbers show no cost, but on some mobile GPUs a copy-source swapchain can turn off framebuffer compression, so "without a theme nothing changes" may not hold on Android. Asking for COPY_SRC only while a glass theme is in force, and reconfiguring when that changes, would rule this out.
  • themes.rs:249: a picture logo is fully decoded before the 4096 px check (decode_file allows up to 16384 px, so roughly 1 GB from a small, very compressible PNG). Mod textures already go through the same path, so this is no new hole, but reading the header size first would make the "at most 4096 px" in the description true.
  • The loading screen re-parses and rasterizes the logo for each new progress hundredth and mark stage (ui/loading.rs:116). That's fine for a normal logo. A 4 MB SVG will make the loading screen stutter, but that only hurts the theme's own author.

The game told the plugins its themes only from their first frame, after it had loaded them:
`ui.themes()` was empty and `ui.set_theme` refused every theme while a plugin's main chunk
ran, which is where docs/PLUGINS.md offers a plugin's theme (Omsi-Hub's plugin logged "no
theme \"omsihub\"" and never offered it again). The game now tells the themes and the one in
use before it loads the plugins (`omsi_plugin::ui::themes_for_load`, read by a new
`UiState`), and each frame as before; a test for it.
usvg's own options read any file an `<image href>` names, a path absolute or relative to the
working directory, and whole, so a theme's logo could read files outside its folder (and on
Linux `/dev/zero` forever, at every start, the setting being saved). Every SVG is now read
with one set of options (`icons::tree`) whose `<image>` is only a `data:` URL, which stays
inside the 4 MB file: `rasterize`, `rasterize_rgba`, `svg_size`, `path_length` and `marks`.
A test with an `<image href>` to a file outside it, which usvg's own options do read.
…s over Light or Dark

Main put the light look in a `Theme` of its own, read from `Ui::theme` (`set_theme(light)`)
and passed to the plugin panels as `ThemeColors::new(light)`; this branch reads the palette
in force, `omsi_ui::theme::th()`. Kept both, a pack's colours showed only where `th()` is
read and Light only where `self.theme` is. Now there is one source of colours:

- `omsi_ui::theme::Palette::LIGHT` is main's light look and `theme::palette(light)` gives
  openOMSI's dark or light palette; `Palette::with_colors` lays a theme's colours over the one
  given, and the theme's radius and glass go on top (`themes::Theme::palette(light)`).
- `themes::sync(pack, light)` puts that in force (the theme read anew only when it is
  another); the pause menu, developer tools and photo mode call it where main called
  `set_theme`. `Ui::theme`, `Ui::set_theme`, `Shell::set_theme` and `theme::Theme` are gone;
  main's lines read `th()`, with its light-look fixes kept (accent ink on buttons, the
  toggle, slider, segment and progress tracks, tooltips and popups from the palette).
- The plugin panels' `ThemeColors::of(palette)` keeps openOMSI's dark greys exactly and takes
  any other palette's surfaces and inks; a card is laid out again when the palette changes
  (`PanelsFrame::palette` for main's `light`). The game's own panels take the palette
  (`themes::palette(light)`) rather than `light`.
- The settings keep both: `ui_theme` (Light or Dark) and `ui_theme_pack`.
- docs/THEMES.md: a theme goes over the look the player chose.

Main's tests for the light look run on the palette (`with_palette`); a test for a theme over
the light look.
…n force

The swapchain asked for COPY_SRC for every player wherever the window allowed it, though only
a glass theme reads the window's picture (the blurred glass behind the game's screens). On
DX12 that costs nothing measurable, but a mobile GPU may give up framebuffer compression for
a swapchain that can be copied from. `SurfaceState::set_glass` asks for COPY_SRC only while a
glass theme is in force and reconfigures the window when that changes; the frame calls it
after the theme is synced. Without a glass theme the window is configured as before the
themes.
…is decoded

A picture logo was decoded whole before its 4096 px limit was checked, and the decoder
allows up to 16384 px: a 4 MB file of one colour could make the game decode about 1 GB.
Its size is now read from its header first (`image::ImageReader::into_dimensions`, the
format guessed from its content as OMSI's misnamed files need), and one larger than 4096 px
is refused undecoded; the decoded size is still checked after. A test with a 5000 px PNG.
@Luc-nbr

Luc-nbr commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @thibautgnt, both addressed:

  1. SVG <image href>: every SVG now goes through one icons::tree whose resolver keeps data: images and returns None for any string href (7bb6739). There's a test with an <image href> to a file outside the folder, which usvg's default options do read.
  2. feat(ui): unify game and launcher themes #3309: merged main and settled on one source, the th() palette. Palette::LIGHT is feat(ui): unify game and launcher themes #3309's light look and theme::palette(light) picks Dark or Light. A pack is with_colors over that, with radius and glass on top (themes::sync(pack, light)). Ui::theme, set_theme and theme::Theme are gone; feat(ui): unify game and launcher themes #3309's widget changes read th() and keep their light-mode fixes. Plugin panels take ThemeColors::of(palette) (openOMSI's dark greys unchanged) and are laid out again when the palette changes. omsi-ui, omsi-plugin-ui and omsi-app tests and the size budget pass on the merged result.

Non-blocking: COPY_SRC is now asked for only while a glass theme is in force (bb8747e), and a picture logo's size is read from its header before decoding (d2d51be). I left the loading screen's re-rasterizing as is.

@thibautgnt thibautgnt 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.

Thanks @Luc-nbr, that was a thorough round. The SVG fix is the right one: a single icons::tree that keeps data: images, plus a test that proves usvg's own options would have read the file. The merge with #3309 now has one source of colours, with a pack laid over palette(light). You also took care of both non-blocking notes (COPY_SRC only while glass is on, and the picture size read from the header). CI is green, and the branch merges cleanly with current main, where size budget and arch-guard pass.

The merge left one place where Light breaks:

  1. The game menu drawn by ui/menu.rs turns white on white in the Light look. own() (crates/omsi-app/src/ui/menu.rs:9) keeps openOMSI's dark colours only when th() == Palette::OPENOMSI. Before the merge, that was true for anyone without a pack. Now themes::sync puts Palette::LIGHT in force for every player with ui_theme=light, so panel() / panel_alt() (:23, :26) return the light panel (252 grey) while the text colours stay fixed light greys: WHITE (236), SOFT, MUTED, DIM (:57, :66-68), and also LIT, SELECTED, CHIP, BORDER, THUMB. The title (:244), the lines and the preview card (:581-589) end up near-white on near-white. On main this menu stays dark in Light. The same thing happens with any pack over Light that doesn't set panel itself.
    Fix: when the palette isn't openOMSI's own, take the inks and line fills from it as well, e.g. WHITE from p.text, SOFT from p.text_soft, MUTED/DIM from p.text_dim, OFF_INK from p.text_faint, LIT/SELECTED from p.hover/p.selected, CHIP from p.field and BORDER from p.edge (the same own(...) wrapper you already use for panel()). Or, if you'd rather keep this menu dark in Light as on main, compare against palette(false)/palette(true) there. A small test under with_palette(Palette::LIGHT, ...) checking that the title ink differs from the panel would keep this from coming back.

Non-blocking:

  • read_logo (crates/omsi-app/src/themes.rs:272): when image can't read the header (a format it doesn't know), the file still goes to decode_file before the size check. That's fine, since those formats aren't the compressible kind, but it is the one path left where "at most 4096 px" is checked only after decoding.

@isaacsa2 isaacsa2 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.

Rechecked d2d51be after the main merge. The shared palette integration and the SVG resolver fix address the earlier conflicts and external-file concern; I am not carrying those forward as blockers. Current CI passes.

The remaining blocker is the light palette in crates/omsi-app/src/ui/menu.rs. panel() and panel_alt() now follow th(), but WHITE, SOFT and the fixed border/selection/chip colours still assume a dark background. With Palette::LIGHT, the panel becomes nearly white while titles and preview text remain [236, 236, 236], making them difficult to read. A theme pack inheriting those light backgrounds has the same mismatch.

Please make the foreground/background pairs follow the active palette consistently, including selection states, or keep this menu consistently dark. Add a light-palette regression for the title/text and selected-row colours, alongside the existing default/theme tests. This is a source-level finding, not a new local screenshot or gameplay test.

This branch has not been deployed

No deployments
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.

4 participants