Skip to content

fix: 审查问题修复(换维度清理、流式音乐、网络线程、网格背压、粒子上限、JS 沙箱) - #5

Merged
gaoyu06 merged 15 commits into
mainfrom
fix/extreme-review
Oct 16, 2026
Merged

gaoyu06 merged 15 commits into
mainfrom
fix/extreme-review

Conversation

@gaoyu06

@gaoyu06 gaoyu06 commented Oct 16, 2026

Copy link
Copy Markdown
Member

修复全量审查中的 extreme-1~12。验证:cargo test 全工作区通过(229+95+23+69+152)。没有在客户端里实际运行。

S2A carries an i32 count; a huge value pushed millions of particles in one
call before tick trimmed them to MAX_PARTICLES, stalling the main thread or
exhausting memory. Clamp the spawn count to MAX_PARTICLES.
Vanilla replaces the WorldClient when the dimension changes. Proxies switch
servers with a Respawn into another dimension followed by the target one and
the old server never sends ChunkUnload, so its chunks and entities stayed in
memory and lingered as ghost terrain/entities. Reset the world and all
per-world side tables (except the local player) and have the renderer drop
every chunk mesh and in-flight mesh job.
Music went through the static-sound path: each new track was fully decoded
to PCM on the main thread (multi-second stall for a 4-minute ogg) and the
~90 MB result was cached forever. Stream music via kira's StreamingSoundData
(decoded on its own thread, never cached), skip caching clips longer than
~10 s and flush the effect cache once it exceeds ~64 MB of PCM.
…ad death

- The network thread alternated a 10 ms timed read with draining the send
  queue, so every outbound packet (attacks, flying, transaction acks) waited
  up to 10 ms on a quiet connection. Split the socket into read and write
  halves: the writer blocks on the queue and writes immediately, the reader
  blocks on the socket. Dropping the handle shuts the socket down so both
  threads exit.
- Reject frame lengths and zlib uncompressed lengths outside vanilla's
  2097152 limit (negative lengths panicked with_capacity, huge ones grew the
  buffer or inflated unbounded) and validate list counts in 0x13/0x22/0x26
  and team packets against the remaining packet size.
- A panic on the reader thread is caught and sent as Disconnected, and
  pump_network treats a disconnected event channel as a lost connection
  instead of leaving the player frozen in the world.
- Each queued mesh job pins a 1-2 MB column snapshot and the job channel
  was unbounded: under a chunk burst the main thread kept submitting 40
  sections a frame regardless of worker progress. Track in-flight jobs and
  stop taking ordinary dirty sections once the backlog reaches 16 per
  worker thread; they stay dirty for later frames.
- A panic in build_section_mesh_neighborhood killed its worker and dropped
  the job. Catch it per job, log the section, count it and deliver an empty
  mesh; recover a poisoned queue lock instead of unwrapping it.
- F3 shows the mesh queue depth and, if any, the failure count.
mouse_factor, fps_limit and the other derived getters took Settings by
value, so every mouse event and frame cloned the whole struct (Vec/String
fields included). Borrow instead and drop the clones at the call sites.
Applying extension block tints rebuilt and uploaded every loaded section
synchronously on the main thread (upload_world), freezing the frame when
tint mods reloaded in game. Install the tint table and mark all sections
dirty instead, like the resource pack reload path.
…e copies

- Recycling one of the 64 player skin rows left the old owner marked as
  requested, so it never got a row again and showed the default skin for
  the rest of the session. Keep decoded skins cached and give rows to the
  players drawn this frame (spawned players, skull owners, local player),
  recycling the least recently wanted row; a returning player gets its row
  back from the cache without a re-download.
- Downloads ran on one new thread per uuid with no timeout. Use four
  shared worker threads and a 15 s ureq timeout.
- Filter the roster by is_requested before copying the base64 textures
  property, instead of cloning every property every frame.
JS mod hooks run synchronously on the render thread with no interrupt
handler or memory limit, so a while(true) or unbounded allocation in a mod
froze or exhausted the client. Install a QuickJS interrupt handler that
aborts a hook after 50 ms (2 s for top-level code at load); an aborted or
out-of-memory hook panics so the manager disables the mod as it does for
other faulting hooks. Enforce a 128 MiB heap cap through a counting
allocator, since rquickjs ignores set_memory_limit with rust-alloc.
ChunkLayer kept freed slots as separate (vertex, index) pairs that never
merged, so long flights fragmented pages until new 128 MB pages were
created, and pages were never freed. Allocate each page's vertex and index
ranges with a first-fit allocator that coalesces neighbours and rewinds
the bump cursor, place sections on the lowest page with room, release
trailing empty pages, and keep only one page on clear().
The 128 MiB heap cap was shared by every JS mod, so one mod leaking up to
it made every other mod's allocations fail too. JsRuntime::load now
consumes the runtime, and the manager creates one per mod: a leak is
confined to the mod that leaks.
QuickJS raises out-of-memory as a catchable exception and the prelude
wraps every handler in try/catch, so the string check on the dispatcher
error almost never fired: a mod at its cap stayed enabled, spinning on
failed allocations every hook. The allocator now records a refusal and
JsPlugin::call disables the mod on it.
Records carry "stream": true in vanilla's sounds.json. They went through
the static path, so every play decoded a multi-minute ogg on the main
thread, and since such clips are now too long to cache, each play did it
again. Honor the flag and stream those entries like music.
insert() frees a section's old slot before placing its new mesh. When that
section was alone on the last page, the page was released and a new 128 MB
page created right after, on every re-mesh that grew it. Release trailing
empty pages from remove() only.
The per-packet clamp did not bound the total: pump_network drains every
queued packet in one frame and only tick() trimmed the Vec, so many
huge-count packets could still push millions of particles. spawn now
evicts the oldest up front so the live set never exceeds MAX_PARTICLES.
@gaoyu06
gaoyu06 merged commit 01dbbdd into main Oct 16, 2026
5 checks passed
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.

1 participant