From 754140ea18eddc56bc55b7ae56b5c5894af2f48c Mon Sep 17 00:00:00 2001 From: Fenad2 Date: Mon, 17 Aug 2026 18:21:08 +0800 Subject: [PATCH] refactor: optimize preview loading with section-based pipeline --- gradle.properties | 4 +- .../browser/ListMoreSchematicBrowser.java | 4 + .../preview/SchematicPreviewLoader.java | 144 +++++++- .../preview/SchematicPreviewModel.java | 162 --------- .../preview/SchematicPreviewSession.java | 95 ++++-- .../preview/gui/SchematicPreviewOverlay.java | 8 +- .../preview/model/SchematicPreviewModel.java | 91 ++++++ .../model/SchematicPreviewScanner.java | 303 +++++++++++++++++ .../model/SchematicPreviewSectionStorage.java | 49 +++ .../render/SchematicPreviewRenderBackend.java | 9 +- .../render/SchematicPreviewRenderManager.java | 45 +-- .../render/SchematicPreviewRenderer.java | 309 +++++++++++------- .../preview/render/SchematicPreviewWorld.java | 2 +- 13 files changed, 874 insertions(+), 351 deletions(-) delete mode 100644 src/main/java/com/listmore/schematic/preview/SchematicPreviewModel.java create mode 100644 src/main/java/com/listmore/schematic/preview/model/SchematicPreviewModel.java create mode 100644 src/main/java/com/listmore/schematic/preview/model/SchematicPreviewScanner.java create mode 100644 src/main/java/com/listmore/schematic/preview/model/SchematicPreviewSectionStorage.java diff --git a/gradle.properties b/gradle.properties index fc00b99..2a0200e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -# Done to increase the memory available to gradle. -org.gradle.jvmargs=-Xmx1G +org.gradle.jvmargs=-Xmx3G +org.gradle.workers.max=2 org.gradle.configuration-cache=false diff --git a/src/main/java/com/listmore/schematic/browser/ListMoreSchematicBrowser.java b/src/main/java/com/listmore/schematic/browser/ListMoreSchematicBrowser.java index cd16687..31ded9d 100644 --- a/src/main/java/com/listmore/schematic/browser/ListMoreSchematicBrowser.java +++ b/src/main/java/com/listmore/schematic/browser/ListMoreSchematicBrowser.java @@ -62,6 +62,7 @@ protected void drawSelectedSchematicInfo( @Nullable DirectoryEntry entry) { if (!isLitematic(entry)) { this.previewLayout = null; + this.previewSession.clear(); super.drawSelectedSchematicInfo(context, entry); return; } @@ -69,6 +70,7 @@ protected void drawSelectedSchematicInfo( Pair metaPair = this.getSchematicVersionAndMetadata(entry); if (metaPair == null || metaPair.getRight() == null) { this.previewLayout = null; + this.previewSession.clear(); super.drawSelectedSchematicInfo(context, entry); return; } @@ -139,6 +141,8 @@ protected void drawSelectedSchematicInfo( this.previewSession.setFile(entry.getFullPath()); this.previewSession.update(); SchematicPreviewOverlay.draw(context, this.previewLayout, this.previewSession, this.lastMouseX, this.lastMouseY); + } else { + this.previewSession.clear(); } } diff --git a/src/main/java/com/listmore/schematic/preview/SchematicPreviewLoader.java b/src/main/java/com/listmore/schematic/preview/SchematicPreviewLoader.java index f47f917..ff22f5c 100644 --- a/src/main/java/com/listmore/schematic/preview/SchematicPreviewLoader.java +++ b/src/main/java/com/listmore/schematic/preview/SchematicPreviewLoader.java @@ -1,78 +1,188 @@ package com.listmore.schematic.preview; +import com.listmore.schematic.preview.model.SchematicPreviewModel; +import com.listmore.schematic.preview.model.SchematicPreviewScanner; + import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import fi.dy.masa.litematica.schematic.LitematicaSchematic; // 后台读取 litematic 文件并提取预览模型 +// 文件解析和 Section 扫描使用不同线程池 public final class SchematicPreviewLoader { - //TODO:目前来看4已经够用了,但真的够吗? + //TODO:目前来看4已经够用了,但真的够吗? --> 6 private static final int MAX_CACHE_ENTRIES = 4; + private static final int SCAN_WORKERS = Math.max(1, + Math.min(6, Runtime.getRuntime().availableProcessors() - 2)); + private static final ExecutorService FILE_EXECUTOR = Executors.newFixedThreadPool(2, + threadFactory("ListMore preview file")); + private static final ExecutorService SCAN_EXECUTOR = Executors.newFixedThreadPool( + SCAN_WORKERS, threadFactory("ListMore preview scan")); + // 缓存项由多个 Session 共享 private static final ConcurrentMap CACHE = new ConcurrentHashMap<>(); private static final ConcurrentLinkedQueue CACHE_ORDER = new ConcurrentLinkedQueue<>(); private SchematicPreviewLoader() { } - public static CompletableFuture load(Path file) { + public static LoadRequest load(Path file) { Path normalizedFile = file.toAbsolutePath().normalize(); FileStamp stamp; try { stamp = new FileStamp(Files.getLastModifiedTime(normalizedFile).toMillis(), Files.size(normalizedFile)); } catch (IOException exception) { - return CompletableFuture.failedFuture(exception); + return new LoadRequest(CompletableFuture.failedFuture(exception), () -> {}); } CacheEntry entry = CACHE.compute(normalizedFile, (path, cached) -> { - if (cached != null && cached.stamp().equals(stamp)) { + if (cached != null && cached.stamp.equals(stamp) && !cached.load.future().isCompletedExceptionally()) { return cached; } + if (cached != null) { + CACHE_ORDER.remove(new CacheReference(path, cached)); + cached.cancelIfUnused(); + } CacheEntry created = new CacheEntry(stamp, startLoad(path)); CACHE_ORDER.add(new CacheReference(path, created)); return created; }); + LoadRequest request = entry.acquire(normalizedFile); trimCache(); - entry.task().whenComplete((preview, throwable) -> { - if (throwable != null || preview == null) { - CACHE.remove(normalizedFile, entry); + entry.load.future().whenComplete((model, throwable) -> { + if (throwable != null || model == null) { + removeEntry(normalizedFile, entry); } }); - return entry.task(); + return request; } - private static CompletableFuture startLoad(Path file) { - return CompletableFuture.supplyAsync(() -> { - LitematicaSchematic schematic = LitematicaSchematic.createFromFile(file.getParent(), file.getFileName().toString()); - if (schematic == null) { - throw new IllegalStateException("Litematica returned no schematic"); + private static SharedLoad startLoad(Path file) { + AtomicBoolean cancelled = new AtomicBoolean(); + CompletableFuture result = new CompletableFuture<>(); + Future worker = FILE_EXECUTOR.submit(() -> { + try { + LitematicaSchematic schematic = LitematicaSchematic.createFromFile( + file.getParent(), file.getFileName().toString()); + if (schematic == null) { + throw new IllegalStateException("Litematica returned no schematic"); + } + if (cancelled.get()) { + throw new CancellationException("Schematic preview load cancelled"); + } + SchematicPreviewModel model = SchematicPreviewScanner.scan( + schematic, SCAN_EXECUTOR, SCAN_WORKERS, cancelled::get); + result.complete(model); + } catch (Throwable throwable) { + result.completeExceptionally(throwable); } - return new LoadedPreview(schematic, SchematicPreviewModel.from(schematic)); + }); + return new SharedLoad(result, () -> { + cancelled.set(true); + worker.cancel(true); + result.cancel(false); }); } + private static ThreadFactory threadFactory(String prefix) { + AtomicInteger nextId = new AtomicInteger(); + return runnable -> { + Thread thread = new Thread(runnable, prefix + "-" + nextId.incrementAndGet()); + thread.setDaemon(true); + thread.setPriority(Math.max(Thread.MIN_PRIORITY, Thread.NORM_PRIORITY - 1)); + return thread; + }; + } + private static void trimCache() { while (CACHE.size() > MAX_CACHE_ENTRIES) { CacheReference oldest = CACHE_ORDER.poll(); if (oldest == null) { return; } - CACHE.remove(oldest.file(), oldest.entry()); + if (CACHE.remove(oldest.file(), oldest.entry())) { + oldest.entry().cancelIfUnused(); + } } } - public record LoadedPreview(LitematicaSchematic schematic, SchematicPreviewModel model) { + private static boolean removeEntry(Path file, CacheEntry entry) { + if (!CACHE.remove(file, entry)) { + return false; + } + CACHE_ORDER.remove(new CacheReference(file, entry)); + return true; + } + + public static final class LoadRequest { + private final CompletableFuture future; + private final Runnable cancellation; + + private LoadRequest(CompletableFuture future, Runnable cancellation) { + this.future = future; + this.cancellation = cancellation; + } + + public CompletableFuture future() { + return this.future; + } + + public void cancel() { + this.cancellation.run(); + } } private record FileStamp(long modifiedTime, long size) { } - private record CacheEntry(FileStamp stamp, CompletableFuture task) { + private static final class CacheEntry { + private final FileStamp stamp; + private final SharedLoad load; + private final AtomicInteger subscribers = new AtomicInteger(); + + private CacheEntry(FileStamp stamp, SharedLoad load) { + this.stamp = stamp; + this.load = load; + } + + private LoadRequest acquire(Path file) { + this.subscribers.incrementAndGet(); + AtomicBoolean released = new AtomicBoolean(); + return new LoadRequest(this.load.future(), () -> { + if (!released.compareAndSet(false, true)) { + return; + } + // Future 属于共享缓存项,只有最后一个离开时才能取消任务 + if (this.subscribers.decrementAndGet() == 0 && !this.load.future().isDone()) { + removeEntry(file, this); + this.load.cancel(); + } + }); + } + + private void cancelIfUnused() { + if (this.subscribers.get() == 0 && !this.load.future().isDone()) { + this.load.cancel(); + } + } + } + + private record SharedLoad(CompletableFuture future, Runnable cancellation) { + private void cancel() { + this.cancellation.run(); + } } private record CacheReference(Path file, CacheEntry entry) { diff --git a/src/main/java/com/listmore/schematic/preview/SchematicPreviewModel.java b/src/main/java/com/listmore/schematic/preview/SchematicPreviewModel.java deleted file mode 100644 index 5e194ad..0000000 --- a/src/main/java/com/listmore/schematic/preview/SchematicPreviewModel.java +++ /dev/null @@ -1,162 +0,0 @@ -package com.listmore.schematic.preview; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import fi.dy.masa.litematica.schematic.LitematicaSchematic; -import fi.dy.masa.litematica.schematic.container.ILitematicaBlockStatePalette; -import fi.dy.masa.litematica.schematic.container.LitematicaBitArray; -import fi.dy.masa.litematica.schematic.container.LitematicaBlockStateContainer; -import fi.dy.masa.litematica.selection.Box; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Vec3i; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.block.state.BlockState; - -public final class SchematicPreviewModel { - private final Vec3i size; - private final float centerX; - private final float centerY; - private final float centerZ; - private final List blocks; - private final Map statesByPosition; - - private SchematicPreviewModel(Vec3i size, float centerX, float centerY, float centerZ, List blocks, - Map statesByPosition) { - this.size = size; - this.centerX = centerX; - this.centerY = centerY; - this.centerZ = centerZ; - this.blocks = Collections.unmodifiableList(blocks); - this.statesByPosition = Collections.unmodifiableMap(statesByPosition); - } - - public Vec3i size() { return this.size; } - public float centerX() { return this.centerX; } - public float centerY() { return this.centerY; } - public float centerZ() { return this.centerZ; } - public List blocks() { return this.blocks; } - - // 查询预览坐标中的方块状态 - public BlockState blockStateAt(int x, int y, int z) { - if (x < 0 || y < 0 || z < 0 || x >= this.size.getX() || y >= this.size.getY() || z >= this.size.getZ()) { - return Blocks.AIR.defaultBlockState(); - } - return this.statesByPosition.getOrDefault(packPosition(x, y, z), Blocks.AIR.defaultBlockState()); - } - - // 从 Litematica 已解析的数据中提取非空气方块 - // 流程:遍历所有区域 -> 计算包围盒 -> 遍历容器提取非空气方块并转为相对坐标 -> 收集方块实体数据 - public static SchematicPreviewModel from(LitematicaSchematic schematic) { - Map areas = schematic.getAreas(); - if (areas.isEmpty()) { - return empty(); - } - - // 遍历所有区域计算包围盒 - int minX = Integer.MAX_VALUE; - int minY = Integer.MAX_VALUE; - int minZ = Integer.MAX_VALUE; - int maxXExclusive = Integer.MIN_VALUE; - int maxYExclusive = Integer.MIN_VALUE; - int maxZExclusive = Integer.MIN_VALUE; - for (Map.Entry entry : areas.entrySet()) { - LitematicaBlockStateContainer container = schematic.getSubRegionContainer(entry.getKey()); - Box area = entry.getValue(); - BlockPos first = area.getPos1(); - BlockPos second = area.getPos2(); - if (container == null || first == null || second == null) { - continue; - } - // 区域原点取两角点中较小者 - int originX = Math.min(first.getX(), second.getX()); - int originY = Math.min(first.getY(), second.getY()); - int originZ = Math.min(first.getZ(), second.getZ()); - Vec3i regionSize = container.getSize(); - // 更新全局包围盒:原点取最小,远端取 origin+size 的最大值 - minX = Math.min(minX, originX); - minY = Math.min(minY, originY); - minZ = Math.min(minZ, originZ); - maxXExclusive = Math.max(maxXExclusive, originX + regionSize.getX()); - maxYExclusive = Math.max(maxYExclusive, originY + regionSize.getY()); - maxZExclusive = Math.max(maxZExclusive, originZ + regionSize.getZ()); - } - if (minX == Integer.MAX_VALUE) { - return empty(); - } - - // 从每个区域中提取非空气方块,转换为相对坐标 - List blocks = new ArrayList<>(); - Map statesByPosition = new HashMap<>(); - for (Map.Entry entry : areas.entrySet()) { - LitematicaBlockStateContainer container = schematic.getSubRegionContainer(entry.getKey()); - BlockPos first = entry.getValue().getPos1(); - BlockPos second = entry.getValue().getPos2(); - if (container == null || first == null || second == null) { - continue; - } - Vec3i regionSize = container.getSize(); - int originX = Math.min(first.getX(), second.getX()); - int originY = Math.min(first.getY(), second.getY()); - int originZ = Math.min(first.getZ(), second.getZ()); - // 遍历容器内所有方块,只保留非空气方块 - LitematicaBitArray storage = container.getArray(); - ILitematicaBlockStatePalette palette = container.getPalette(); - boolean[] airIds = findAirIds(palette); - long storageIndex = 0L; - for (int y = 0; y < regionSize.getY(); y++) { - for (int z = 0; z < regionSize.getZ(); z++) { - for (int x = 0; x < regionSize.getX(); x++) { - int paletteId = storage.getAt(storageIndex++); - if (paletteId < 0 || paletteId >= airIds.length || !airIds[paletteId]) { - BlockState state = palette.getBlockState(paletteId); - if (state == null || state.isAir()) { - continue; - } - // 绝对坐标 -> 相对坐标:减去全局包围盒原点 - int relativeX = x + originX - minX; - int relativeY = y + originY - minY; - int relativeZ = z + originZ - minZ; - long position = packPosition(relativeX, relativeY, relativeZ); - // putIfAbsent 防止重叠区域重复添加同一位置的方块 - if (statesByPosition.putIfAbsent(position, state) == null) { - blocks.add(new Block(relativeX, relativeY, relativeZ, state)); - } - } - } - } - } - } - - // 包围盒尺寸 = 远端 - 原点,中心 = 尺寸 * 0.5(几何中心) - Vec3i size = new Vec3i(maxXExclusive - minX, maxYExclusive - minY, maxZExclusive - minZ); - return new SchematicPreviewModel(size, size.getX() * 0.5F, size.getY() * 0.5F, size.getZ() * 0.5F, - blocks, statesByPosition); - } - - private static SchematicPreviewModel empty() { - return new SchematicPreviewModel(BlockPos.ZERO, 0.0F, 0.0F, 0.0F, List.of(), Map.of()); - } - - private static boolean[] findAirIds(ILitematicaBlockStatePalette palette) { - boolean[] airIds = new boolean[palette.getPaletteSize()]; - for (int id = 0; id < airIds.length; id++) { - BlockState state = palette.getBlockState(id); - airIds[id] = state == null || state.isAir(); - } - return airIds; - } - //突然想到一个很神的点子,如果用c/c++或者rust去写计算部分呢?真神人了 - - // 将坐标打包为 long - private static long packPosition(int x, int y, int z) { - return ((long) x << 42) | ((long) y << 21) | z; - } - - // 单个非空气方块及其相对原理图坐标 - public record Block(int x, int y, int z, BlockState state) { - } -} diff --git a/src/main/java/com/listmore/schematic/preview/SchematicPreviewSession.java b/src/main/java/com/listmore/schematic/preview/SchematicPreviewSession.java index de83daa..b04d788 100644 --- a/src/main/java/com/listmore/schematic/preview/SchematicPreviewSession.java +++ b/src/main/java/com/listmore/schematic/preview/SchematicPreviewSession.java @@ -1,12 +1,15 @@ package com.listmore.schematic.preview; +import com.listmore.schematic.preview.model.SchematicPreviewModel; + +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.concurrent.CompletableFuture; +import java.util.Objects; import java.util.concurrent.atomic.AtomicLong; -import com.listmore.schematic.preview.SchematicPreviewLoader.LoadedPreview; +import com.listmore.schematic.preview.SchematicPreviewLoader.LoadRequest; import com.listmore.schematic.preview.render.SchematicPreviewRenderManager; -import net.minecraft.core.Vec3i; // 单个原理图浏览器的预览状态 public final class SchematicPreviewSession { @@ -14,7 +17,9 @@ public final class SchematicPreviewSession { private final SchematicPreviewTransform transform = new SchematicPreviewTransform(); private final SchematicPreviewRenderManager renderer = new SchematicPreviewRenderManager(); private Path file; - private CompletableFuture loadingTask; + private FileStamp fileStamp; + private long nextFileCheckNanos; + private LoadRequest loadRequest; private SchematicPreviewModel model; private Throwable loadFailure; // 后台加载线程写入,GUI绘制线程读取 @@ -34,47 +39,74 @@ public SchematicPreviewModel model() { return this.model; } - public Vec3i size() { - return this.model != null ? this.model.size() : null; - } - public boolean isLoading() { - return this.loadingTask != null && !this.loadingTask.isDone(); + return this.loadRequest != null && !this.loadRequest.future().isDone(); } public boolean hasFailure() { return this.loadFailure != null; } - // 仅在列表中选中新的 litematic 文件时启动新的加载任务 - // 流程:路径规范化 -> 去重检查 -> 重置旧状态 -> 启动异步加载 -> 结果写入 pendingResult + // 仅在文件路径或文件版本变化时启动新的加载任务 // generation 防止旧请求的结果覆盖新请求 public void setFile(Path file) { Path normalizedFile = file.toAbsolutePath().normalize(); - if (this.closed || normalizedFile.equals(this.file)) { + if (this.closed) { + return; + } + long now = System.nanoTime(); + if (normalizedFile.equals(this.file) && now < this.nextFileCheckNanos) { + return; + } + FileStamp stamp = FileStamp.read(normalizedFile); + this.nextFileCheckNanos = now + 1_000_000_000L; + if (normalizedFile.equals(this.file) && Objects.equals(stamp, this.fileStamp)) { return; } // 重置所有状态,准备新文件加载 + if (this.loadRequest != null) { + this.loadRequest.cancel(); + } this.file = normalizedFile; + this.fileStamp = stamp; this.model = null; - this.renderer.close(); + this.renderer.clearModel(); this.loadFailure = null; this.pendingResult = null; this.transform.reset(); // 递增 generation 使旧请求的回调失效 long requestGeneration = this.generation.incrementAndGet(); - CompletableFuture task = SchematicPreviewLoader.load(normalizedFile); - this.loadingTask = task; - task.whenComplete((loaded, throwable) -> { + LoadRequest request = SchematicPreviewLoader.load(normalizedFile); + this.loadRequest = request; + request.future().whenComplete((loaded, throwable) -> { // 检查:未关闭 + generation 匹配 + 未被新任务替换 - if (this.closed || requestGeneration != this.generation.get() || task != this.loadingTask) { + if (this.closed || requestGeneration != this.generation.get() || request != this.loadRequest) { return; } this.pendingResult = new LoadResult(requestGeneration, loaded, throwable); }); } + // 预览区域暂时不可见时释放模型和网格,但保留可复用的渲染后端资源 + public void clear() { + if (this.file == null && this.model == null && this.loadRequest == null) { + return; + } + this.generation.incrementAndGet(); + if (this.loadRequest != null) { + this.loadRequest.cancel(); + } + this.file = null; + this.fileStamp = null; + this.nextFileCheckNanos = 0L; + this.loadRequest = null; + this.pendingResult = null; + this.model = null; + this.loadFailure = null; + this.renderer.clearModel(); + } + // 在 GUI 绘制线程调用,消费后台加载线程写入的 pendingResult // 检查 generation 确保只处理最新请求的结果,忽略已过期的加载任务 // 加载成功后将后台生成的模型提交给渲染器 @@ -85,34 +117,51 @@ public void update() { } this.pendingResult = null; + if (this.loadRequest != null) { + this.loadRequest.cancel(); + this.loadRequest = null; + } if (result.throwable() != null) { this.loadFailure = result.throwable(); return; } - if (result.preview() == null) { + if (result.model() == null) { this.loadFailure = new IllegalStateException("Litematica returned no preview"); return; } try { // 加载成功 -> 提交模型给渲染器 - this.model = result.preview().model(); - this.renderer.setSchematic(result.preview().schematic()); - this.renderer.setModel(this.model); + this.model = result.model(); + this.renderer.modelChanged(); } catch (Throwable throwable) { this.loadFailure = throwable; } } + // clear() 保留渲染后端以便复用;close() 同时销毁后端持有的 GPU 资源 public void close() { this.closed = true; this.generation.incrementAndGet(); - this.loadingTask = null; + if (this.loadRequest != null) { + this.loadRequest.cancel(); + } + this.loadRequest = null; this.pendingResult = null; this.model = null; this.renderer.close(); } - private record LoadResult(long generation, LoadedPreview preview, Throwable throwable) { + private record LoadResult(long generation, SchematicPreviewModel model, Throwable throwable) { + } + + private record FileStamp(long modifiedTime, long size) { + private static FileStamp read(Path file) { + try { + return new FileStamp(Files.getLastModifiedTime(file).toMillis(), Files.size(file)); + } catch (IOException ignored) { + return null; + } + } } } diff --git a/src/main/java/com/listmore/schematic/preview/gui/SchematicPreviewOverlay.java b/src/main/java/com/listmore/schematic/preview/gui/SchematicPreviewOverlay.java index 71b72e6..3f3dedf 100644 --- a/src/main/java/com/listmore/schematic/preview/gui/SchematicPreviewOverlay.java +++ b/src/main/java/com/listmore/schematic/preview/gui/SchematicPreviewOverlay.java @@ -37,9 +37,11 @@ public static void draw( } else if (session.hasFailure()) { drawCentered(context, layout, StringUtils.translate("listmore.schematic_preview.failed")); } else if (session.model() != null) { - boolean rendered = session.renderer().render(context, layout, session.transform()); - if (!rendered) { - drawModelPlaceholder(context, layout, session.model().blocks().size()); + boolean rendered = session.renderer().render(context, layout, session.transform(), session.model()); + if (session.renderer().hasFailure()) { + drawCentered(context, layout, StringUtils.translate("listmore.schematic_preview.failed")); + } else if (!rendered) { + drawModelPlaceholder(context, layout, session.model().blockCount()); } } else { drawCentered(context, layout, StringUtils.translate("listmore.schematic_preview.empty")); diff --git a/src/main/java/com/listmore/schematic/preview/model/SchematicPreviewModel.java b/src/main/java/com/listmore/schematic/preview/model/SchematicPreviewModel.java new file mode 100644 index 0000000..69fbc3c --- /dev/null +++ b/src/main/java/com/listmore/schematic/preview/model/SchematicPreviewModel.java @@ -0,0 +1,91 @@ +package com.listmore.schematic.preview.model; + +import java.util.List; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Vec3i; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; + +public final class SchematicPreviewModel { + // 统一包围盒的全局尺寸;所有 Section 坐标均相对于该包围盒原点 + private final Vec3i size; + private final SchematicPreviewSectionStorage sections; + + SchematicPreviewModel(Vec3i size, SchematicPreviewSectionStorage sections) { + this.size = size; + this.sections = sections; + } + + public Vec3i size() { + return this.size; + } + + public List
sections() { + return this.sections.values(); + } + + public int blockCount() { + return this.sections.blockCount(); + } + + public boolean isEmpty() { + return this.sections.isEmpty(); + } + + public BlockState blockStateAt(int x, int y, int z) { + if (x < 0 || y < 0 || z < 0 || x >= this.size.getX() || y >= this.size.getY() || z >= this.size.getZ()) { + return Blocks.AIR.defaultBlockState(); + } + return this.sections.blockStateAt(x, y, z); + } + + static SchematicPreviewModel empty() { + return new SchematicPreviewModel(BlockPos.ZERO, SchematicPreviewSectionStorage.empty()); + } + + public static final class Section { + private final int sectionX; + private final int sectionY; + private final int sectionZ; + // 空气不存入数组,null 表示该局部位置为空气。 + private final BlockState[] states; + private final int blockCount; + + Section(int sectionX, int sectionY, int sectionZ, BlockState[] states) { + this.sectionX = sectionX; + this.sectionY = sectionY; + this.sectionZ = sectionZ; + this.states = states; + int count = 0; + for (BlockState state : states) { + if (state != null) { + count++; + } + } + this.blockCount = count; + } + + public int sectionX() { return this.sectionX; } + public int sectionY() { return this.sectionY; } + public int sectionZ() { return this.sectionZ; } + public int minX() { return this.sectionX << 4; } + public int minY() { return this.sectionY << 4; } + public int minZ() { return this.sectionZ << 4; } + public int blockCount() { return this.blockCount; } + + public BlockState stateAt(int x, int y, int z) { + BlockState state = this.states[localIndex(x, y, z)]; + return state != null ? state : Blocks.AIR.defaultBlockState(); + } + + public BlockState stateAtIndex(int index) { + return this.states[index]; + } + } + + static int localIndex(int x, int y, int z) { + // 固定布局:x 为最低 4 位,随后是 z,最高 4 位为 y + return (y << 8) | (z << 4) | x; + } +} diff --git a/src/main/java/com/listmore/schematic/preview/model/SchematicPreviewScanner.java b/src/main/java/com/listmore/schematic/preview/model/SchematicPreviewScanner.java new file mode 100644 index 0000000..8ff19e7 --- /dev/null +++ b/src/main/java/com/listmore/schematic/preview/model/SchematicPreviewScanner.java @@ -0,0 +1,303 @@ +package com.listmore.schematic.preview.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.function.BooleanSupplier; + +import fi.dy.masa.litematica.schematic.LitematicaSchematic; +import fi.dy.masa.litematica.schematic.container.ILitematicaBlockStatePalette; +import fi.dy.masa.litematica.schematic.container.LitematicaBitArray; +import fi.dy.masa.litematica.schematic.container.LitematicaBlockStateContainer; +import fi.dy.masa.litematica.selection.Box; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Vec3i; +import net.minecraft.world.level.block.state.BlockState; + +// 将 Litematica 数据按 16^3 Section 扫描并合并为预览模型。 +public final class SchematicPreviewScanner { + private static final int SECTION_SIZE = 16; + private static final int SECTION_VOLUME = SECTION_SIZE * SECTION_SIZE * SECTION_SIZE; + private static final int TASKS_PER_WORKER = 2; + private static final int MIN_PARALLEL_SECTIONS = 8; + private static final long SERIAL_SCAN_VOLUME_THRESHOLD = (long) SECTION_VOLUME * MIN_PARALLEL_SECTIONS; + + private SchematicPreviewScanner() { + } + + public static SchematicPreviewModel scan(LitematicaSchematic schematic, ExecutorService executor, + int workerCount, BooleanSupplier cancelled) { + List regions = collectRegions(schematic); + if (regions.isEmpty()) { + return SchematicPreviewModel.empty(); + } + + Bounds bounds = findBounds(regions); + Map mergedSections = new LinkedHashMap<>(); + for (RegionSource region : regions) { + checkCancelled(cancelled); + scanRegion(region.relativeTo(bounds.minX(), bounds.minY(), bounds.minZ()), executor, + Math.max(1, workerCount), cancelled, mergedSections); + } + + Map sections = new LinkedHashMap<>(capacityFor(mergedSections.size())); + mergedSections.forEach((position, data) -> sections.put(position, + new SchematicPreviewModel.Section(data.sectionX(), data.sectionY(), data.sectionZ(), data.states()))); + Vec3i size = new Vec3i(bounds.maxXExclusive() - bounds.minX(), + bounds.maxYExclusive() - bounds.minY(), bounds.maxZExclusive() - bounds.minZ()); + return new SchematicPreviewModel(size, new SchematicPreviewSectionStorage(sections)); + } + + private static List collectRegions(LitematicaSchematic schematic) { + List regions = new ArrayList<>(); + for (Map.Entry entry : schematic.getAreas().entrySet()) { + LitematicaBlockStateContainer container = schematic.getSubRegionContainer(entry.getKey()); + BlockPos first = entry.getValue().getPos1(); + BlockPos second = entry.getValue().getPos2(); + if (container == null || first == null || second == null) { + continue; + } + Vec3i size = container.getSize(); + regions.add(new RegionSource( + Math.min(first.getX(), second.getX()), Math.min(first.getY(), second.getY()), + Math.min(first.getZ(), second.getZ()), size.getX(), size.getY(), size.getZ(), + container.getArray(), readPalette(container.getPalette()))); + } + return regions; + } + + private static Bounds findBounds(List regions) { + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int minZ = Integer.MAX_VALUE; + int maxXExclusive = Integer.MIN_VALUE; + int maxYExclusive = Integer.MIN_VALUE; + int maxZExclusive = Integer.MIN_VALUE; + for (RegionSource region : regions) { + minX = Math.min(minX, region.originX()); + minY = Math.min(minY, region.originY()); + minZ = Math.min(minZ, region.originZ()); + maxXExclusive = Math.max(maxXExclusive, region.originX() + region.sizeX()); + maxYExclusive = Math.max(maxYExclusive, region.originY() + region.sizeY()); + maxZExclusive = Math.max(maxZExclusive, region.originZ() + region.sizeZ()); + } + return new Bounds(minX, minY, minZ, maxXExclusive, maxYExclusive, maxZExclusive); + } + + private static BlockState[] readPalette(ILitematicaBlockStatePalette palette) { + BlockState[] states = new BlockState[palette.getPaletteSize()]; + for (int id = 0; id < states.length; id++) { + BlockState state = palette.getBlockState(id); + states[id] = state == null || state.isAir() ? null : state; + } + return states; + } + + private static void scanRegion(RegionSource region, ExecutorService executor, int workerCount, + BooleanSupplier cancelled, Map mergedSections) { + SectionCursor cursor = new SectionCursor(region); + if (workerCount <= 1 || cursor.sectionCount() < MIN_PARALLEL_SECTIONS + || region.volume() <= SERIAL_SCAN_VOLUME_THRESHOLD) { + scanRegionSerial(region, cursor, cancelled, mergedSections); + return; + } + + // 只维持有限数量的在途任务 + // 完成顺序无需稳定,合并会按 Section 坐标处理重叠区域 + ExecutorCompletionService completion = new ExecutorCompletionService<>(executor); + List> submitted = new ArrayList<>(); + int taskLimit = Math.max(1, workerCount * TASKS_PER_WORKER); + int inFlight = 0; + try { + while (cursor.hasNext() && inFlight < taskLimit) { + submitted.add(submitSection(completion, region, cursor.next(), cancelled)); + inFlight++; + } + while (inFlight > 0) { + checkCancelled(cancelled); + mergeSection(completion.take().get(), mergedSections); + inFlight--; + if (cursor.hasNext()) { + submitted.add(submitSection(completion, region, cursor.next(), cancelled)); + inFlight++; + } + } + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new CancellationException("Schematic preview scan interrupted"); + } catch (ExecutionException exception) { + throw propagate(exception.getCause()); + } finally { + submitted.forEach(future -> future.cancel(true)); + } + } + + private static Future submitSection(ExecutorCompletionService completion, + RegionSource region, SectionCoordinates coordinates, BooleanSupplier cancelled) { + return completion.submit(() -> scanSection(region, coordinates.x(), coordinates.y(), coordinates.z(), cancelled)); + } + + private static void scanRegionSerial(RegionSource region, SectionCursor cursor, BooleanSupplier cancelled, + Map mergedSections) { + while (cursor.hasNext()) { + checkCancelled(cancelled); + SectionCoordinates coordinates = cursor.next(); + mergeSection(scanSection(region, coordinates.x(), coordinates.y(), coordinates.z(), cancelled), + mergedSections); + } + } + + private static void mergeSection(SectionData scanned, Map mergedSections) { + if (scanned.states() == null) { + return; + } + long key = SchematicPreviewSectionStorage.packPosition( + scanned.sectionX(), scanned.sectionY(), scanned.sectionZ()); + SectionData target = mergedSections.get(key); + if (target == null) { + mergedSections.put(key, scanned); + return; + } + for (int index = 0; index < SECTION_VOLUME; index++) { + if (target.states()[index] == null && scanned.states()[index] != null) { + target.states()[index] = scanned.states()[index]; + } + } + } + + private static SectionData scanSection(RegionSource region, int sectionX, int sectionY, int sectionZ, + BooleanSupplier cancelled) { + int sectionMinX = sectionX << 4; + int sectionMinY = sectionY << 4; + int sectionMinZ = sectionZ << 4; + int minX = Math.max(sectionMinX, region.originX()); + int minY = Math.max(sectionMinY, region.originY()); + int minZ = Math.max(sectionMinZ, region.originZ()); + int maxX = Math.min(sectionMinX + SECTION_SIZE, region.originX() + region.sizeX()); + int maxY = Math.min(sectionMinY + SECTION_SIZE, region.originY() + region.sizeY()); + int maxZ = Math.min(sectionMinZ + SECTION_SIZE, region.originZ() + region.sizeZ()); + BlockState[] states = null; + int checked = 0; + int sizeLayer = region.sizeX() * region.sizeZ(); + + for (int y = minY; y < maxY; y++) { + int localY = y - region.originY(); + for (int z = minZ; z < maxZ; z++) { + int localZ = z - region.originZ(); + long storageIndex = (long) localY * sizeLayer + (long) localZ * region.sizeX() + + minX - region.originX(); + for (int x = minX; x < maxX; x++, storageIndex++) { + if ((checked++ & 255) == 0) { + checkCancelled(cancelled); + } + int paletteId = region.storage().getAt(storageIndex); + if (paletteId < 0 || paletteId >= region.palette().length) { + continue; + } + BlockState state = region.palette()[paletteId]; + if (state == null) { + continue; + } + if (states == null) { + states = new BlockState[SECTION_VOLUME]; + } + // Section 内固定为 16 x 16 x 16,索引布局由 Model 和 Renderer 共同使用。 + states[SchematicPreviewModel.localIndex(x & 15, y & 15, z & 15)] = state; + } + } + } + return new SectionData(sectionX, sectionY, sectionZ, states); + } + + private static RuntimeException propagate(Throwable cause) { + if (cause instanceof CancellationException cancellation) { + return cancellation; + } + if (cause instanceof RuntimeException runtime) { + return runtime; + } + return new IllegalStateException("Failed to scan schematic preview section", cause); + } + + private static void checkCancelled(BooleanSupplier cancelled) { + if (Thread.currentThread().isInterrupted() || cancelled.getAsBoolean()) { + throw new CancellationException("Schematic preview load cancelled"); + } + } + + private static int capacityFor(int size) { + return Math.max(16, (int) Math.ceil(size / 0.75D)); + } + + private static final class SectionCursor { + private final int minSectionX; + private final int minSectionZ; + private final int maxSectionX; + private final int maxSectionY; + private final int maxSectionZ; + private int sectionX; + private int sectionY; + private int sectionZ; + + private SectionCursor(RegionSource region) { + this.minSectionX = region.originX() >> 4; + this.minSectionZ = region.originZ() >> 4; + this.maxSectionX = (region.originX() + region.sizeX() - 1) >> 4; + this.maxSectionY = (region.originY() + region.sizeY() - 1) >> 4; + this.maxSectionZ = (region.originZ() + region.sizeZ() - 1) >> 4; + this.sectionX = this.minSectionX; + this.sectionY = region.originY() >> 4; + this.sectionZ = this.minSectionZ; + } + + private boolean hasNext() { + return this.sectionY <= this.maxSectionY; + } + + private long sectionCount() { + return (long) (this.maxSectionX - this.minSectionX + 1) + * (this.maxSectionY - this.sectionY + 1) + * (this.maxSectionZ - this.minSectionZ + 1); + } + + private SectionCoordinates next() { + SectionCoordinates coordinates = new SectionCoordinates(this.sectionX, this.sectionY, this.sectionZ); + if (++this.sectionX > this.maxSectionX) { + this.sectionX = this.minSectionX; + if (++this.sectionZ > this.maxSectionZ) { + this.sectionZ = this.minSectionZ; + this.sectionY++; + } + } + return coordinates; + } + } + + private record Bounds(int minX, int minY, int minZ, int maxXExclusive, int maxYExclusive, + int maxZExclusive) { + } + + private record RegionSource(int originX, int originY, int originZ, int sizeX, int sizeY, int sizeZ, + LitematicaBitArray storage, BlockState[] palette) { + private long volume() { + return (long) this.sizeX * this.sizeY * this.sizeZ; + } + + private RegionSource relativeTo(int minX, int minY, int minZ) { + return new RegionSource(this.originX - minX, this.originY - minY, this.originZ - minZ, + this.sizeX, this.sizeY, this.sizeZ, this.storage, this.palette); + } + } + + private record SectionData(int sectionX, int sectionY, int sectionZ, BlockState[] states) { + } + + private record SectionCoordinates(int x, int y, int z) { + } +} diff --git a/src/main/java/com/listmore/schematic/preview/model/SchematicPreviewSectionStorage.java b/src/main/java/com/listmore/schematic/preview/model/SchematicPreviewSectionStorage.java new file mode 100644 index 0000000..e36e1a9 --- /dev/null +++ b/src/main/java/com/listmore/schematic/preview/model/SchematicPreviewSectionStorage.java @@ -0,0 +1,49 @@ +package com.listmore.schematic.preview.model; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; + +// 同一批 Section 的遍历视图和坐标索引 +// List 保留扫描顺序供渲染遍历,Map 支持按世界相对坐标快速查询 +final class SchematicPreviewSectionStorage { + private final List values; + private final Map byPosition; + private final int blockCount; + + SchematicPreviewSectionStorage(Map sections) { + this.byPosition = Collections.unmodifiableMap(new LinkedHashMap<>(sections)); + this.values = List.copyOf(this.byPosition.values()); + this.blockCount = this.values.stream().mapToInt(SchematicPreviewModel.Section::blockCount).sum(); + } + + List values() { + return this.values; + } + + int blockCount() { + return this.blockCount; + } + + boolean isEmpty() { + return this.values.isEmpty(); + } + + BlockState blockStateAt(int x, int y, int z) { + SchematicPreviewModel.Section section = this.byPosition.get(packPosition(x >> 4, y >> 4, z >> 4)); + return section != null ? section.stateAt(x & 15, y & 15, z & 15) : Blocks.AIR.defaultBlockState(); + } + + static long packPosition(int x, int y, int z) { + // 此编码是 Scanner 合并 Section 与模型查询的共同键格式。 + return ((long) x << 42) | ((long) y << 21) | z; + } + + static SchematicPreviewSectionStorage empty() { + return new SchematicPreviewSectionStorage(Map.of()); + } +} diff --git a/src/main/java/com/listmore/schematic/preview/render/SchematicPreviewRenderBackend.java b/src/main/java/com/listmore/schematic/preview/render/SchematicPreviewRenderBackend.java index f0cba5e..7aa0517 100644 --- a/src/main/java/com/listmore/schematic/preview/render/SchematicPreviewRenderBackend.java +++ b/src/main/java/com/listmore/schematic/preview/render/SchematicPreviewRenderBackend.java @@ -1,11 +1,16 @@ package com.listmore.schematic.preview.render; -import com.listmore.schematic.preview.SchematicPreviewModel; +import com.listmore.schematic.preview.model.SchematicPreviewModel; import com.listmore.schematic.preview.SchematicPreviewTransform; import com.listmore.schematic.preview.gui.SchematicPreviewLayout; // 原理图预览的版本专属渲染后端,公共预览逻辑只通过这个接口调用 public interface SchematicPreviewRenderBackend extends AutoCloseable { - default void setSchematic(Object schematic) { + // clearModel 只清除当前模型的网格并保留后端, close 才会彻底释放后端资源 + default void clearModel() { + } + + default boolean hasFailure() { + return false; } // 将当前快照绘制到预览区域;无法绘制时返回false diff --git a/src/main/java/com/listmore/schematic/preview/render/SchematicPreviewRenderManager.java b/src/main/java/com/listmore/schematic/preview/render/SchematicPreviewRenderManager.java index 0023be9..40a2979 100644 --- a/src/main/java/com/listmore/schematic/preview/render/SchematicPreviewRenderManager.java +++ b/src/main/java/com/listmore/schematic/preview/render/SchematicPreviewRenderManager.java @@ -1,6 +1,6 @@ package com.listmore.schematic.preview.render; -import com.listmore.schematic.preview.SchematicPreviewModel; +import com.listmore.schematic.preview.model.SchematicPreviewModel; import com.listmore.schematic.preview.SchematicPreviewTransform; import com.listmore.schematic.preview.gui.SchematicPreviewLayout; @@ -11,32 +11,14 @@ //#endif public final class SchematicPreviewRenderManager implements AutoCloseable { - private SchematicPreviewModel model; - private Object schematic; private long modelRevision; private final SchematicPreviewRenderBackend backend = createBackend(); - // 提交新的原理图快照,模型只会在 GUI 线程调用此方法 - public void setModel(SchematicPreviewModel model) { - if (this.model == model) { - return; - } - this.model = model; + // 通知后端模型快照已变化 + public void modelChanged() { this.modelRevision++; } - public void setSchematic(Object schematic) { - this.schematic = schematic; - if (this.backend != null) { - this.backend.setSchematic(schematic); - } - } - - // 返回当前快照的版本号 - public long modelRevision() { - return this.modelRevision; - } - // 将预览模型绘制到右侧信息面板 // 版本差异由渲染后端处理:通过接口委托给版本专属的 SchematicPreviewRenderer 实现 public boolean render( @@ -45,26 +27,31 @@ public boolean render( //#else GuiGraphics context, //#endif - SchematicPreviewLayout layout, SchematicPreviewTransform transform) { - return this.backend != null && this.backend.render(context, layout, transform, this.model, this.modelRevision); + SchematicPreviewLayout layout, SchematicPreviewTransform transform, + SchematicPreviewModel model) { + return this.backend != null && this.backend.render(context, layout, transform, model, this.modelRevision); + } + + public void clearModel() { + if (this.backend != null) { + this.backend.clearModel(); + } + this.modelRevision++; } - public SchematicPreviewModel model() { - return this.model; + public boolean hasFailure() { + return this.backend != null && this.backend.hasFailure(); } - // 关闭预览时释放渲染资源 @Override public void close() { if (this.backend != null) { this.backend.close(); } - this.model = null; - this.schematic = null; this.modelRevision++; } - // 通过反射加载版本专属的渲染后端,避免编译时硬依赖 + // 通过反射加载版本专属的渲染后端 // 如果当前版本没有对应的 SchematicPreviewRenderer 类,回退到 null(GUI 占位预览) private static SchematicPreviewRenderBackend createBackend() { try { diff --git a/src/main/java/com/listmore/schematic/preview/render/SchematicPreviewRenderer.java b/src/main/java/com/listmore/schematic/preview/render/SchematicPreviewRenderer.java index b1b3f9c..a7b7a00 100644 --- a/src/main/java/com/listmore/schematic/preview/render/SchematicPreviewRenderer.java +++ b/src/main/java/com/listmore/schematic/preview/render/SchematicPreviewRenderer.java @@ -1,14 +1,13 @@ package com.listmore.schematic.preview.render; import com.listmore.ListMore; -import com.listmore.schematic.preview.SchematicPreviewModel; +import com.listmore.schematic.preview.model.SchematicPreviewModel; import com.listmore.schematic.preview.SchematicPreviewTransform; import com.listmore.schematic.preview.gui.SchematicPreviewLayout; import java.util.ArrayList; import java.util.Comparator; import java.util.EnumMap; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -45,7 +44,6 @@ //#if MC >= 26.1 //$$ import fi.dy.masa.litematica.render.schematic.BlockModelRendererSchematic; //$$ import fi.dy.masa.litematica.render.schematic.IBlockOutputSchematic; -//$$ import fi.dy.masa.litematica.schematic.LitematicaSchematic; //#else import com.mojang.blaze3d.vertex.PoseStack; import net.minecraft.client.renderer.ItemBlockRenderTypes; @@ -96,11 +94,19 @@ import org.lwjgl.system.MemoryStack; public final class SchematicPreviewRenderer implements SchematicPreviewRenderBackend { + private static final long BUILD_BUDGET_NANOS = 2_000_000L; + private static final int MAX_SECTIONS_PER_FRAME = 2; + private static final long SMALL_MODEL_BUILD_BUDGET_NANOS = 8_000_000L; + private static final int SMALL_MODEL_MAX_SECTIONS_PER_FRAME = 8; + private static final int SMALL_MODEL_MAX_BLOCKS = 4_096; + private static final int MAX_BUILD_ATTEMPTS = 3; + // 每个模型 Section 对应一个独立 GPU 网格 private final List meshes = new ArrayList<>(); + private List pendingSections = List.of(); + private int nextSection; + private long nextBuildAttemptNanos; + private int buildFailures; private long builtRevision = Long.MIN_VALUE; - //#if MC >= 26.1 - //$$ private LitematicaSchematic schematic; - //#endif private SchematicPreviewWorld world; private TextureTarget target; //#if MC >= 1.21.11 @@ -120,13 +126,6 @@ public final class SchematicPreviewRenderer implements SchematicPreviewRenderBac //#if MC >= 26.1 //$$ private final FluidRenderer fluidRenderer = new FluidRenderer(Minecraft.getInstance().getModelManager().getFluidStateModelSet()); //#endif - @Override - public void setSchematic(Object schematic) { - //#if MC >= 26.1 - //$$ this.schematic = schematic instanceof LitematicaSchematic loaded ? loaded : null; - //#endif - } - // 将原理图模型渲染到离屏帧缓冲,再将结果贴图绘制到 GUI 面板 // 流程:校验模型 -> 确保帧缓冲尺寸 -> 增量重建网格 -> 清空帧缓冲 -> 3D 渲染 -> 贴图到 GUI public boolean render(Object rawContext, SchematicPreviewLayout layout, SchematicPreviewTransform transform, @@ -136,15 +135,15 @@ public boolean render(Object rawContext, SchematicPreviewLayout layout, Schemati //#else GuiGraphics context = (GuiGraphics) rawContext; //#endif - if (model == null || model.blocks().isEmpty() || layout.contentWidth() <= 0 || layout.contentHeight() <= 0) { + if (model == null || model.isEmpty() || layout.contentWidth() <= 0 || layout.contentHeight() <= 0) { return false; } // 确保离屏帧缓冲尺寸与当前预览区域匹配 this.ensureTarget(layout.contentWidth(), layout.contentHeight()); - // 仅当模型快照版本号变化时才重建网格,避免每帧重建 + // 模型变化时初始化构建队列,之后每帧只消耗固定时间 if (this.builtRevision != revision) { try { - this.rebuild(model, revision); + this.beginRebuild(model, revision); this.rebuildFailureLogged = false; } catch (Throwable throwable) { if (!this.rebuildFailureLogged) { @@ -154,6 +153,25 @@ public boolean render(Object rawContext, SchematicPreviewLayout layout, Schemati return false; } } + if (this.hasFailure()) { + return false; + } + int previousSection = this.nextSection; + try { + this.buildPendingSections(model); + if (this.nextSection > previousSection) { + this.rebuildFailureLogged = false; + } + } catch (Throwable throwable) { + if (!this.rebuildFailureLogged) { + ListMore.LOGGER.error("Failed to build schematic preview section", throwable); + this.rebuildFailureLogged = true; + } + this.closeBuiltMeshes(); + this.nextSection = 0; + this.buildFailures++; + this.nextBuildAttemptNanos = System.nanoTime() + 250_000_000L; + } if (this.meshes.isEmpty()) { return false; } @@ -218,37 +236,78 @@ private void ensureTarget(int width, int height) { //#endif } - // 从原理图模型重建所有网格数据并上传到 GPU - // 流程:释放旧网格 -> 创建虚拟世界视图 -> 遍历方块按区块分组 -> 调用原版渲染器生成网格 -> 构建 GPU 缓冲区 - private void rebuild(SchematicPreviewModel model, long revision) { + // 初始化增量构建 + private void beginRebuild(SchematicPreviewModel model, long revision) { this.closeMeshes(); - this.builtRevision = revision; - //#if MC >= 26.1 - //$$ if (this.schematic == null) { - //$$ return; - //$$ } - //#endif + this.nextSection = 0; + this.nextBuildAttemptNanos = 0L; + this.buildFailures = 0; if (this.world == null) { this.world = new SchematicPreviewWorld(Minecraft.getInstance()); } this.world.setModel(model); + float centerX = model.size().getX() * 0.5F; + float centerY = model.size().getY() * 0.5F; + float centerZ = model.size().getZ() * 0.5F; + this.pendingSections = new ArrayList<>(model.sections()); + this.pendingSections.sort(Comparator.comparingDouble(section -> { + float dx = section.minX() + 8.0F - centerX; + float dy = section.minY() + 8.0F - centerY; + float dz = section.minZ() + 8.0F - centerZ; + return dx * dx + dy * dy + dz * dz; + })); + this.builtRevision = revision; + } + + private void buildPendingSections(SchematicPreviewModel model) { + if (this.nextSection >= this.pendingSections.size() + || System.nanoTime() < this.nextBuildAttemptNanos) { + return; + } + // 小模型优先在一帧完成,大模型受数量和时间预算限制以避免卡住 GUI 绘制 + boolean buildSmallModelImmediately = model.blockCount() <= SMALL_MODEL_MAX_BLOCKS + && this.pendingSections.size() <= SMALL_MODEL_MAX_SECTIONS_PER_FRAME; + long deadline = System.nanoTime() + (buildSmallModelImmediately + ? SMALL_MODEL_BUILD_BUDGET_NANOS : BUILD_BUDGET_NANOS); + int maxSections = buildSmallModelImmediately + ? SMALL_MODEL_MAX_SECTIONS_PER_FRAME : MAX_SECTIONS_PER_FRAME; + int builtThisFrame = 0; + do { + SchematicPreviewModel.Section section = this.pendingSections.get(this.nextSection); + ChunkMesh mesh = this.buildSection(section); + if (mesh != null) { + this.meshes.add(mesh); + } + this.nextSection++; + builtThisFrame++; + } while (this.nextSection < this.pendingSections.size() + && builtThisFrame < maxSections && System.nanoTime() < deadline); + } + + // 单个 Section 独立网格化并上传 + private ChunkMesh buildSection(SchematicPreviewModel.Section section) { SchematicPreviewWorld view = this.world; //#if MC >= 26.1 //$$ ModelManager modelManager = Minecraft.getInstance().getModelManager(); //#endif - // 按区块位置分组构建网格,每个 ChunkPos 对应一个 ChunkMeshBuilder - Map chunks = new HashMap<>(); + ChunkMeshBuilder chunk = new ChunkMeshBuilder(); //#if MC >= 26.1 //$$ BlockModelRendererSchematic renderer = new BlockModelRendererSchematic(); //$$ renderer.enableCache(); //#endif try { - // 遍历所有非空气方块,分别渲染流体和固体模型 - for (SchematicPreviewModel.Block block : model.blocks()) { - BlockState state = block.state(); - BlockPos position = new BlockPos(block.x(), block.y(), block.z()); - // >> 4 将方块坐标转为区块坐标,computeIfAbsent 保证每个区块只有一个 Builder - ChunkMeshBuilder chunk = chunks.computeIfAbsent(new ChunkPos(block.x() >> 4, block.z() >> 4), ignored -> new ChunkMeshBuilder()); + for (int index = 0; index < 4096; index++) { + BlockState state = section.stateAtIndex(index); + if (state == null) { + continue; + } + int localX = index & 15; + int localZ = (index >> 4) & 15; + int localY = index >> 8; + int blockX = section.minX() + localX; + int blockY = section.minY() + localY; + int blockZ = section.minZ() + localZ; + BlockPos position = new BlockPos(blockX, blockY, blockZ); // 先渲染流体(水、岩浆等),再渲染固体方块模型 //#if MC >= 26.1 //$$ if (!state.getFluidState().isEmpty()) { @@ -266,30 +325,27 @@ private void rebuild(SchematicPreviewModel model, long revision) { //#if MC >= 26.1 //$$ IBlockOutputSchematic output = (x, y, z, quad, instance) -> chunk.builder(quad.materialInfo().layer()) //$$ .putBlockBakedQuad(x, y, z, quad, instance); - //$$ renderer.tessellateBlock(view, state, position, new Vec3(block.x() & 15, block.y(), block.z() & 15), + //$$ renderer.tessellateBlock(view, state, position, new Vec3(localX, blockY, localZ), //$$ modelManager.getBlockStateModelSet().get(state), state.getSeed(position), output); //#else PoseStack pose = new PoseStack(); // & 15 取区块内局部坐标(0-15),模型顶点需要相对区块原点的偏移 - pose.translate(block.x() & 15, block.y(), block.z() & 15); + pose.translate(localX, blockY, localZ); Minecraft.getInstance().getBlockRenderer().renderBatched(state, position, view, pose, chunk.builder(ItemBlockRenderTypes.getChunkRenderType(state)), true, Minecraft.getInstance().getBlockRenderer().getBlockModel(state) .collectParts(RandomSource.create(state.getSeed(position)))); //#endif } + } catch (Throwable throwable) { + chunk.close(); + throw throwable; } finally { //#if MC >= 26.1 //$$ renderer.disableCache(); //#endif } - // 所有方块遍历完毕后,将每个区块的网格数据上传到 GPU - chunks.forEach((position, chunk) -> { - ChunkMesh built = chunk.build(position); - if (built != null) { - this.meshes.add(built); - } - }); + return chunk.build(new ChunkPos(section.sectionX(), section.sectionZ()), section.minY() + 8.0F); } // 设置相机、投影矩阵和全局 Uniform,然后将所有网格渲染到离屏帧缓冲 @@ -304,9 +360,9 @@ private void renderTarget(SchematicPreviewModel model, SchematicPreviewTransform float sinYaw = Mth.sin(yaw); float cosYaw = Mth.cos(yaw); Vector3f camera = new Vector3f( - model.centerX() - sinYaw * horizontal * distance, - model.centerY() - Mth.sin(pitch) * distance, - model.centerZ() + cosYaw * horizontal * distance); + model.size().getX() * 0.5F - sinYaw * horizontal * distance, + model.size().getY() * 0.5F - Mth.sin(pitch) * distance, + model.size().getZ() * 0.5F + cosYaw * horizontal * distance); float panScale = 2.0F * distance * (float) Math.tan(35.0F * Mth.DEG_TO_RAD); Vector3f screenRight = new Vector3f(cosYaw, 0.0F, sinYaw); Vector3f screenUp = new Vector3f(-sinYaw * Mth.sin(pitch), horizontal, @@ -315,32 +371,35 @@ private void renderTarget(SchematicPreviewModel model, SchematicPreviewTransform .add(screenUp.mul(transform.panY() * panScale)); Matrix4fStack modelView = RenderSystem.getModelViewStack(); modelView.pushMatrix(); - // 与 SchematicPreview 的轨道约定一致:UI 偏航角描述观察方向 - // 而地形视图矩阵使用相机偏航角的逆,因此取 -yaw 并共轭 - Quaternionf rotation = new Quaternionf().rotationYXZ(-yaw, pitch, 0.0F).conjugate(); - modelView.set(new Matrix4f().rotation(rotation)); - // 构建透视投影矩阵,近平面 0.05,远平面 4096,FOV 70° - //#if MC >= 26.1 - //$$ this.previewProjection.setupPerspective(0.05F, 4096.0F, 70.0F, - //$$ this.target.width, this.target.height); - //$$ Matrix4f projectionMatrix = this.previewProjection.getMatrix(new Matrix4f()); - //#else - Matrix4f projectionMatrix = new Matrix4f().perspective(70.0F * Mth.DEG_TO_RAD, - (float) this.target.width / this.target.height, 0.05F, 4096.0F); - //#endif - // 备份原版投影矩阵和全局 Uniform,渲染完成后恢复 - RenderSystem.backupProjectionMatrix(); - if (this.projection == null) { + boolean projectionBackedUp = false; + boolean globalUniformCaptured = false; + GpuBuffer previousGlobalUniform = null; + try { + Quaternionf rotation = new Quaternionf().rotationYXZ(-yaw, pitch, 0.0F).conjugate(); + modelView.set(new Matrix4f().rotation(rotation)); + // 构建透视投影矩阵,近平面 0.05,远平面 4096,FOV 70° //#if MC >= 26.1 - //$$ this.projection = new ProjectionMatrixBuffer("ListMore schematic preview"); + //$$ this.previewProjection.setupPerspective(0.05F, 4096.0F, 70.0F, + //$$ this.target.width, this.target.height); + //$$ Matrix4f projectionMatrix = this.previewProjection.getMatrix(new Matrix4f()); //#else - this.projection = new PerspectiveProjectionMatrixBuffer("ListMore schematic preview"); + Matrix4f projectionMatrix = new Matrix4f().perspective(70.0F * Mth.DEG_TO_RAD, + (float) this.target.width / this.target.height, 0.05F, 4096.0F); //#endif - } - RenderSystem.setProjectionMatrix(this.projection.getBuffer(projectionMatrix), ProjectionType.PERSPECTIVE); - GpuBuffer previousGlobalUniform = RenderSystem.getGlobalSettingsUniform(); - this.writeGlobalUniform(camera); - try { + // 备份原版投影矩阵和全局 Uniform,渲染完成后恢复 + RenderSystem.backupProjectionMatrix(); + projectionBackedUp = true; + if (this.projection == null) { + //#if MC >= 26.1 + //$$ this.projection = new ProjectionMatrixBuffer("ListMore schematic preview"); + //#else + this.projection = new PerspectiveProjectionMatrixBuffer("ListMore schematic preview"); + //#endif + } + RenderSystem.setProjectionMatrix(this.projection.getBuffer(projectionMatrix), ProjectionType.PERSPECTIVE); + previousGlobalUniform = RenderSystem.getGlobalSettingsUniform(); + globalUniformCaptured = true; + this.writeGlobalUniform(camera); //#if MC >= 26.2 //$$ Minecraft.getInstance().gameRenderer.lighting().setupFor(Lighting.Entry.LEVEL); //#else @@ -348,8 +407,12 @@ private void renderTarget(SchematicPreviewModel model, SchematicPreviewTransform //#endif this.renderMeshes(camera); } finally { - RenderSystem.setGlobalSettingsUniform(previousGlobalUniform); - RenderSystem.restoreProjectionMatrix(); + if (globalUniformCaptured) { + RenderSystem.setGlobalSettingsUniform(previousGlobalUniform); + } + if (projectionBackedUp) { + RenderSystem.restoreProjectionMatrix(); + } modelView.popMatrix(); } } @@ -439,26 +502,25 @@ private void renderMeshes(Vector3f camera) { } GpuBuffer sharedIndex = maxSequentialIndices == 0 ? null : sequential.getBuffer(maxSequentialIndices); IndexType sharedIndexType = maxSequentialIndices == 0 ? null : sequential.type(); - // 先画不透明层(reverse=false),再画半透明层(reverse=true,从远到近保证混合正确) + // 先画不透明层,再按远到近绘制半透明层 //#if MC >= 1.21.11 //$$ this.renderLayerGroup(orderedMeshes, sectionUniforms, atlas, sharedIndex, sharedIndexType, - //$$ ChunkSectionLayerGroup.OPAQUE, false); + //$$ ChunkSectionLayerGroup.OPAQUE); //$$ this.renderLayerGroup(orderedMeshes, sectionUniforms, atlas, sharedIndex, sharedIndexType, - //$$ ChunkSectionLayerGroup.TRANSLUCENT, true); + //$$ ChunkSectionLayerGroup.TRANSLUCENT); //#else this.renderLayerGroup(orderedMeshes, transformUniforms, atlas, sharedIndex, sharedIndexType, - ChunkSectionLayerGroup.OPAQUE, false); + ChunkSectionLayerGroup.OPAQUE); this.renderLayerGroup(orderedMeshes, transformUniforms, atlas, sharedIndex, sharedIndexType, - ChunkSectionLayerGroup.TRANSLUCENT, true); + ChunkSectionLayerGroup.TRANSLUCENT); //#endif } // 为指定的渲染层组(不透明或半透明)创建 RenderPass 并绘制所有网格 // 每个 ChunkSectionLayer 对应一种渲染管线(如 solid、cutout、translucent) - // reverse 参数控制绘制顺序:半透明层需要从远到近绘制以保证混合正确 private void renderLayerGroup(List orderedMeshes, GpuBufferSlice[] meshUniforms, GpuTextureView atlas, GpuBuffer sharedIndex, IndexType sharedIndexType, - ChunkSectionLayerGroup group, boolean reverse) { + ChunkSectionLayerGroup group) { try (RenderPass pass = RenderSystem.getDevice().createCommandEncoder().createRenderPass( () -> "ListMore schematic preview " + group.label(), this.target.getColorTextureView(), //#if MC >= 26.2 @@ -511,8 +573,6 @@ private void renderLayerGroup(List orderedMeshes, GpuBufferSlice[] me //#endif } if (!draws.isEmpty()) { - // 半透明层需要反转绘制顺序(远 -> 近),不透明层保持原序(近 -> 远,利用深度测试) - if (reverse) draws = draws.reversed(); //#if MC >= 1.21.11 //$$ pass.drawMultipleIndexed(draws, sharedIndex, sharedIndexType, List.of("ChunkSection"), meshUniforms); //#else @@ -524,10 +584,30 @@ private void renderLayerGroup(List orderedMeshes, GpuBufferSlice[] me } private void closeMeshes() { + this.closeBuiltMeshes(); + this.pendingSections = List.of(); + this.nextSection = 0; + this.nextBuildAttemptNanos = 0L; + this.buildFailures = 0; + } + + private void closeBuiltMeshes() { this.meshes.forEach(ChunkMesh::close); this.meshes.clear(); } + @Override + public void clearModel() { + // 仅释放与当前快照关联的网格,离屏目标和通用 Uniform 留给下一次预览复用 + this.closeMeshes(); + this.builtRevision = Long.MIN_VALUE; + } + + @Override + public boolean hasFailure() { + return this.buildFailures >= MAX_BUILD_ATTEMPTS; + } + // 快照变化时重建 GPU 缓冲区 private record SectionBuffers(GpuBuffer vertexBuffer, GpuBuffer indexBuffer, int indexCount, IndexType indexType) implements AutoCloseable { @@ -557,33 +637,43 @@ private BufferBuilder builder(ChunkSectionLayer layer) { } // 将 BufferBuilder 中的网格数据上传到 GPU,创建顶点和索引缓冲区 - // 返回包含所有 GPU 缓冲区和原始 MeshData 的 ChunkMesh - private ChunkMesh build(ChunkPos position) { + // 返回包含各渲染层 GPU 缓冲区的 ChunkMesh + private ChunkMesh build(ChunkPos position, float centerY) { Map buffers = new EnumMap<>(ChunkSectionLayer.class); - List meshData = new ArrayList<>(); - this.builders.forEach((layer, builder) -> { - // BufferBuilder.build() 将 CPU 端顶点数据打包为 MeshData - MeshData mesh = builder.build(); - if (mesh == null) { - return; + try { + for (Map.Entry entry : this.builders.entrySet()) { + // MeshData 上传完成后立即关闭 + // CPU 侧缓冲不跨帧持有 + try (MeshData mesh = entry.getValue().build()) { + if (mesh == null) { + continue; + } + GpuBuffer vertex = RenderSystem.getDevice().createBuffer( + () -> "ListMore preview vertex buffer", + GpuBuffer.USAGE_VERTEX | GpuBuffer.USAGE_COPY_DST, mesh.vertexBuffer()); + GpuBuffer index = null; + try { + index = mesh.indexBuffer() == null ? null : RenderSystem.getDevice().createBuffer( + () -> "ListMore preview index buffer", + GpuBuffer.USAGE_INDEX | GpuBuffer.USAGE_COPY_DST, mesh.indexBuffer()); + buffers.put(entry.getKey(), new SectionBuffers(vertex, index, + mesh.drawState().indexCount(), mesh.drawState().indexType())); + } catch (Throwable throwable) { + vertex.close(); + if (index != null) { + index.close(); + } + throw throwable; + } + } } - meshData.add(mesh); - // 将 MeshData 上传到 GPU,创建顶点和索引缓冲区 - GpuBuffer vertex = RenderSystem.getDevice().createBuffer(() -> "ListMore preview vertex buffer", - GpuBuffer.USAGE_VERTEX | GpuBuffer.USAGE_COPY_DST, mesh.vertexBuffer()); - // 索引缓冲区可能为空(使用共享顺序索引时) - GpuBuffer index = mesh.indexBuffer() == null ? null : RenderSystem.getDevice().createBuffer( - () -> "ListMore preview index buffer", GpuBuffer.USAGE_INDEX | GpuBuffer.USAGE_COPY_DST, - mesh.indexBuffer()); - buffers.put(layer, new SectionBuffers(vertex, index, mesh.drawState().indexCount(), mesh.drawState().indexType())); - }); - if (buffers.isEmpty()) { - // 所有层都为空时释放资源并返回 null - meshData.forEach(MeshData::close); + return buffers.isEmpty() ? null : new ChunkMesh(position, centerY, buffers); + } catch (Throwable throwable) { + buffers.values().forEach(SectionBuffers::close); + throw throwable; + } finally { this.close(); - return null; } - return new ChunkMesh(position, buffers, meshData, new ArrayList<>(this.allocators.values())); } @Override @@ -593,20 +683,18 @@ public void close() { } } - // 一个区块位置的完整网格数据,包含各渲染层的 GPU 缓冲区和原始构建数据 - private record ChunkMesh(ChunkPos chunk, Map buffers, - List meshData, List allocators) implements AutoCloseable { - // 计算区块中心到相机的距离平方,用于排序(远到近) + // 一个 Section 的 GPU 网格数据 + private record ChunkMesh(ChunkPos chunk, float centerY, + Map buffers) implements AutoCloseable { + // 计算 Section 中心到相机的三维距离平方,用于远到近的排序 private double distanceTo(Vector3f camera) { float x = this.chunk.getMinBlockX() + 8.0F; float z = this.chunk.getMinBlockZ() + 8.0F; - return Mth.square(x - camera.x) + Mth.square(z - camera.z); + return Mth.square(x - camera.x) + Mth.square(this.centerY - camera.y) + Mth.square(z - camera.z); } @Override public void close() { - this.allocators.forEach(ByteBufferBuilder::close); - this.meshData.forEach(MeshData::close); this.buffers.values().forEach(SectionBuffers::close); } } @@ -615,9 +703,6 @@ public void close() { public void close() { this.closeMeshes(); this.builtRevision = Long.MIN_VALUE; - //#if MC >= 26.1 - //$$ this.schematic = null; - //#endif this.world = null; if (this.target != null) { this.target.destroyBuffers(); this.target = null; } //#if MC >= 1.21.11 diff --git a/src/main/java/com/listmore/schematic/preview/render/SchematicPreviewWorld.java b/src/main/java/com/listmore/schematic/preview/render/SchematicPreviewWorld.java index 12f6b1a..d0ca3bc 100644 --- a/src/main/java/com/listmore/schematic/preview/render/SchematicPreviewWorld.java +++ b/src/main/java/com/listmore/schematic/preview/render/SchematicPreviewWorld.java @@ -1,6 +1,6 @@ package com.listmore.schematic.preview.render; -import com.listmore.schematic.preview.SchematicPreviewModel; +import com.listmore.schematic.preview.model.SchematicPreviewModel; import fi.dy.masa.litematica.world.FakeLightingProvider; //#if MC >= 26.1