Asset loading intermittently exceeds RLIMIT_NOFILE on 0.19 despite the #22560 semaphore (500ms timeout falls through to unbounded opens)
- Dominant language
- Rust
- Stars
- 48.2k
- Forks
- 4.8k
- Avg merge
- 3d 22h
- Merged PRs (30d)
- 161
Description
## Bevy version and features
- Bevy `0.19.0`
- Non-default features: `["jpeg", "ktx2", "basis-universal", "wav"]`
## Relevant system information
- Rust `1.95.0` (stable)
- macOS `26.2` (Apple Silicon), default GUI-launch soft `RLIMIT_NOFILE` = 256
- Backend: Metal
## What you did
I preload ~650 assets in a single startup system: a list of paths looped through `asset_server.load()`, handles stashed in a resource so nothing unloads mid-run. It's ~30 GLBs (scenes + animations + embedded textures), a few hundred textures (KTX2/PNG), ~80 audio files, and 100+ WGSL shaders, so effectively everything is requested in the same frame.
## What went wrong
**Expected:** the concurrent-open-files cap added in #22560 keeps me under the OS `RLIMIT_NOFILE`, so a burst of loads never hits `EMFILE`.
**Actual:** the loads still intermittently fail with `Too many open files (EMFILE)` on 0.19.
Reading #22560, I think the cause is that the semaphore acquire has a **500ms timeout, after which the loader "continues like before"** (commit `32124bb`), i.e. it opens the file anyway. Under a burst like mine, the large GLB/KTX2 reads hold permits for a while, so many waiting loaders hit the timeout and then open their files past the cap, unbounded again. That also explains the intermittency: it only breaks when enough long reads pile up for the timeouts to cascade.
Most resulting failures are visible (missing textures), but the worst one is silent: a **compute shader** asset fails to load, `PipelineCache` reports "Pipeline could not be compiled because the following shader could not be loaded", and then the pipeline **stays in `Err` forever**. My compute pass never dispatches, and a material reading that storage buffer renders uninitialized GPU memory (garbage triangles). for me It took days to trace that back to `EMFILE`, because nothing surfaces the permanent failure at runtime.
> Note: I first raised this in the Discord platform channel but wanted to create on hre also since it may be a silent failure affecting other OSes too.
fyi:
https://discord.com/channels/691052431525675048/749690399135891486/1522946680410996757
Contributor guide
Research direction
Start by tracing the asset_server.load() path through the semaphore acquire and its 500ms timeout, then follow how failures reach PipelineCache. Reproduce a burst of large GLB/KTX2 and shader loads under RLIMIT_NOFILE; done means the cap is not bypassed after timeout and compute-shader load failures are surfaced rather than leaving the pipeline permanently Err.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- game-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100