Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2623,7 +2623,8 @@ function handleQuakeDebugRecordingButtonClick(event: Event): void {
function syncQuakeInteractionPresentation(): void {
const menuSurfaceOpen = menu.isMainMenuOpen() || menu.isMenuPanelOpen();
const pointerUnlocked = document.pointerLockElement !== host;
const gameplayPointerUnlocked = quakeGameplayStarted && pointerUnlocked;
const mobileControlsAvailable = quakePointerGameplay.isMobileAvailable();
const gameplayPointerUnlocked = quakeGameplayStarted && pointerUnlocked && !mobileControlsAvailable;
const debugPointerUnlocked = quakeDebugPanelFlow.isModeEnabled() && pointerUnlocked;
const clickToPlayVisible = gameplayPointerUnlocked && !menuSurfaceOpen;
setQuakeClickToPlayPauseState(clickToPlayVisible);
Expand Down Expand Up @@ -4394,7 +4395,7 @@ async function completeQuakeSceneReadiness(
): Promise<void> {
const completeWorldTexturesTask = progress?.startTask("World textures");
try {
await world.waitForVisibleAtlasPages();
await world.waitForVisibleTextures();
} finally {
completeWorldTexturesTask?.();
}
Expand Down
5 changes: 3 additions & 2 deletions src/quake.css
Original file line number Diff line number Diff line change
Expand Up @@ -2398,20 +2398,21 @@ body.quake-menu-unlocked #quake-weapon {
user-select: none;
touch-action: none;
--quake-mobile-control-center-bottom: max(142px, calc(env(safe-area-inset-bottom) + 142px));
--quake-mobile-move-zone-left: max(18px, env(safe-area-inset-left));
}
#quake-mobile-look-zone {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 34vw;
left: max(34vw, calc(var(--quake-mobile-move-zone-left) + 156px));
z-index: 0;
pointer-events: auto;
touch-action: none;
}
#quake-mobile-move-zone {
position: absolute;
left: max(18px, env(safe-area-inset-left));
left: var(--quake-mobile-move-zone-left);
bottom: calc(var(--quake-mobile-control-center-bottom) - 72px);
width: 144px;
height: 144px;
Expand Down
48 changes: 48 additions & 0 deletions src/runtime/renderBundleMesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,29 @@ export async function preloadQuakeRenderBundleAtlasPages(
await preloadQuakeRenderBundleAssetUrls(urls);
}

export async function preloadQuakeRenderBundleElementAssets(
meshElement: HTMLElement,
elements: Iterable<HTMLElement>,
): Promise<void> {
const urls = quakeRenderBundleElementAssetUrls(meshElement, elements);
await preloadQuakeRenderBundleAssetUrls(urls);
}

export function quakeRenderBundleElementAssetUrls(
meshElement: HTMLElement,
elements: Iterable<HTMLElement>,
): string[] {
const urls = new Set<string>();
for (const element of elements) {
collectQuakeRenderBundleInlineAssetUrls(
urls,
element.getAttribute("style") ?? "",
(varName) => meshElement.style.getPropertyValue(varName),
);
}
return [...urls];
}

export interface QuakeRenderBundleDebugOutlineOptions {
hideTextures?: boolean;
}
Expand Down Expand Up @@ -1263,6 +1286,31 @@ export function quakeRenderBundlePreloadAssetUrls(renderBundle: QuakePreparedRen
return [...urls];
}

function collectQuakeRenderBundleInlineAssetUrls(
urls: Set<string>,
styleText: string,
resolveVar: (name: string) => string | undefined,
): void {
if (!styleText) return;
for (const match of styleText.matchAll(/url\(\s*(?:"([^"]*)"|'([^']*)'|([^)]*?))\s*\)/g)) {
const url = normalizeQuakeRenderBundleCssUrl(match[1] ?? match[2] ?? match[3] ?? "");
if (url) urls.add(url);
}
for (const match of styleText.matchAll(/var\(\s*(--bg\d+)\s*\)/g)) {
const value = resolveVar(match[1] ?? "");
if (value) collectQuakeRenderBundleInlineAssetUrls(urls, value, resolveVar);
}
}

function normalizeQuakeRenderBundleCssUrl(value: string): string {
let url = value.trim();
if (!url) return "";
if ((url.startsWith("&quot;") && url.endsWith("&quot;")) || (url.startsWith("&#34;") && url.endsWith("&#34;"))) {
url = url.slice(url.indexOf(";") + 1, url.lastIndexOf("&"));
}
return url.trim();
}

function preloadQuakeRenderBundleStyle(renderBundle: QuakePreparedRenderBundle): Promise<void> {
const key = quakeRenderBundleStyleKey(renderBundle);
if (!key) return Promise.resolve();
Expand Down
55 changes: 46 additions & 9 deletions src/runtime/world.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
exposeQuakeRenderBundleAtlasPages,
mountQuakeRenderBundleMesh,
preloadQuakeRenderBundleAtlasPages,
preloadQuakeRenderBundleElementAssets,
registerQuakeRenderBundleDebugLeafSourceFace,
registerQuakeRenderBundleDebugOutlineLeaves,
stripPolyMeshMetadata,
Expand Down Expand Up @@ -155,6 +156,7 @@ export interface QuakeWorldController {
syncVisibility: (force?: boolean) => void;
visibleLeavesAt: (origin: [number, number, number]) => Set<number> | null;
waitForVisibleAtlasPages: () => Promise<void>;
waitForVisibleTextures: () => Promise<void>;
}

export interface QuakeWorldDebugBucket {
Expand Down Expand Up @@ -214,6 +216,8 @@ export function createQuakeWorldController(options: QuakeWorldControllerOptions)
let visibleAtlasPageKey = "";
let visibleAtlasPageSet = new Set<number>();
let visibleAtlasPageReadyPromise: Promise<void> = Promise.resolve();
let visibleAtlasPrewarmReadyPromise: Promise<void> = Promise.resolve();
let visibleWorldTextureReadyPromise: Promise<void> = Promise.resolve();
let debugShellVisible = false;

const clear = (): void => {
Expand Down Expand Up @@ -241,6 +245,8 @@ export function createQuakeWorldController(options: QuakeWorldControllerOptions)
visibleAtlasPageKey = "";
visibleAtlasPageSet = new Set();
visibleAtlasPageReadyPromise = Promise.resolve();
visibleAtlasPrewarmReadyPromise = Promise.resolve();
visibleWorldTextureReadyPromise = Promise.resolve();
};

const clearPresentationResyncTimers = (): void => {
Expand Down Expand Up @@ -1129,6 +1135,12 @@ export function createQuakeWorldController(options: QuakeWorldControllerOptions)
};

const waitForVisibleAtlasPages = (): Promise<void> => visibleAtlasPageReadyPromise;
const waitForVisibleTextures = (): Promise<void> =>
Promise.all([
visibleAtlasPageReadyPromise,
visibleAtlasPrewarmReadyPromise,
visibleWorldTextureReadyPromise,
]).then(() => undefined);

const syncWorldAtlasResidencyPages = (leafIndex: number | null): void => {
const atlasResidency = currentRenderBundle?.atlasResidency;
Expand All @@ -1143,7 +1155,7 @@ export function createQuakeWorldController(options: QuakeWorldControllerOptions)
const exposedPages = currentPages.length ? currentPages : allPages;
const warmPages = prewarmPages.length ? prewarmPages : exposedPages;
setVisibleAtlasResidencyPages(exposedPages);
void preloadQuakeRenderBundleAtlasPages(currentRenderBundle, warmPages);
visibleAtlasPrewarmReadyPromise = preloadQuakeRenderBundleAtlasPages(currentRenderBundle, warmPages);
};

const setVisibleAtlasResidencyPages = (pageIndexes: readonly number[]): void => {
Expand All @@ -1159,14 +1171,38 @@ export function createQuakeWorldController(options: QuakeWorldControllerOptions)
};

const syncMountedAtlasResidencyPages = (): void => {
if (!currentHandle || !currentRenderBundle || currentRenderBundle.atlasResidency?.mode !== "pvs-pages") return;
const nextPages = mountedAtlasResidencyPageIndexes(visibleAtlasPageSet);
const pageKey = nextPages.join(",");
if (pageKey === visibleAtlasPageKey) return;
exposeQuakeRenderBundleAtlasPages(currentHandle.element, currentRenderBundle, nextPages);
visibleAtlasPageSet = new Set(nextPages);
visibleAtlasPageKey = pageKey;
visibleAtlasPageReadyPromise = preloadQuakeRenderBundleAtlasPages(currentRenderBundle, nextPages);
if (!currentHandle || !currentRenderBundle) return;
if (currentRenderBundle.atlasResidency?.mode === "pvs-pages") {
const nextPages = mountedAtlasResidencyPageIndexes(visibleAtlasPageSet);
const pageKey = nextPages.join(",");
if (pageKey !== visibleAtlasPageKey) {
exposeQuakeRenderBundleAtlasPages(currentHandle.element, currentRenderBundle, nextPages);
visibleAtlasPageSet = new Set(nextPages);
visibleAtlasPageKey = pageKey;
visibleAtlasPageReadyPromise = preloadQuakeRenderBundleAtlasPages(currentRenderBundle, nextPages);
}
}
syncMountedWorldTextureReadiness();
};

const syncMountedWorldTextureReadiness = (): void => {
if (!currentHandle) {
visibleWorldTextureReadyPromise = Promise.resolve();
return;
}
visibleWorldTextureReadyPromise = preloadQuakeRenderBundleElementAssets(
currentHandle.element,
mountedWorldTextureElements(),
);
};

const mountedWorldTextureElements = (): HTMLElement[] => {
const elements: HTMLElement[] = [];
for (const leaf of quakeLeaves) {
if (leaf.meshKind !== "world" || !leaf.mounted || !leaf.element.isConnected) continue;
elements.push(leaf.element);
}
return elements;
};

const normalizedAtlasResidencyPageIndexes = (pageIndexes: Iterable<number>): number[] =>
Expand Down Expand Up @@ -1353,6 +1389,7 @@ export function createQuakeWorldController(options: QuakeWorldControllerOptions)
syncVisibility,
visibleLeavesAt: (origin: [number, number, number]) => currentVisibility?.visibleLeavesAt(origin) ?? null,
waitForVisibleAtlasPages,
waitForVisibleTextures,
};
}

Expand Down
31 changes: 31 additions & 0 deletions test/runtime/renderBundlePreloadUrls.test.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import assert from "node:assert/strict";
import test from "node:test";

import { Window } from "happy-dom";

import { importTsModule } from "../importTsModule.mjs";

const {
quakeRenderBundleElementAssetUrls,
quakeRenderBundlePreloadAssetUrls,
} = await importTsModule("src/runtime/renderBundleMesh.ts");

Expand Down Expand Up @@ -44,3 +47,31 @@ test("render bundles without complete asset URLs fail before runtime preload", (
/assetUrls must be complete/,
);
});

test("mounted render bundle leaves expose direct URLs and atlas root-var URLs for preload", () => {
const window = new Window();
const mesh = window.document.createElement("div");
mesh.className = "polycss-mesh";
mesh.style.setProperty("--bg0", 'url("/q/b/e1m1/a0.png")');

const directLeaf = window.document.createElement("s");
directLeaf.setAttribute(
"style",
'background:url("/q/b/e1m1/direct.png") 0px 0px / 64px 64px no-repeat',
);
const atlasLeaf = window.document.createElement("s");
atlasLeaf.setAttribute(
"style",
"background:var(--bg0) 0px 0px / 64px 64px no-repeat",
);
const duplicateAtlasLeaf = window.document.createElement("s");
duplicateAtlasLeaf.setAttribute(
"style",
"background-image:var(--bg0)",
);

assert.deepEqual(
quakeRenderBundleElementAssetUrls(mesh, [directLeaf, atlasLeaf, duplicateAtlasLeaf]),
["/q/b/e1m1/direct.png", "/q/b/e1m1/a0.png"],
);
});
Loading