Skip to content
Closed
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
6 changes: 3 additions & 3 deletions packages/core/src/shader/Shader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export class Shader implements IReferable {
const shaderSource = shaderCompiler._parseShaderSource(nameOrShaderSource);
if (shaderMap[shaderSource.name]) {
console.error(`Shader named "${shaderSource.name}" already exists.`);
return;
return shaderMap[shaderSource.name];
}

const basePathForIncludeKey = new URL(path ?? "", ShaderPass._shaderRootPath).href;
Expand Down Expand Up @@ -141,7 +141,7 @@ export class Shader implements IReferable {
} else {
if (shaderMap[nameOrShaderSource]) {
console.error(`Shader named "${nameOrShaderSource}" already exists.`);
return;
return shaderMap[nameOrShaderSource];
}
if (shaderPassesOrSubShadersOrPlatformTarget.length > 0) {
if (shaderPassesOrSubShadersOrPlatformTarget[0].constructor === ShaderPass) {
Expand Down Expand Up @@ -175,7 +175,7 @@ export class Shader implements IReferable {
const shaderMap = Shader._shaderMap;
if (shaderMap[data.name]) {
console.error(`Shader named "${data.name}" already exists.`);
return;
return shaderMap[data.name];
}

const subShaderList = data.subShaders.map((subData) => {
Expand Down
33 changes: 30 additions & 3 deletions packages/loader/src/ShaderLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,44 @@ import {

@resourceLoader(AssetType.Shader, ["shader", "shaderc"])
class ShaderLoader extends Loader<Shader> {
private static _shaderNameRegex = /^(?:(?:\s+)|(?:\/\/[^\r\n]*(?:\r?\n|$))|(?:\/\*[\s\S]*?\*\/))*Shader\s+"([^"]+)"/;
private static _shaderSourceMap = new WeakMap<Shader, { url: string; source: string }>();

load(item: LoadItem, resourceManager: ResourceManager): AssetPromise<Shader> {
const url = item.url!;
// @ts-expect-error _request is @internal
return resourceManager._request<string>(url, { ...item, type: "text" }).then((code) => {
const source = code.trimStart();
if (source.startsWith("{")) {
// @ts-expect-error _createFromPrecompiled is @internal
return Shader._createFromPrecompiled(JSON.parse(source));
const data = JSON.parse(source);
return this._getOrCreateShader(data.name, url, code, () => {
// @ts-expect-error _createFromPrecompiled is @internal
return Shader._createFromPrecompiled(data);
});
}

return Shader.create(code, undefined, url);
const shaderName = ShaderLoader._shaderNameRegex.exec(source)?.[1];
if (!shaderName) {
throw new Error(`Unable to parse shader name from "${url}".`);
}
return this._getOrCreateShader(shaderName, url, code, () => Shader.create(code, undefined, url));
});
}

private _getOrCreateShader(name: string, url: string, source: string, create: () => Shader): Shader {
const existingShader = Shader.find(name);
if (existingShader) {
const existingSource = ShaderLoader._shaderSourceMap.get(existingShader);
if (existingSource?.url === url && existingSource.source === source) {
return existingShader;
}

const existingURL = existingSource ? `"${existingSource.url}"` : "an unknown source";
throw new Error(`Shader named "${name}" from "${url}" conflicts with the shader registered from ${existingURL}.`);
}

const shader = create();
ShaderLoader._shaderSourceMap.set(shader, { url, source });
return shader;
}
}
3 changes: 2 additions & 1 deletion tests/src/core/Shader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ describe("Shader", () => {

// Create same name shader
const errorSpy = vi.spyOn(console, "error");
Shader.create("custom", [new SubShader("Default", [makePass()])]);
const duplicateShader = Shader.create("custom", [new SubShader("Default", [makePass()])]);
expect(errorSpy).toHaveBeenCalledWith('Shader named "custom" already exists.');
expect(duplicateShader).equal(customShader);
vi.resetAllMocks();

// Create shader by empty SubShader array
Expand Down
118 changes: 118 additions & 0 deletions tests/src/loader/ShaderLoader.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { AssetPromise, AssetType, Shader } from "@galacean/engine";
import "@galacean/engine-loader";
import { WebGLEngine } from "@galacean/engine";
import { ShaderCompiler } from "@galacean/engine-shader-compiler";
import { describe, expect, it, vi } from "vitest";

const shaderSource = `
// Leading comments are valid ShaderLab source.
Shader "Loader/ReusableAcrossEngines" {
SubShader "Default" {
Pass "Default" {
struct Attributes { vec4 POSITION; };

VertexShader = vert;
FragmentShader = frag;

void vert(Attributes attr) {
gl_Position = attr.POSITION;
}

void frag() {
gl_FragColor = vec4(1.0);
}
}
}
}`;

const precompiledShader = JSON.stringify({
name: "Loader/ReusablePrecompiledAcrossEngines",
platformTarget: 0,
subShaders: []
});

const conflictingShaderSource = shaderSource
.replace("Loader/ReusableAcrossEngines", "Loader/ConflictingAcrossEngines")
.replace("vec4(1.0)", "vec4(0.0)");

describe("ShaderLoader", () => {
it("reuses source and precompiled shaders across engine resource managers", async () => {
const sourceURL = "Shaders/reusable-across-engines.shader";
const precompiledURL = "Shaders/reusable-precompiled-across-engines.shaderc";
const canvas = document.createElement("canvas");
const engine1 = await WebGLEngine.create({
canvas,
shaderCompiler: new ShaderCompiler()
});
const request1 = vi
// @ts-expect-error _requestByRemoteUrl is @internal
.spyOn(engine1.resourceManager, "_requestByRemoteUrl")
.mockImplementation((url: string) => AssetPromise.resolve(url === sourceURL ? shaderSource : precompiledShader));

let engine2: WebGLEngine;
let sourceShader: Shader;
let precompiled: Shader;
try {
sourceShader = await engine1.resourceManager.load<Shader>({ url: sourceURL, type: AssetType.Shader });
precompiled = await engine1.resourceManager.load<Shader>({ url: precompiledURL, type: AssetType.Shader });
expect(request1).toHaveBeenCalledTimes(2);
expect(sourceShader.compileVariant(engine1, [])).true;

engine1.destroy();
engine2 = await WebGLEngine.create({
canvas,
shaderCompiler: new ShaderCompiler()
});
const request2 = vi
// @ts-expect-error _requestByRemoteUrl is @internal
.spyOn(engine2.resourceManager, "_requestByRemoteUrl")
.mockImplementation((url: string) =>
AssetPromise.resolve(url === sourceURL ? shaderSource : precompiledShader)
);

const reusedSource = await engine2.resourceManager.load<Shader>({ url: sourceURL, type: AssetType.Shader });
const reusedPrecompiled = await engine2.resourceManager.load<Shader>({
url: precompiledURL,
type: AssetType.Shader
});

expect(reusedSource).equal(sourceShader);
expect(reusedPrecompiled).equal(precompiled);
expect(request2).toHaveBeenCalledTimes(2);
expect(reusedSource.compileVariant(engine2, [])).true;
} finally {
engine1.destroy();
engine2?.destroy();
Shader.find("Loader/ReusableAcrossEngines")?.destroy(true);
Shader.find("Loader/ReusablePrecompiledAcrossEngines")?.destroy(true);
}
});

it("rejects a same-name shader whose source changes across engines", async () => {
const url = "Shaders/conflicting-across-engines.shader";
const canvas = document.createElement("canvas");
const engine1 = await WebGLEngine.create({ canvas, shaderCompiler: new ShaderCompiler() });
vi.spyOn(engine1.resourceManager, "_requestByRemoteUrl")
// @ts-expect-error _requestByRemoteUrl is @internal
.mockReturnValue(AssetPromise.resolve(conflictingShaderSource));

let engine2: WebGLEngine;
try {
await engine1.resourceManager.load<Shader>({ url, type: AssetType.Shader });
engine1.destroy();

engine2 = await WebGLEngine.create({ canvas, shaderCompiler: new ShaderCompiler() });
vi.spyOn(engine2.resourceManager, "_requestByRemoteUrl")
// @ts-expect-error _requestByRemoteUrl is @internal
.mockReturnValue(AssetPromise.resolve(conflictingShaderSource.replace("vec4(0.0)", "vec4(0.5)")));

await expect(engine2.resourceManager.load<Shader>({ url, type: AssetType.Shader })).rejects.toThrow(
`Shader named "Loader/ConflictingAcrossEngines" from "${url}" conflicts`
);
} finally {
engine1.destroy();
engine2?.destroy();
Shader.find("Loader/ConflictingAcrossEngines")?.destroy(true);
}
});
});
Loading