Skip to content

Implement async load for route objects - #1417

Merged
leezer3 merged 26 commits into
leezer3:masterfrom
adfriz:route-load-object-async
Sep 16, 2026
Merged

leezer3 merged 26 commits into
leezer3:masterfrom
adfriz:route-load-object-async

Conversation

@adfriz

@adfriz adfriz commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Route loading should covers both .csv and .rw . it was spent most of its time decoding object files one by one on the loader thread: every Structure.* declaration called Host.LoadObject() inline before parsing continued.

This PR collects all Structure.* declarations (Rail, Walls, Dikes, Forms, Roofs, Cracks, FreeObj, Beacon, Poles, Ground etc..) during parsing, decodes them concurrently with Parallel.For, then commits the results sequentially in file order.

Loading behaviour is unchanged in game, so there should be no object popping in game because it loaded late.

Worker count is automatic: CPU <= 4 ? CPU - 1 : min(8, CPU), reserving one thread for the loading screen on small machines.

Parsing the CSV/RW text is cheap, file I/O + object parsing + texture decode is the actual bottleneck. On high multi-core machines the object phase speeds up roughly with core count (currently capped at 8 logical core not physical core count, although disk/RAM speed may become the limit).

Thread-safety hardening

Shared state that was previously only touched single-threaded is now guarded:

  • HostInterface: add private cache lock with Store/Report/Prune/Clear helpers, the cache hits are cloned outside the lock; cache inserts upsert instead of throwing on duplicates.
  • Host (main program, ObjectViewer, RouteViewer): failure/missing-file sets updated atomically, messages emitted outside locks.
  • TextureManager: texture decode happens outside the lookup lock with double-checked registration, all allocator paths and readers (load/unload/stats) are also locked.
  • Log message lists (all three apps): writes locked, readers use snapshots.

Testing

  • Needs a cold-load run on a heavy route, the first load timing is what we want, route reload through F5 should still faster.
  • Not yet tested on Linux/macOS
  • Was tested with shinkansen route v1.00 beta EN in route viewer, file 1fine-Hayabusa1v1.00.csv . The load is around ~11s with reload is ~4s
  • Route viewer settings should be minimal, no shadow, texture filtering is bilinear with only 1 level, vsync on, object optimization is on high mode.

@adfriz

adfriz commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

wait... this messing with the $include route command...

i got plenty of error rail structure index out of range in Cycle.Rail command

well... let me set is as draft...

@adfriz
adfriz marked this pull request as draft September 12, 2026 15:08
@adfriz

adfriz commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

@ginga81 do you want to try this?

in route viewer try use minimal testing settings as i written above.

@adfriz

adfriz commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

alright, i think that one will fix it. although on my test its around ~4s slower... now first load is ~15s

well... at least better than wrong looking route.

@adfriz
adfriz marked this pull request as ready for review September 12, 2026 15:38
@adfriz

adfriz commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Ready for review.

@ginga81

ginga81 commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

All CPU cores are active, and I feel like loading times have become incredibly fast!
It is running on Linux (Ubuntu 24.04 + X11/KDE).
I haven't tweaked any settings; I'm using my usual configuration, including maximum graphics quality.

I'll open a separate issue for this later, but even when shadows are turned off, the brightness doesn't behave as it used to—it stays bright even when entering a tunnel.
すべてのCPUが動作

@adfriz

adfriz commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

ok i have added similar async load for object viewer.

currently only for b3d and csv mesh. for .animated mesh need more test although it works pretty well...

Note:
i add progress bar option for object viewer, same as in the route viewer. But i also add text based progress in the window name if extensions.cfg is loaded, it should be like Object viewer - Progress 0/10

i dont know if that route viewer like progress bar is needed here... maybe i will delete it and leave the window name progress only.

@leezer3

leezer3 commented Sep 13, 2026

Copy link
Copy Markdown
Owner

X files are broken somehow,

bug

