Visitar URL original
fix: Show the internal before-passes and the camera use of engine 2.24 passes by mvaligursky · Pull Request #17 · playcanvas/inspector · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
fix: Show the internal before-passes and the camera use of engine 2.2…
…4 passes

The frame graph view nests the internal before-passes of a pass under it, ahead of its
before-passes, and reads the first and last camera use of a forward pass from the pass,
where engine 2.24 keeps them. Earlier engines, which have neither, keep working, and the
requires cubemaps row is only shown on engines which still have the property.

Fixed #13
  • Loading branch information
Martin Valigursky
Martin Valigursky committed Oct 27, 2026
commit 174abf2a40c5fbf6710dbcd7463798ca8311c43b
44 changes: 37 additions & 7 deletions src/frame-graph-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,8 @@ function instancePage(list, key, ctx) {
// pass properties the model shows explicitly, or that are plumbing
const SKIP_PASS = [
'name', 'enabled', 'executeEnabled', 'requiresCubemaps', 'device', 'renderTarget', 'colorOps',
'colorArrayOps', 'depthStencilOps', 'beforePasses', 'afterPasses', 'children', 'samples',
'colorArrayOps', 'depthStencilOps', 'internalBeforePasses', 'beforePasses', 'afterPasses', 'children', 'samples',
'firstCameraUse', 'lastCameraUse',
'scaleX', 'scaleY', 'options', 'fullSizeClearRect', 'layerRenderSteps', 'renderer', 'scene',
'shadowRenderer'
];
Expand Down Expand Up @@ -332,7 +333,9 @@ function captureFrameGraph(app) {
/** @type {Map<FramePass, FramePass>} */
const parentOf = new Map();
const mark = (pass) => {
const children = [...pass.beforePasses, ...pass.afterPasses, ...(Array.isArray(pass.children) ? pass.children : [])];
// the internal before-passes, which engines before 2.24 do not have, execute ahead of the
// before-passes
const children = [...pass.internalBeforePasses ?? [], ...pass.beforePasses, ...pass.afterPasses, ...(Array.isArray(pass.children) ? pass.children : [])];
for (const child of children) {
parentOf.set(child, pass);
mark(child);
Expand Down Expand Up @@ -501,7 +504,8 @@ function passRows(frame, device) {
const layer = step.layer;
const enabled = layer.enabled && (pass.layerComposition?.isEnabled(layer, step.transparent) ?? true);
const camera = step.cameraComponent?.entity;
const flags = `${step.firstCameraUse ? ' · first' : ''}${step.lastCameraUse ? ' · last' : ''}${enabled ? '' : ' · disabled'}`;
const use = cameraUse(step, i, pass);
const flags = `${use.first ? ' · first' : ''}${use.last ? ' · last' : ''}${enabled ? '' : ' · disabled'}`;
rows.push({
key: `${entry.key}/${i}`,
item: new LayerStepSelection(pass, step, i),
Expand Down Expand Up @@ -638,11 +642,14 @@ function buildPassModel(pass, ctx) {
const runs = Array.isArray(/** @type {any} */ (entry.parent).children) && /** @type {any} */ (entry.parent).children.includes(pass);
push(general, runs ? 'run by' : 'owned by', { text: passDisplayName(entry.parent), cls: 'ref', target: entry.parent });
}
const internalBeforePasses = /** @type {any} */ (pass).internalBeforePasses;
if (internalBeforePasses?.length) push(general, 'internal before passes', passList(internalBeforePasses));
if (pass.beforePasses.length) push(general, 'before passes', passList(pass.beforePasses));
if (pass.afterPasses.length) push(general, 'after passes', passList(pass.afterPasses));
const children = /** @type {any} */ (pass).children;
if (Array.isArray(children) && children.length) push(general, 'per-view passes', passList(children));
push(general, 'requires cubemaps', read(pass, 'requiresCubemaps'));
// engines from 2.24 no longer have the property
if (/** @type {any} */ (pass).requiresCubemaps !== undefined) push(general, 'requires cubemaps', read(pass, 'requiresCubemaps'));
sections.push(general);

const rt = entry ? entry.renderTarget : (pass.renderTarget === undefined ? undefined : (pass.renderTarget ?? device.backBuffer));
Expand Down Expand Up @@ -695,9 +702,10 @@ function buildPassModel(pass, ctx) {
push(section, `[${i}] camera`, describeValue(step.cameraComponent?.entity ?? null));
push(section, `[${i}] layer`, describeValue(layer));
const list = ctx.frame?.visible?.get(step) ?? null;
const use = cameraUse(step, i, pass);
push(section, `[${i}] draws`, {
text: `${step.transparent ? 'transparent' : 'opaque'}, ${drawnText(list, layer)}` +
`${enabled ? '' : ', disabled'}${step.firstCameraUse ? ', first camera use' : ''}${step.lastCameraUse ? ', last camera use' : ''}`,
`${enabled ? '' : ', disabled'}${use.first ? ', first camera use' : ''}${use.last ? ', last camera use' : ''}`,
cls: 'obj',
expand: list ? () => instancePage(list, `${ctx.passKey ?? ''}/${i}`, ctx) : undefined
});
Expand All @@ -722,6 +730,27 @@ function stepEnabled(step, pass) {
return step.layer.enabled && (pass.layerComposition?.isEnabled(step.layer, step.transparent) ?? true);
}

/**
* Whether a layer render step of a forward pass is the first to render its camera in the frame,
* and whether it is the last. Engines before 2.24 keep these flags on each step. Later ones keep
* them on the pass, which renders a single camera, so they belong to its first and its last step.
*
* @param {*} step - A layer render step.
* @param {number} index - The index of the step in its pass.
* @param {RenderPassForward} pass - The pass it belongs to.
* @returns {{ first: boolean, last: boolean }} The flags.
*/
function cameraUse(step, index, pass) {
const forward = /** @type {any} */ (pass);
if (forward.firstCameraUse === undefined) {
return { first: !!step.firstCameraUse, last: !!step.lastCameraUse };
}
return {
first: !!forward.firstCameraUse && index === 0,
last: !!forward.lastCameraUse && index === pass.layerRenderSteps.length - 1
};
}

/**
* Everything the property view shows for one layer step of a forward pass: the camera and layer it
* renders, what it clears, and the instances it drew in draw order, listed straight away and paged
Expand All @@ -748,8 +777,9 @@ function buildStepModel(selection, ctx) {
push(general, 'draws', { text: drawnText(list, layer), cls: list ? 'num' : 'null' });
push(general, 'enabled', describeValue(enabled));
push(general, 'clears', { text: clears || 'nothing', cls: clears ? 'obj' : 'null' });
if (step.firstCameraUse) push(general, 'first camera use', describeValue(true));
if (step.lastCameraUse) push(general, 'last camera use', describeValue(true));
const use = cameraUse(step, index, pass);
if (use.first) push(general, 'first camera use', describeValue(true));
if (use.last) push(general, 'last camera use', describeValue(true));
if (ctx.debug?.stepKey === ctx.passKey && list) {
push(general, 'debug frame', { text: `stops after draw ${ctx.debug.index} of ${list.instances.length}`, cls: 'num' });
}
Expand Down
7 changes: 4 additions & 3 deletions src/perf-capture.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,12 @@ function profilerName(pass) {

/**
* @param {any} pass - A frame pass.
* @returns {any[]} The passes it runs around itself: its before and after passes, and the passes
* of each view of a multi-view pass.
* @returns {any[]} The passes it runs around itself: its internal before-passes, which engines
* before 2.24 do not have, its before and after passes, and the passes of each view of a
* multi-view pass.
*/
function nestedPasses(pass) {
return [...pass.beforePasses ?? [], ...pass.afterPasses ?? [], ...(Array.isArray(pass.children) ? pass.children : [])];
return [...pass.internalBeforePasses ?? [], ...pass.beforePasses ?? [], ...pass.afterPasses ?? [], ...(Array.isArray(pass.children) ? pass.children : [])];
}

/**
Expand Down
62 changes: 62 additions & 0 deletions test/inspector.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2366,6 +2366,68 @@ describe('Inspector frame graph capture', function () {

expect(captureFrameGraph(app).entries.map(e => e.pass)).to.deep.equal([forward]);
});

it('nests the internal before-passes under their pass, ahead of its before-passes', function () {
const shadowMap = { name: 'ShadowMap' };
const shadow = pass('RenderPassShadowDirectional', { renderTarget: shadowMap });
const before = pass('UserBefore');
const { app, forward } = forwardScene([instance('wall')]);
forward.internalBeforePasses = [shadow];
forward.beforePasses = [before];
app.frameGraph.passes = [shadow, before, forward];

const frame = captureFrameGraph(app);
expect(frame.entries.map(e => e.parent)).to.deep.equal([forward, forward, null]);
expect(frame.entries.map(e => e.depth)).to.deep.equal([1, 1, 0]);
expect(frame.entries[2].children).to.equal(2);

const ctx = { device: app.graphicsDevice, frame, pages: new Map(), filter: '', stable: true };
const general = buildPassModel(forward, ctx).find(section => section.key === 'pass');
const labels = general.rows.map(row => row.label);
expect(labels.indexOf('internal before passes')).to.be.below(labels.indexOf('before passes'));
// listed once, not again among the other properties
const props = buildPassModel(forward, ctx).find(section => section.key === 'props');
expect(props?.rows.map(row => row.label) ?? []).to.not.include('internalBeforePasses');
});

it('reads the camera use from a forward pass, or from its steps on engines before 2.24', function () {
const { app, forward, step } = forwardScene([instance('wall')]);
const second = { ...step, transparent: true };
forward.layerRenderSteps = [step, second];
const flags = () => {
const frame = captureFrameGraph(app);
const [, ...stepRows] = passRows(frame, app.graphicsDevice);
const ctx = { device: app.graphicsDevice, frame, pages: new Map(), filter: '', stable: true };
return stepRows.map((row) => {
const labels = buildStepModel(row.item, { ...ctx, passKey: row.key })[0].rows.map(r => r.label);
return [row.cells.at(-1).text.includes('· first'), row.cells.at(-1).text.includes('· last'),
labels.includes('first camera use'), labels.includes('last camera use')];
});
};

// the pass renders a single camera, so its flags belong to its first and its last step
forward.firstCameraUse = true;
forward.lastCameraUse = true;
expect(flags()).to.deep.equal([[true, false, true, false], [false, true, false, true]]);
forward.firstCameraUse = false;
expect(flags()).to.deep.equal([[false, false, false, false], [false, true, false, true]]);

// engines before 2.24 flag each step
delete forward.firstCameraUse;
delete forward.lastCameraUse;
second.firstCameraUse = true;
second.lastCameraUse = true;
expect(flags()).to.deep.equal([[false, false, false, false], [true, true, true, true]]);
});

it('shows whether a pass requires cubemaps only on engines before 2.24, which have the property', function () {
const app = /** @type {any} */ ({ graphicsDevice: { backBuffer: {}, gpuProfiler: null }, frameGraph: { passes: [] } });
const ctx = { device: app.graphicsDevice, frame: null, pages: new Map(), filter: '', stable: true };
const labels = p => buildPassModel(p, ctx).find(section => section.key === 'pass').rows.map(row => row.label);

expect(labels(pass('Current'))).to.not.include('requires cubemaps');
expect(labels(pass('Earlier', { requiresCubemaps: false }))).to.include('requires cubemaps');
});
});

describe('Inspector viewport tools', function () {
Expand Down
Loading