Code review follow-up to commit 0904372. Five Important fixes plus
three Minor polish items found by the reviewer before StreamingController
depends on this class under churn.
I1: Dispose is now thread-safe via Interlocked.Exchange on an int
guard. Two concurrent Dispose calls no longer double-dispose the
CancellationTokenSource.
I2: EnqueueLoad/EnqueueUnload now throw ObjectDisposedException when
called after Dispose instead of silently dropping the job. Jobs
vanishing into a completed channel was a debugging hazard.
I3: Start throws ObjectDisposedException when called after Dispose
instead of silently doing nothing (the old guard only checked
whether the thread was non-null, not whether the streamer was
still usable).
I4: New test Load_ExecutesLoaderOnBackgroundThread captures the
loader delegate's ManagedThreadId and asserts it differs from
the test thread's id, proving the whole reason this class
exists (off-thread execution) is actually happening.
I5: New LandblockStreamResult.WorkerCrashed record type for the
outer catch in WorkerLoop. Previously the crash path wrote
Failed(0, ex.ToString()) which collided with landblock (0, 0)
in the north ocean, making "worker crashed" indistinguishable
from "landblock 0 failed to load".
Minor polish:
- M1: Test spin constants (SpinTimeoutMs, SpinStepMs,
SpinMaxIterations) extracted so the 200 x 10ms pattern has one
source of truth.
- M2: DefaultDrainBatchSize public const on LandblockStreamer so
the batch cap has a name and a comment explaining why 4.
- M3: Safety-argument comment on the sync-over-async
WaitToReadAsync call explaining why it cannot deadlock (dedicated
thread, no SyncContext).
- M6: XML remarks on the class and on DrainCompletions documenting
threading contract (Enqueue = any thread, Drain = single consumer
thread).
112 Core + 96 Core.Net tests green.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
37 lines
1.7 KiB
C#
37 lines
1.7 KiB
C#
using AcDream.Core.World;
|
|
|
|
namespace AcDream.App.Streaming;
|
|
|
|
/// <summary>
|
|
/// A job posted to <see cref="LandblockStreamer"/>'s inbox. Either a load
|
|
/// (fetch this landblock from the dats and build its CPU-side mesh data)
|
|
/// or an unload (release any state tied to this landblock on the render
|
|
/// thread's next Tick drain).
|
|
/// </summary>
|
|
public abstract record LandblockStreamJob(uint LandblockId)
|
|
{
|
|
public sealed record Load(uint LandblockId) : LandblockStreamJob(LandblockId);
|
|
public sealed record Unload(uint LandblockId) : LandblockStreamJob(LandblockId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Outbox record the render thread drains. Either a successful load, a
|
|
/// failed load (logged and ignored until region recenters off/back), or
|
|
/// an unload notification (tells the render thread to release GPU state
|
|
/// for this landblock id).
|
|
/// </summary>
|
|
public abstract record LandblockStreamResult(uint LandblockId)
|
|
{
|
|
public sealed record Loaded(uint LandblockId, LoadedLandblock Landblock) : LandblockStreamResult(LandblockId);
|
|
public sealed record Failed(uint LandblockId, string Error) : LandblockStreamResult(LandblockId);
|
|
public sealed record Unloaded(uint LandblockId) : LandblockStreamResult(LandblockId);
|
|
|
|
/// <summary>
|
|
/// The worker loop itself crashed with an unhandled exception. Not tied
|
|
/// to a specific landblock — distinguished from <see cref="Failed"/>
|
|
/// because consumers typically route this to a fatal-log path rather
|
|
/// than retrying a single landblock later. LandblockId is 0 by
|
|
/// convention; readers should pattern-match on the type, not the id.
|
|
/// </summary>
|
|
public sealed record WorkerCrashed(string Error) : LandblockStreamResult(0);
|
|
}
|