Optimize terrain rendering for large exports

This commit is contained in:
2026-10-04 21:47:08 +05:00
parent 3bdf987916
commit 79d0f53a79
8 changed files with 647 additions and 274 deletions

View File

@@ -56,7 +56,7 @@ export async function renderSnapshot(
throw new Error("Invalid path");
res.setHeader(
"Content-Type",
path.endsWith(".js")
path.endsWith(".js") || path.endsWith(".mjs")
? "text/javascript"
: path.endsWith(".json")
? "application/json"
@@ -92,6 +92,11 @@ export async function renderSnapshot(
await new Promise((r) => server.close(r));
throw new Error("MINEGEO_RENDER_IDLE_SECONDS must be at least 60");
}
const backend = process.env.MINEGEO_RENDER_BACKEND || "auto";
if (!["auto", "software"].includes(backend)) {
await new Promise((r) => server.close(r));
throw new Error("MINEGEO_RENDER_BACKEND must be auto or software");
}
const watchdog = setInterval(() => {
if (Date.now() - lastActivity > idleSeconds * 1000) {
failure = new Error(
@@ -104,20 +109,52 @@ export async function renderSnapshot(
try {
if (signal?.aborted) throw new Error("Rendering cancelled");
await report({ percent: 24, message: "Starting terrain renderer" });
browser = await chromium.launch({
headless: true,
args: ["--use-angle=swiftshader", "--enable-unsafe-swiftshader"],
let page;
let selectedBackend = backend;
async function openRenderer(mode) {
browser = await chromium.launch({
headless: true,
args:
mode === "software"
? ["--use-angle=swiftshader", "--enable-unsafe-swiftshader"]
: ["--enable-gpu", "--enable-unsafe-swiftshader"],
});
if (signal?.aborted || failure)
throw failure || new Error("Rendering cancelled");
const rendererPage = await browser.newPage();
await rendererPage.exposeFunction("minegeoProgress", report);
rendererPage.on("pageerror", (err) =>
console.error("Renderer:", err.message),
);
await rendererPage.goto(`http://127.0.0.1:${server.address().port}`);
await rendererPage.waitForFunction(() => window.rendererReady);
return rendererPage;
}
try {
page = await openRenderer(backend);
} catch (error) {
if (backend === "software" || signal?.aborted || failure || !browser)
throw error;
await browser.close();
console.warn(
"GPU renderer initialization failed; falling back to software rendering.",
);
selectedBackend = "software";
page = await openRenderer("software");
}
const graphics = await page.evaluate(() => window.rendererInfo);
console.log(`MineGeo graphics (${selectedBackend}): ${graphics.renderer}`);
await report({
percent: 24,
message: `Graphics backend: ${graphics.renderer}`,
});
const page = await browser.newPage();
await page.exposeFunction("minegeoProgress", report);
page.on("pageerror", (err) => console.error("Renderer:", err.message));
await page.goto(`http://127.0.0.1:${server.address().port}`);
await page.waitForFunction(() => window.rendererReady);
const stats = await page.evaluate(async () => {
const response = await fetch("/terrain.json");
if (!response.ok) throw new Error("Unable to load terrain snapshot");
return window.buildTerrain(await response.json());
});
stats.graphics = graphics;
stats.backend = selectedBackend;
await report({
percent: 60,
message: `Terrain ready: ${stats.blocks} blocks; rendering ${viewpoints.length} panoramas`,
@@ -131,12 +168,17 @@ export async function renderSnapshot(
({ p, face, size }) => window.renderFace(p, face, size),
{ p, face, size },
);
const png = Buffer.from(data.split(",")[1], "base64");
await sharp(png)
.webp({ quality: 85 })
.toFile(`${output}/${p.id}/${face}.webp`);
const encoded = Buffer.from(data.split(",")[1], "base64");
// Chromium encodes WebP once; retain a PNG fallback for canvas runtimes
// without WebP support rather than silently writing the wrong MIME type.
if (data.startsWith("data:image/webp;"))
await writeFile(`${output}/${p.id}/${face}.webp`, encoded);
else
await sharp(encoded)
.webp({ quality: 85 })
.toFile(`${output}/${p.id}/${face}.webp`);
if (face === "nz")
await sharp(png)
await sharp(encoded)
.resize(512, 512)
.webp({ quality: 76 })
.toFile(`${output}/${p.id}/preview.webp`);
@@ -148,9 +190,11 @@ export async function renderSnapshot(
}
await report({ percent: 85, message: "Rendering the surface map" });
const map = await page.evaluate(() => window.renderMap(1024));
await sharp(Buffer.from(map.split(",")[1], "base64"))
.webp({ quality: 90 })
.toFile(`${output}/map.webp`);
const mapBytes = Buffer.from(map.split(",")[1], "base64");
if (map.startsWith("data:image/webp;"))
await writeFile(`${output}/map.webp`, mapBytes);
else
await sharp(mapBytes).webp({ quality: 90 }).toFile(`${output}/map.webp`);
await report({
percent: 90,
message: "Rendered all panoramas and the surface map",
@@ -166,7 +210,7 @@ export async function renderSnapshot(
} finally {
clearInterval(watchdog);
signal?.removeEventListener("abort", cancel);
await browser?.close();
await browser?.close().catch(() => {});
await new Promise((r) => server.close(r));
}
}