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
35 changes: 26 additions & 9 deletions modules/shadertools/src/lib/shader-assembly/assemble-shaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -932,13 +932,13 @@ function relocateWGSLModuleBindingMatch(
const location =
registryLocation !== undefined
? registryLocation
: relocationState.nextHintedBindingLocation === null
? allocateAutoBindingLocation(group, context.usedBindingsByGroup)
: allocateAutoBindingLocation(
group,
context.usedBindingsByGroup,
relocationState.nextHintedBindingLocation
);
: allocateAutoBindingLocation(
group,
context.usedBindingsByGroup,
module.name,
relocationState.nextHintedBindingLocation ?? undefined,
context.bindingRegistry
);
validateModuleWGSLBinding(module.name, group, location, name);
if (
registryLocation !== undefined &&
Expand Down Expand Up @@ -1122,9 +1122,19 @@ function registerUsedBindingLocation(
function allocateAutoBindingLocation(
group: number,
usedBindingsByGroup: Map<number, Set<number>>,
preferredBindingLocation?: number
moduleName: string,
preferredBindingLocation?: number,
bindingRegistry?: Map<string, number>
): number {
const usedBindings = usedBindingsByGroup.get(group) || new Set<number>();
const registeredBindingLocations = new Set<number>();
const registryGroupPrefix = `${group}:`;
const registryModulePrefix = `${registryGroupPrefix}${moduleName}:`;
for (const [registryKey, location] of bindingRegistry || []) {
if (registryKey.startsWith(registryModulePrefix)) {
registeredBindingLocations.add(location);
}
Comment thread
ibgreen-openai marked this conversation as resolved.
}
let nextBinding =
preferredBindingLocation ??
(group === 0
Expand All @@ -1133,10 +1143,17 @@ function allocateAutoBindingLocation(
? Math.max(...usedBindings) + 1
: 0);

while (usedBindings.has(nextBinding)) {
while (usedBindings.has(nextBinding) || registeredBindingLocations.has(nextBinding)) {
nextBinding++;
}

// Active modules were reserved above; only stale, inactive modules can own this free slot.
for (const [registryKey, location] of bindingRegistry || []) {
if (location === nextBinding && registryKey.startsWith(registryGroupPrefix)) {
bindingRegistry?.delete(registryKey);
}
}

return nextBinding;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -900,14 +900,15 @@ fn calculateDiffuseTransmissionIBL(
);
let rotatedNormal = environmentRotation * -pbrInfo.n.xz;
let oppositeNormal = vec3f(rotatedNormal.x, -pbrInfo.n.y, rotatedNormal.y);
let environmentColor = textureSample(
let environmentColor = textureSampleLevel(
pbr_diffuseEnvSampler,
pbr_diffuseEnvSamplerSampler,
oppositeNormal
oppositeNormal,
0.0
).rgb * max(pbrScene.environmentIntensity, 0.0);
#else
let environmentColor = SRGBtoLINEAR(
textureSample(pbr_diffuseEnvSampler, pbr_diffuseEnvSamplerSampler, -pbrInfo.n)
textureSampleLevel(pbr_diffuseEnvSampler, pbr_diffuseEnvSamplerSampler, -pbrInfo.n, 0.0)
).rgb;
#endif
let nonReflectedEnergy = vec3f(1.0) - clamp(pbrInfo.reflectance0, vec3f(0.0), vec3f(1.0));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,46 @@ struct Group2RegistryBUniforms {
`
};

const PERMUTED_GROUP_3_TEXTURE_MODULE: ShaderModule = {
name: 'permutedGroup3TextureModule',
bindingLayout: [
{name: 'permutedMaterial', group: 3},
{name: 'pbr_baseColorSampler', group: 3},
{name: 'pbr_baseColorSamplerSampler', group: 3},
{name: 'pbr_transmissionSampler', group: 3},
{name: 'pbr_transmissionSamplerSampler', group: 3}
],
source: /* wgsl */ `\
struct PermutedMaterialUniforms {
value: f32
};

@group(3) @binding(0) var<uniform> permutedMaterial: PermutedMaterialUniforms;

#if HAS_BASECOLORMAP
@group(3) @binding(auto) var pbr_baseColorSampler: texture_2d<f32>;
@group(3) @binding(auto) var pbr_baseColorSamplerSampler: sampler;
#endif

#if HAS_TRANSMISSIONMAP
@group(3) @binding(auto) var pbr_transmissionSampler: texture_2d<f32>;
@group(3) @binding(auto) var pbr_transmissionSamplerSampler: sampler;
#endif
`
};

const GROUP_3_EXPLICIT_REGISTRY_MODULE: ShaderModule = {
name: 'group3ExplicitRegistryModule',
bindingLayout: [{name: 'group3ExplicitRegistryBinding', group: 3}],
source: /* wgsl */ `\
struct Group3ExplicitRegistryUniforms {
value: f32
};

@group(3) @binding(1) var<uniform> group3ExplicitRegistryBinding: Group3ExplicitRegistryUniforms;
`
};

const MULTILINE_EXPLICIT_MODULE: ShaderModule = {
name: 'multilineExplicitModule',
bindingLayout: [{name: 'multilineExplicit', group: 2}],
Expand Down Expand Up @@ -1159,6 +1199,129 @@ test('assembleWGSLShader#keeps module auto allocations stable within one assembl
t.end();
});

test('assembleWGSLShader#keeps disjoint texture permutations compatible when combined', t => {
const shaderAssembler = new WGSLShaderAssembler();
const assemblePermutation = (defines: Record<string, boolean>) =>
shaderAssembler.assembleWGSLShader({
platformInfo: PLATFORM_INFO,
source: APP_WGSL,
modules: [PERMUTED_GROUP_3_TEXTURE_MODULE],
defines
});

const transmissionOnly = assemblePermutation({HAS_TRANSMISSIONMAP: true});
const baseColorOnly = assemblePermutation({HAS_BASECOLORMAP: true});
const combined = assemblePermutation({HAS_BASECOLORMAP: true, HAS_TRANSMISSIONMAP: true});

const getBindingLocation = (
assembledShader: ReturnType<typeof assemblePermutation>,
name: string
): number | undefined =>
assembledShader.bindingTable.find(binding => binding.name === name)?.binding;

t.equal(getBindingLocation(transmissionOnly, 'pbr_transmissionSampler'), 1);
t.equal(getBindingLocation(transmissionOnly, 'pbr_transmissionSamplerSampler'), 2);
t.equal(getBindingLocation(baseColorOnly, 'pbr_baseColorSampler'), 3);
t.equal(getBindingLocation(baseColorOnly, 'pbr_baseColorSamplerSampler'), 4);
t.equal(getBindingLocation(combined, 'pbr_transmissionSampler'), 1);
t.equal(getBindingLocation(combined, 'pbr_transmissionSamplerSampler'), 2);
t.equal(getBindingLocation(combined, 'pbr_baseColorSampler'), 3);
t.equal(getBindingLocation(combined, 'pbr_baseColorSamplerSampler'), 4);

t.end();
});

test('assembleWGSLShader#reclaims bindings from inactive runtime-generated modules', t => {
const shaderAssembler = new WGSLShaderAssembler();
const maximumBindingsPerGroup = 16;
let highestBindingLocation = 0;

for (let moduleIndex = 0; moduleIndex < maximumBindingsPerGroup * 4; moduleIndex++) {
const moduleName = `runtimeGeneratedMaterial${moduleIndex}`;
const textureName = `runtimeGeneratedTexture${moduleIndex}`;
const runtimeGeneratedModule: ShaderModule = {
name: moduleName,
bindingLayout: [
{name: textureName, group: 3},
{name: `${textureName}Sampler`, group: 3}
],
source: /* wgsl */ `\
@group(3) @binding(auto) var ${textureName}: texture_2d<f32>;
@group(3) @binding(auto) var ${textureName}Sampler: sampler;
`
};
const assembledShader = shaderAssembler.assembleWGSLShader({
platformInfo: {
...PLATFORM_INFO,
limits: {maxBindingsPerBindGroup: maximumBindingsPerGroup}
},
source: APP_WGSL,
modules: [PERMUTED_GROUP_3_TEXTURE_MODULE, runtimeGeneratedModule],
defines: {HAS_TRANSMISSIONMAP: true}
});
const textureBindingLocation = assembledShader.bindingTable.find(
binding => binding.name === textureName
)?.binding;
const samplerBindingLocation = assembledShader.bindingTable.find(
binding => binding.name === `${textureName}Sampler`
)?.binding;

highestBindingLocation = Math.max(
highestBindingLocation,
textureBindingLocation ?? 0,
samplerBindingLocation ?? 0
);
}

t.ok(
highestBindingLocation < maximumBindingsPerGroup,
'historical runtime-generated modules never consume the available bind-group slots'
);
t.equal(
highestBindingLocation,
4,
'active material bindings stay stable while generated texture/sampler pairs reuse their slots'
);

t.end();
});

test('assembleWGSLShader#scopes inactive reservations to automatic bindings in their group', t => {
const shaderAssembler = new WGSLShaderAssembler();

shaderAssembler.assembleWGSLShader({
platformInfo: PLATFORM_INFO,
source: APP_WGSL,
modules: [PERMUTED_GROUP_3_TEXTURE_MODULE],
defines: {HAS_TRANSMISSIONMAP: true}
});

const explicitShader = shaderAssembler.assembleWGSLShader({
platformInfo: PLATFORM_INFO,
source: APP_WGSL,
modules: [GROUP_3_EXPLICIT_REGISTRY_MODULE]
});
const isolatedGroupShader = shaderAssembler.assembleWGSLShader({
platformInfo: PLATFORM_INFO,
source: APP_WGSL,
modules: [GROUP_2_REGISTRY_A]
});

t.equal(
explicitShader.bindingTable.find(binding => binding.name === 'group3ExplicitRegistryBinding')
?.binding,
1,
'inactive automatic reservations do not invalidate explicit bindings in a different shader'
);
t.equal(
isolatedGroupShader.bindingTable.find(binding => binding.name === 'group2RegistryA')?.binding,
0,
'inactive locations in another binding group do not change automatic allocation'
);

t.end();
});

test('assembleWGSLShader#rejects application group 0 bindings above reserved range', t => {
const shaderAssembler = new WGSLShaderAssembler();

Expand Down
92 changes: 91 additions & 1 deletion modules/shadertools/test/modules/lighting/pbr-material.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,40 @@ import {
getShaderModuleUniformLayoutValidationResult,
getShaderModuleUniforms,
type PBRMaterialUniforms,
pbrMaterial
type PlatformInfo,
pbrMaterial,
pbrScene,
WGSLShaderAssembler
} from '@luma.gl/shadertools';
import {getWebGPUTestDevice} from '@luma.gl/test-utils';
import test from 'test/utils/vitest-tape';

const FLOAT32_EPSILON = 1e-6;

const WEBGPU_PLATFORM: PlatformInfo = {
type: 'webgpu',
shaderLanguage: 'wgsl',
shaderLanguageVersion: 300,
gpu: 'test',
features: new Set()
};

const DIFFUSE_TRANSMISSION_UNIFORMITY_SHADER = /* wgsl */ `
@vertex
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> @builtin(position) vec4f {
return vec4f(f32(vertexIndex), 0.0, 0.0, 1.0);
}

@fragment
fn fragmentMain(@builtin(position) position: vec4f) -> @location(0) vec4f {
fragmentInputs.pbr_vPosition = position.xyz;
fragmentInputs.pbr_vNormal = vec3f(0.0, 0.0, 1.0);
fragmentInputs.pbr_vUV0 = position.xy * 0.01;
fragmentInputs.pbr_vUV1 = fragmentInputs.pbr_vUV0;
return pbr_filterColor(vec4f(1.0));
}
`;

const CORE_UNIFORM_BUFFER_LAYOUT = {
unlit: {offset: 0, size: 1},
baseColorMapEnabled: {offset: 1, size: 1},
Expand Down Expand Up @@ -173,6 +201,68 @@ function almostEqual(actualValue: number, expectedValue: number): boolean {
return Math.abs(actualValue - expectedValue) <= FLOAT32_EPSILON;
}

test('shadertools#pbrMaterial compiles texture-dependent diffuse-transmission IBL on WebGPU', async testCase => {
const diffuseTransmissionSource =
pbrMaterial.source?.match(/fn calculateDiffuseTransmissionIBL\([\s\S]*?\n}\n#endif/)?.[0] || '';

testCase.ok(diffuseTransmissionSource, 'diffuse-transmission IBL helper is present');
testCase.equal(
(diffuseTransmissionSource.match(/\btextureSampleLevel\(/g) || []).length,
2,
'scene and legacy environment paths use derivative-free cubemap sampling'
);
testCase.notOk(
/\btextureSample\(/.test(diffuseTransmissionSource),
'data-dependent transmission branches do not require uniform implicit derivatives'
);

const device = await getWebGPUTestDevice();
if (!device) {
testCase.comment('WebGPU unavailable; diffuse-transmission source assertions still run');
testCase.end();
return;
}

for (const useSceneEnvironment of [false, true]) {
const shaderSource = new WGSLShaderAssembler().assembleWGSLShader({
platformInfo: WEBGPU_PLATFORM,
source: DIFFUSE_TRANSMISSION_UNIFORMITY_SHADER,
modules: useSceneEnvironment ? [pbrScene, pbrMaterial] : [pbrMaterial],
defines: {
HAS_NORMALS: true,
HAS_UV: true,
HAS_TRANSMISSIONMAP: true,
USE_IBL: true,
USE_MATERIAL_EXTENSIONS: true,
USE_SCENE_ENVIRONMENT: useSceneEnvironment
}
}).source;
const environmentName = useSceneEnvironment ? 'scene' : 'legacy';
const shader = device.createShader({
id: `pbr-diffuse-transmission-${environmentName}-uniformity`,
source: shaderSource
});

try {
const compilationErrors = (await shader.getCompilationInfo())
.filter(message => message.type === 'error')
.map(message => message.message);

testCase.equal(
compilationErrors.length,
0,
`${environmentName} IBL compiles with a texture-dependent transmission factor${
compilationErrors.length ? `: ${compilationErrors.join('; ')}` : ''
}`
);
} finally {
shader.destroy();
}
}

testCase.end();
});

test('shadertools#pbrMaterial exposes typed defaults and uniform names', testCase => {
const pbrMaterialUniformTypecheck: Required<PBRMaterialUniforms> = pbrMaterial.defaultUniforms;
testCase.ok(pbrMaterialUniformTypecheck, 'pbrMaterial default uniforms are typed');
Expand Down
Loading