I'm not entirely certain why, not got time to debug this tonight, but at a probable guess, your material cache is broken- Hitting the last loaded object's materials or something?

postnumber2_5.zip

Definitely no sign of schwellen4.bmp in there.

bug2

Randomly missing textures, very probably related to the above. These are stable throughout a reload, but if I close Route Viewer and re-open, different missing textures- I'd presume this is because the reload simply hits the cache.

@adfriz

adfriz commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

is there a link for that route?

@adfriz

adfriz commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

That commits may work...

@adfriz

adfriz commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

another fix and ram usage improvement. i hope its working on linux and macos...

@leezer3

leezer3 commented Sep 14, 2026

Copy link
Copy Markdown
Owner

That looks better now.

I'll try and test on Mac, but might be a few days.

@ginga81

ginga81 commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

It seemed to work fine on my Ubuntu 24.04.
ObjectViewer

@ginga81

ginga81 commented Sep 14, 2026

Copy link
Copy Markdown
Contributor
Screenshot from 2026-09-14 18-30-04 However, the scene redraws when I change direction, and the frame rate subsequently drops to 10 fps. It redraws the entire scene into the distance every time I change direction, and the frame rate does not recover afterwards.

@adfriz

adfriz commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Screenshot from 2026-09-14 18-30-04 However, the scene redraws when I change direction, and the frame rate subsequently drops to 10 fps. It redraws the entire scene into the distance every time I change direction, and the frame rate does not recover afterwards.

The opaque faces seems too much, did you didn't set the object optimization mode to high?

@ginga81

ginga81 commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

I wondered what "object optimization mode" was, so I checked the options and saw it was indeed set to "Low"...
Changing it to "High" restored things to how they were before.
Sorry for not understanding it sooner.
Screenshot from 2026-09-14 20-31-00

Add a private cache lock with Store/Report/Prune helpers, clone cache hits outside the lock, and record per-phase plugin load timings.
Route all cache inserts and failure reports through the locked helpers, and read the log via snapshots.
Decode outside the lookup lock with double-check registration, lock all allocator paths and readers, and prune object caches through locked helpers.
Mirror the main program hardening in ObjectViewer and RouteViewer, including log snapshots for all message readers.
Collect Structure declarations during parsing, decode them concurrently with auto-sized parallelism, then commit sequentially so loading screen progress stays monotonic and 100 percent still means playable.
Cycle.Ground and Cycle.Rail validate indices against committed Structure dictionaries. Replaying them after the object barrier restores baseline behavior.
Use per-file parser instances instead of static mutable state, so parallel Structure decoding cannot mix state across threads.
Unload textures and clear host object caches in DeInitialize before disposing the window; RouteViewer only deinitializes on the final close.
Load each unique object file once and serialize same-path texture registration, so parallel workers no longer parse and decode the same files redundantly.
Replace static host and hack state with per-load parser instances.
Replace static host with an instance field and drop the unused sound-folder hack.
Full ordered teardown (object caches, texture bytes, sound buffers) before loading a different route file.
Force LOH compaction during route-switch teardown and log managed memory plus cache counts before and after.
Null the static RouteData once parsing and applying finish.
…h check

Commit 392ad68 removed the width*height*4 truncation copy in CreateTexture that had been masking over-allocation in all 12 decompressors (depth*sizeOfPlane + height*bps + width*bpp). Every DDS then threw ArgumentException (data bytes are not of the expected length) and rendered white. Allocate the exact plane size and defensively truncate in CreateTexture.
…ureInternal)

