Optimize terrain rendering for large exports
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user