minerender 2.1.0-alpha.142 → 2.1.0-alpha.143

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -184,8 +184,6 @@ async function sleep(timeout) {
184
184
  });
185
185
  }
186
186
  function yieldToEventLoop() {
187
- const scheduler = globalThis.scheduler;
188
- if (scheduler?.yield) return scheduler.yield();
189
187
  return new Promise((resolve) => {
190
188
  if (typeof MessageChannel !== "undefined") {
191
189
  const channel = new MessageChannel();
@@ -18528,6 +18526,7 @@ function skipsBlockFace(data, rule, block, neighbor, face) {
18528
18526
  // src/world/MineRenderWorld.ts
18529
18527
  init_log();
18530
18528
  var log18 = logger("MineRenderWorld");
18529
+ var YIELD_INTERVAL_MS = 8;
18531
18530
  var MineRenderWorld = class {
18532
18531
  constructor(scene, options = {}) {
18533
18532
  this._chunks = /* @__PURE__ */ new Map();
@@ -18641,7 +18640,7 @@ var MineRenderWorld = class {
18641
18640
  failures.set(chunk, error);
18642
18641
  if (this._chunks.get(`${chunk.x}_${chunk.y}_${chunk.z}`) === chunk) this.pendingBiomes.set(chunk, void 0);
18643
18642
  }
18644
- if (performance.now() - sliceStart > 8) {
18643
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18645
18644
  await yieldToEventLoop();
18646
18645
  sliceStart = performance.now();
18647
18646
  }
@@ -18796,7 +18795,7 @@ var MineRenderWorld = class {
18796
18795
  ));
18797
18796
  for (const pos of positions) changes.set(`${pos.x},${pos.y},${pos.z}`, pos);
18798
18797
  }
18799
- if (performance.now() - sliceStart > 8) {
18798
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18800
18799
  await yieldToEventLoop();
18801
18800
  sliceStart = performance.now();
18802
18801
  }
@@ -18804,6 +18803,62 @@ var MineRenderWorld = class {
18804
18803
  }
18805
18804
  /** Replaces a chunk column's blocks, biomes, and saved light and, with `renderEntities`, its supported saved mobs. */
18806
18805
  async placeChunk(chunk, executor) {
18806
+ return this.placeColumn(chunk, executor);
18807
+ }
18808
+ /**
18809
+ * Replaces parsed columns in input order, then refreshes neighboring geometry once.
18810
+ * Duplicate coordinates retain their order. A failed column stops placement; queued refreshes finish before rejection.
18811
+ */
18812
+ async placeChunks(chunks) {
18813
+ const columns = [];
18814
+ for (const chunk of chunks) {
18815
+ const { x, z } = chunk ?? {};
18816
+ if (![x, z].every(Number.isSafeInteger)) {
18817
+ throw new RangeError("Chunk column coordinates must be safe integers");
18818
+ }
18819
+ const sections = Array.from(chunk.sections, (section) => {
18820
+ const { y } = section ?? {};
18821
+ if (!Number.isSafeInteger(y)) throw new RangeError("Chunk section coordinates must be safe integers");
18822
+ return { ...section, y };
18823
+ });
18824
+ columns.push({ ...chunk, x, z, sections });
18825
+ }
18826
+ if (!columns.length) return;
18827
+ await this.culling;
18828
+ let failed = false, failure, sliceStart = performance.now();
18829
+ try {
18830
+ for (const column of columns) {
18831
+ await this.placeColumn(column, void 0, true);
18832
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18833
+ await yieldToEventLoop();
18834
+ sliceStart = performance.now();
18835
+ }
18836
+ }
18837
+ } catch (error) {
18838
+ failed = true;
18839
+ failure = error;
18840
+ } finally {
18841
+ try {
18842
+ const failures = await this.refreshBiomes(true);
18843
+ if (failures.size) log18.warn("Failed to refresh biome colors", failures.values().next().value);
18844
+ } catch (error) {
18845
+ if (!failed) {
18846
+ failed = true;
18847
+ failure = error;
18848
+ }
18849
+ }
18850
+ try {
18851
+ await this.updateCulling([]);
18852
+ } catch (error) {
18853
+ if (!failed) {
18854
+ failed = true;
18855
+ failure = error;
18856
+ }
18857
+ }
18858
+ }
18859
+ if (failed) throw failure;
18860
+ }
18861
+ async placeColumn(chunk, executor, deferCulling = false) {
18807
18862
  if (![chunk.x, chunk.z].every(Number.isSafeInteger)) {
18808
18863
  throw new RangeError("Chunk column coordinates must be safe integers");
18809
18864
  }
@@ -18865,7 +18920,7 @@ var MineRenderWorld = class {
18865
18920
  } finally {
18866
18921
  this.markSectionChanged(target);
18867
18922
  }
18868
- if (performance.now() - sliceStart > 8) {
18923
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18869
18924
  await yieldToEventLoop();
18870
18925
  sliceStart = performance.now();
18871
18926
  }
@@ -18888,9 +18943,12 @@ var MineRenderWorld = class {
18888
18943
  await placeEntities?.();
18889
18944
  } finally {
18890
18945
  this.markBiomesChanged(chunk.x, chunk.z, void 0, false);
18891
- const failures = await this.refreshBiomes();
18892
- if (failures.size) log18.warn("Failed to refresh biome colors", failures.values().next().value);
18893
- await this.updateCulling([...changes.values()]);
18946
+ if (deferCulling) await this.updateCulling([...changes.values()], true);
18947
+ else {
18948
+ const failures = await this.refreshBiomes();
18949
+ if (failures.size) log18.warn("Failed to refresh biome colors", failures.values().next().value);
18950
+ await this.updateCulling([...changes.values()]);
18951
+ }
18894
18952
  }
18895
18953
  }
18896
18954
  /**
@@ -18920,7 +18978,7 @@ var MineRenderWorld = class {
18920
18978
  for (const { x, z } of columns.values()) {
18921
18979
  this.entities?.clearColumn(x, z);
18922
18980
  await this.clearChunkColumn(x, z);
18923
- if (performance.now() - sliceStart > 8) {
18981
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18924
18982
  await yieldToEventLoop();
18925
18983
  sliceStart = performance.now();
18926
18984
  }
@@ -19059,15 +19117,25 @@ var MineRenderWorld = class {
19059
19117
  for (const [index, mask] of masks) await chunk.setCullMaskIndex(index, mask);
19060
19118
  await chunk.refreshLight(indices === "all" ? void 0 : indices);
19061
19119
  chunk.refreshAmbientOcclusion(this._ambientOcclusion);
19062
- if ((this._ambientOcclusion || this.ambientOcclusionPending) && performance.now() - sliceStart > 8) {
19120
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19063
19121
  await yieldToEventLoop();
19064
19122
  sliceStart = performance.now();
19065
19123
  }
19066
19124
  }
19067
- const builds = await Promise.allSettled([...batch.keys()].map(async (chunk) => {
19068
- await chunk.rebuildSectionMesh();
19069
- batch.delete(chunk);
19070
- }));
19125
+ const pending = [];
19126
+ for (const chunk of [...batch.keys()]) {
19127
+ const build = (async () => {
19128
+ await chunk.rebuildSectionMesh();
19129
+ batch.delete(chunk);
19130
+ })();
19131
+ void build.catch(() => void 0);
19132
+ pending.push(build);
19133
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19134
+ await yieldToEventLoop();
19135
+ sliceStart = performance.now();
19136
+ }
19137
+ }
19138
+ const builds = await Promise.allSettled(pending);
19071
19139
  const failure = builds.find((result) => result.status === "rejected");
19072
19140
  if (failure?.status === "rejected") throw failure.reason;
19073
19141
  } catch (error) {
@@ -20527,6 +20595,7 @@ var Chunk = class _Chunk {
20527
20595
  };
20528
20596
 
20529
20597
  // src/world/WorldStreamer.ts
20598
+ var MAX_PREFETCHED_COLUMNS = 4;
20530
20599
  var WorldStreamer = class {
20531
20600
  constructor(world, source, options = {}) {
20532
20601
  this.world = world;
@@ -20536,6 +20605,7 @@ var WorldStreamer = class {
20536
20605
  this.missing = /* @__PURE__ */ new Map();
20537
20606
  this.failed = /* @__PURE__ */ new Map();
20538
20607
  this.desired = [];
20608
+ this.reads = /* @__PURE__ */ new Map();
20539
20609
  this.disposed = false;
20540
20610
  this.loadRadius = options.loadRadius ?? 2;
20541
20611
  this.unloadRadius = options.unloadRadius ?? Math.min(32, this.loadRadius + 1);
@@ -20580,6 +20650,12 @@ var WorldStreamer = class {
20580
20650
  if (this.loading && !this.within(this.loading.position, this.loadRadius)) {
20581
20651
  this.loading.controller.abort();
20582
20652
  }
20653
+ for (const [key, read] of this.reads) {
20654
+ if (!this.within(read.position, this.loadRadius)) {
20655
+ read.controller.abort();
20656
+ if (read.ready) this.reads.delete(key);
20657
+ }
20658
+ }
20583
20659
  }
20584
20660
  return this.running ??= Promise.resolve().then(() => this.drain());
20585
20661
  }
@@ -20600,8 +20676,11 @@ var WorldStreamer = class {
20600
20676
  this.desired = [];
20601
20677
  this.center = void 0;
20602
20678
  this.loading?.controller.abort();
20679
+ for (const read of this.reads.values()) read.controller.abort();
20603
20680
  return this.disposal ??= Promise.resolve().then(async () => {
20604
20681
  await this.running?.catch(() => void 0);
20682
+ await Promise.all([...this.reads.values()].map((read) => read.promise));
20683
+ this.reads.clear();
20605
20684
  try {
20606
20685
  await this.unloadMany([...this.owned.values()]);
20607
20686
  } finally {
@@ -20627,6 +20706,10 @@ var WorldStreamer = class {
20627
20706
  await this.unloadMany(obsolete);
20628
20707
  continue;
20629
20708
  }
20709
+ if (this.world.placeChunks) {
20710
+ if (await this.placeReadyChunks()) continue;
20711
+ return;
20712
+ }
20630
20713
  const position = this.desired.find((position2) => this.needsLoad(position2));
20631
20714
  if (!position) return;
20632
20715
  let chunk;
@@ -20649,26 +20732,105 @@ var WorldStreamer = class {
20649
20732
  continue;
20650
20733
  }
20651
20734
  this.owned.set(this.key(position), position);
20735
+ await this.placeOwnedColumns([position], () => this.world.placeChunk(chunk));
20736
+ }
20737
+ } finally {
20738
+ for (const [key, read] of this.reads) {
20739
+ read.controller.abort();
20740
+ if (read.ready) this.reads.delete(key);
20741
+ }
20742
+ this.running = void 0;
20743
+ }
20744
+ }
20745
+ fillReadSlots() {
20746
+ while (!this.disposed && this.reads.size < MAX_PREFETCHED_COLUMNS) {
20747
+ const position = this.desired.find((position2) => this.needsLoad(position2) && !this.reads.has(this.key(position2)));
20748
+ if (!position) return;
20749
+ const key = this.key(position);
20750
+ const read = {
20751
+ position,
20752
+ controller: new AbortController(),
20753
+ promise: Promise.resolve(),
20754
+ ready: false,
20755
+ failed: false
20756
+ };
20757
+ this.reads.set(key, read);
20758
+ read.promise = (async () => {
20652
20759
  try {
20653
- await this.world.placeChunk(chunk);
20654
- this.loaded.set(this.key(position), position);
20760
+ read.chunk = await this.source.getChunk(position.x, position.z, read.controller.signal);
20761
+ if (read.chunk) this.validateChunk(read.chunk, position);
20655
20762
  } catch (error) {
20656
- try {
20657
- await this.unloadMany([position]);
20658
- } catch (cleanupError) {
20659
- const failure = error instanceof Error ? error : new Error(String(error));
20660
- try {
20661
- Object.defineProperty(failure, "cause", { value: cleanupError, configurable: true });
20662
- } catch {
20663
- throw Object.assign(new Error(failure.message), { cause: cleanupError });
20664
- }
20665
- throw failure;
20763
+ read.failed = true;
20764
+ read.error = error;
20765
+ } finally {
20766
+ read.ready = true;
20767
+ if (read.controller.signal.aborted || this.disposed || !this.within(position, this.loadRadius)) {
20768
+ this.reads.delete(key);
20666
20769
  }
20667
- throw error;
20770
+ }
20771
+ })();
20772
+ }
20773
+ }
20774
+ /** Places only the ready prefix, so later source reads cannot delay the nearest ready column. */
20775
+ async placeReadyChunks() {
20776
+ this.fillReadSlots();
20777
+ const position = this.desired.find((position2) => this.needsLoad(position2));
20778
+ if (!position) {
20779
+ if (!this.reads.size) return false;
20780
+ await Promise.all([...this.reads.values()].map((read) => read.promise));
20781
+ return true;
20782
+ }
20783
+ const head = this.reads.get(this.key(position));
20784
+ if (!head) {
20785
+ for (let index = this.desired.length - 1; index >= 0; index--) {
20786
+ const key = this.key(this.desired[index]);
20787
+ if (this.reads.get(key)?.ready) {
20788
+ this.reads.delete(key);
20789
+ return true;
20668
20790
  }
20669
20791
  }
20670
- } finally {
20671
- this.running = void 0;
20792
+ await Promise.race([...this.reads.values()].map((read) => read.promise));
20793
+ return true;
20794
+ }
20795
+ if (!head.ready) {
20796
+ await head.promise;
20797
+ return true;
20798
+ }
20799
+ const chunks = [], positions = [];
20800
+ for (const desired of this.desired) {
20801
+ if (!this.needsLoad(desired)) continue;
20802
+ const key = this.key(desired), read = this.reads.get(key);
20803
+ if (!read?.ready) break;
20804
+ this.reads.delete(key);
20805
+ if (read.failed) this.failed.set(key, { ...desired, error: read.error });
20806
+ else if (!read.chunk) this.missing.set(key, desired);
20807
+ else {
20808
+ chunks.push(read.chunk);
20809
+ positions.push(desired);
20810
+ }
20811
+ }
20812
+ if (!chunks.length) return true;
20813
+ for (const position2 of positions) this.owned.set(this.key(position2), position2);
20814
+ await this.placeOwnedColumns(positions, () => this.world.placeChunks(chunks));
20815
+ return true;
20816
+ }
20817
+ async placeOwnedColumns(positions, place) {
20818
+ try {
20819
+ await place();
20820
+ for (const position of positions) this.loaded.set(this.key(position), position);
20821
+ } catch (error) {
20822
+ try {
20823
+ await this.unloadMany(positions);
20824
+ } catch (cleanupError) {
20825
+ const failure = error instanceof Error ? error : new Error(String(error));
20826
+ try {
20827
+ Object.defineProperty(failure, "cause", { value: cleanupError, configurable: true });
20828
+ } catch {
20829
+ throw Object.assign(new Error(failure.message), { cause: cleanupError });
20830
+ }
20831
+ throw failure;
20832
+ }
20833
+ throw error;
20672
20834
  }
20673
20835
  }
20674
20836
  needsLoad(position) {