textureCache could hold a null value when GetTexture failed (e.g. missing texture on BVE5/BVE6 routes), crashing on cachedTexture.MultipleFrames. Never cache failed decodes, purge poisoned null entries, guard null Origin in all TryGetValue paths, and route ObjectLibrary through the TryGetCachedTexture/StoreCachedTexture helpers with StoreCachedTexture hardened against null.
biSizeImage may legally be zero for uncompressed (BI_RGB) bitmaps, and pixels are read up to EOF, so the zero-size warning is a false positive there. Restrict it to compressed formats.
The Adam7 path copied 8 raw bytes per pixel into a Width*Height*4 output buffer, overrunning it and silently failing the whole texture. Keep each channel's high byte instead, mirroring the non-interlaced path.
Custom templates are legal in X files (3ds Max exporters emit metadata blocks such as KeyValuePair), but any data instance of an unknown template threw Unrecognised token and failed the whole object. Consume and skip such blocks instead; the nFaces==0 recovery path stays strict.
LoadStructureList decoded every structure file sequentially. Collect entries first, decode deduplicated files with Parallel.For (same DOP formula as the CSV parser), then commit per key in file order with identical semantics, including null entries for failed loads.
@adfriz
adfriz force-pushed the route-load-object-async branch from 2ddf61a to ac3965b Compare September 14, 2026 15:09
@adfriz

adfriz commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

alright, the commits from the DDS parts is mostly for BVE5/6 routes. now it should as fast as openbve routes.

There is still odd fps drop bug when turning on shadow feature when loading bve5/6 routes... idk what it cause....

@leezer3

leezer3 commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Apple appears to be OK, but I haven't done much testing on this at the minute.

Viewers seem to be broken on Apple though:

System.ApplicationException: ERROR: 0:25: '' :  version '410' is not supported
ERROR: 0:25: '' : syntax error: #version
ERROR: 0:26: 'precision' : syntax error: syntax error

  at LibRender2.Shaders.AbstractShader.LoadShader (System.String shaderSource, OpenTK.Graphics.OpenGL.ShaderType shaderType) [0x0005d] in /Users/christopher/RiderProjects/OpenBVE/source/LibRender2/Shaders/AbstractShader.cs:114 
  at LibRender2.Shaders.AbstractShader..ctor (LibRender2.BaseRenderer renderer, System.String vertexShaderName, System.String fragmentShaderName, System.Boolean isFromStream, System.Boolean fragColor) [0x0005b] in /Users/christopher/RiderProjects/OpenBVE/source/LibRender2/Shaders/AbstractShader.cs:59 
  at LibRender2.Shaders.Shader..ctor (LibRender2.BaseRenderer Renderer, System.String vertexShaderName, System.String fragmentShaderName, System.Boolean isFromStream) [0x00000] in /Users/christopher/RiderProjects/OpenBVE/source/LibRender2/Shaders/Shader.cs:79 
  at LibRender2.BaseRenderer.Initialize () [0x00010] in /Users/christopher/RiderProjects/OpenBVE/source/LibRender2/BaseRenderer.cs:391

That's somewhat odd, as it's just the default shader which is used on the main game which works OK :/
Maybe the requested context has got tweaked or something. I'll take a better look on Weds...

@adfriz

adfriz commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

That fix might work?

@leezer3
leezer3 merged commit e568154 into leezer3:master Sep 16, 2026
5 checks passed
@leezer3

leezer3 commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Merged, but I've just found this breaks the Object Viewer auto-reload. Might have to disable async for that?

@adfriz

adfriz commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

oh yeah its broken... but only when loading extensions.cfg

i will trace back, seems easy enough to fix

@leezer3

leezer3 commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Definitely broken with just a plain animated file. This is what I noticed it with:

[Object]
States=bogie_commonwealth.b3d, bogie_commonwealth.b3d, wheelset_solid.b3d
StateFunction = TrackNumber[0] + 1
Position=0,0,0

(Added a couple of minor functions this morning, was just using a random couple of files to test them with)

Timing related I'd assume.

@adfriz

adfriz commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

another minor bug, auto load not triggered when changing something inside .animated file that referenced in current loaded extensions.cfg, need manual reload.

but changing someting in extensions.cfg works as expected.

i think that bug not a big deal?

@adfriz

adfriz commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

can you test with #1421 ?

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