Skip to content
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
58ea366
reference-counting option for PrimitiveCollection constructor
pmconne May 22, 2025
6909c7d
Reference-counting in PrimitiveCollection
pmconne May 23, 2025
ca9939a
createContextFromSharedContext
pmconne May 25, 2025
54fedc0
SharedSceneContext
pmconne May 25, 2025
a52323e
SharedContextSpec
pmconne May 25, 2025
329e797
auto-destroy option
pmconne May 25, 2025
706b751
Scene tests
pmconne May 25, 2025
75d44ed
don't use context._gl.drawingBufferWidth/Height directly
pmconne May 25, 2025
10ee496
drawingBufferWidth/Height
pmconne May 25, 2025
3c1b668
readPixels gets default width/height from this, not gl
pmconne May 25, 2025
9a6159e
docs
pmconne May 26, 2025
cdb10c8
test rendering background color using shared context.
pmconne May 26, 2025
0325745
blit
pmconne May 26, 2025
03eb3af
Viewer accepts SharedContext
pmconne May 26, 2025
6bd19ae
WIP sandcastle
pmconne May 26, 2025
8213b0b
DataSourceDisplay reference-counting
pmconne May 26, 2025
de6c74a
primitives
pmconne May 26, 2025
df48524
Revert "DataSourceDisplay reference-counting"
pmconne May 26, 2025
2be94f7
clean up sandcastle
pmconne May 26, 2025
795e308
add a working shared primitive
pmconne May 26, 2025
e219831
Fix ImageBasedLighting; tweak sandcastle.
pmconne May 26, 2025
ad03f92
Merge branch 'main' into pmc/shared-context
pmconne Jul 7, 2025
1fddcff
Make new APIs private; split reference-counting from destroyPrimitives
pmconne Jul 7, 2025
cc811ba
Try to indicate tests require WebGL.
pmconne Jul 7, 2025
54ded89
skip tests if WebGL stubbed.
pmconne Jul 7, 2025
7d8a499
Remove doc refs to SharedContext
pmconne Jul 7, 2025
1a9530b
not helpful
pmconne Jul 7, 2025
bad8500
Move sandcastle to development directory
pmconne Jul 7, 2025
697514b
fine jasmine you win
pmconne Jul 7, 2025
b2bfe97
I yield!
pmconne Jul 7, 2025
852582f
rm SharedContext from API doc
pmconne Jul 7, 2025
378e1eb
missing file extension in import statement
pmconne Jul 7, 2025
96b703e
Tweak inline docs
ggetz Jul 11, 2025
e13f814
Fix docs tag, update unit tests
ggetz Jul 11, 2025
481cb57
Remove TODO (issue filed)
pmconne Jul 23, 2025
18bf6de
Merge branch 'main' into pmc/shared-context
pmconne Jul 23, 2025
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
1 change: 1 addition & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"version": "0.2.0",
"configurations": [

{
"type": "node",
"request": "launch",
Expand Down
123 changes: 123 additions & 0 deletions Apps/Sandcastle/gallery/development/Shared Context.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<meta name="description" content="Multiple views synced across time and space." />
<meta name="cesium-sandcastle-labels" content="Beginner, Showcases,New in 1.129" />
<title>Cesium Demo</title>
<script type="text/javascript" src="../Sandcastle-header.js"></script>
<script type="module" src="../load-cesium-es6.js"></script>
</head>
<body class="sandcastle-loading" data-sandcastle-bucket="bucket-requirejs.html">
<style>
@import url(../templates/bucket.css);
#cesiumContainer {
display: flex;
width: 100%;
height: 100%;
}
#view3D {
display: inline-block;
width: 100%;
}
#view2D {
display: inline-block;
width: 100%;
}
</style>
<div id="cesiumContainer" class="fullSize">
<div id="view3D"></div>
<div id="view2D"></div>
</div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar"></div>
<script id="cesium_sandcastle_script">
window.startup = async function (Cesium) {
"use strict";
//Sandcastle_Begin
const contextOptions = new Cesium.SharedContext();
// const contextOptions = undefined;
// Uncomment the line above and comment out the one preceding it to illustrate how primitives cannot be shared between scenes by default.

const options = {
contextOptions,
fullscreenButton: false,
sceneModePicker: false,
};

const view1 = new Cesium.Viewer("view3D", options);
const view2 = new Cesium.Viewer("view2D", options);

// Add the same entity to both viewers. Each viewer will create separate WebGL resources to draw it.
const greenCylinder = {
name: "Green cylinder with black outline",
position: Cesium.Cartesian3.fromDegrees(-100.0, 40.0, 200000.0),
cylinder: {
length: 400000.0,
topRadius: 200000.0,
bottomRadius: 200000.0,
material: Cesium.Color.GREEN.withAlpha(0.5),
outline: true,
outlineColor: Cesium.Color.BLACK,
},
};

view1.entities.add(greenCylinder);
view2.entities.add(greenCylinder);

// Add the same cylinder primitive to both viewers. Each will use the same WebGL resources to draw it.
const cylinder = new Cesium.CylinderGeometry({
length: 400000.0,
topRadius: 200000.0,
bottomRadius: 200000.0,
});
const geometry = Cesium.CylinderGeometry.createGeometry(cylinder);
const primitive = new Cesium.Primitive({
geometryInstances: new Cesium.GeometryInstance({
geometry,
modelMatrix: Cesium.Matrix4.multiplyByTranslation(
Cesium.Transforms.eastNorthUpToFixedFrame(
Cesium.Cartesian3.fromDegrees(-95.59777, 40.03883),
),
new Cesium.Cartesian3(0.0, 0.0, 500000.0),
new Cesium.Matrix4(),
),
id: "red cylinder",
attributes: {
color: Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.RED),
},
}),
appearance: new Cesium.PerInstanceColorAppearance(),
asynchronous: false,
});

view1.scene.primitives.add(primitive);
view2.scene.primitives.add(primitive);

// Add the same tileset to both viewers. Each viewer will use the same WebGL resources to draw it.
const tileset = await Cesium.Cesium3DTileset.fromIonAssetId(2464651);
for (const view of [view1, view2]) {
view.scene.primitives.add(tileset);
view.zoomTo(
tileset,
new Cesium.HeadingPitchRange(0.5, -0.2, tileset.boundingSphere.radius * 4.0),
);
}
//Sandcastle_End
Sandcastle.finishedLoading();
};
if (typeof Cesium !== "undefined") {
window.startupCalled = true;
window.startup(Cesium).catch((error) => {
"use strict";
console.error(error);
});
}
</script>
</body>
</html>
12 changes: 8 additions & 4 deletions packages/engine/Source/Renderer/Context.js
Original file line number Diff line number Diff line change
Expand Up @@ -1415,6 +1415,10 @@ Context.prototype.draw = function (
continueDraw(this, drawCommand, shaderProgram, uniformMap);
};

Context.prototype.beginFrame = function () {
// A no-op. Overridden when drawing to a SharedContext.
};

Context.prototype.endFrame = function () {
const gl = this._gl;
gl.useProgram(null);
Expand Down Expand Up @@ -1442,8 +1446,8 @@ Context.prototype.endFrame = function () {
* @param {object} readState An object with the following properties:
* @param {number} [readState.x=0] The x offset of the rectangle to read from.
* @param {number} [readState.y=0] The y offset of the rectangle to read from.
* @param {number} [readState.width=gl.drawingBufferWidth] The width of the rectangle to read from.
* @param {number} [readState.height=gl.drawingBufferHeight] The height of the rectangle to read from.
* @param {number} [readState.width=this.drawingBufferWidth] The width of the rectangle to read from.
* @param {number} [readState.height=this.drawingBufferHeight] The height of the rectangle to read from.
* @param {Framebuffer} [readState.framebuffer] The framebuffer to read from. If undefined, the read will be from the default framebuffer.
* @returns {Uint8Array|Uint16Array|Float32Array|Uint32Array} The pixels in the specified rectangle.
*/
Expand All @@ -1453,8 +1457,8 @@ Context.prototype.readPixels = function (readState) {
readState = readState ?? Frozen.EMPTY_OBJECT;
const x = Math.max(readState.x ?? 0, 0);
const y = Math.max(readState.y ?? 0, 0);
const width = readState.width ?? gl.drawingBufferWidth;
const height = readState.height ?? gl.drawingBufferHeight;
const width = readState.width ?? this.drawingBufferWidth;
const height = readState.height ?? this.drawingBufferHeight;
const framebuffer = readState.framebuffer;

//>>includeStart('debug', pragmas.debug);
Expand Down
6 changes: 4 additions & 2 deletions packages/engine/Source/Renderer/Renderbuffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ function Renderbuffer(options) {
const maximumRenderbufferSize = ContextLimits.maximumRenderbufferSize;

const format = options.format ?? RenderbufferFormat.RGBA4;
const width = defined(options.width) ? options.width : gl.drawingBufferWidth;
const width = defined(options.width)
? options.width
: context.drawingBufferWidth;
const height = defined(options.height)
? options.height
: gl.drawingBufferHeight;
: context.drawingBufferHeight;
const numSamples = options.numSamples ?? 1;

//>>includeStart('debug', pragmas.debug);
Expand Down
187 changes: 187 additions & 0 deletions packages/engine/Source/Renderer/SharedContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import clone from "../Core/clone.js";
import destroyObject from "../Core/destroyObject.js";
import DeveloperError from "../Core/DeveloperError.js";
import Context from "./Context.js";

/**
* Enables a single WebGL context to be used by any number of {@link Scene}s.
* You can pass a SharedContext in place of a {@link ContextOptions} to the constructors of {@link Scene}, {@link CesiumWidget}, and {@link Viewer}.
* {@link Primitive}s associated with the shared WebGL context can be displayed in any Scene that uses the same context.
* The context renders each Scene to an off-screen canvas, then blits the result to that Scene's on-screen canvas.
*
* @private
* @alias SharedContext
* @constructor
*
* @param {object} [options] Object with the following properties:
* @param {ContextOptions} [options.contextOptions] Context and WebGL creation properties.
* @param {boolean} [options.autoDestroy=true] Destroys this context and all of its WebGL resources after all Scenes using the context are destroyed.

* @see {@link http://www.khronos.org/registry/webgl/specs/latest/#5.2|WebGLContextAttributes}
*
* @example
* // Create two Scenes sharing a single WebGL context
* const context = new Cesium.SharedContext();
* const scene1 = new Cesium.Scene({
* canvas: canvas1,
* contextOptions: context,
* });
* const scene2 = new Cesium.Scene({
* canvas: canvas2,
* contextOptions: context,
* });
*/
function SharedContext(options) {
this._autoDestroy = options?.autoDestroy ?? true;
this._canvas = document.createElement("canvas");
this._context = new Context(this._canvas, clone(options?.contextOptions));
this._canvases = [];
}

/**
* Creates an instance of {@link Context} that manages the shared WebGL context for a specific canvas.
* @param {HTMLCanvasElement} canvas The canvas element to which the context will be associated
* @returns {Context} The created context instance
* @private
*/
SharedContext.prototype.createSceneContext = function (canvas) {
const context2d = canvas.getContext("2d", { alpha: true });

//>>includeStart('debug', pragmas.debug);
if (!context2d) {
throw new DeveloperError(
"canvas used with SharedContext must provide a 2d context",
);
}

if (this._canvases.includes(canvas)) {
throw new DeveloperError("canvas is already associated with a scene");
}
//>>includeEnd('debug');

const sharedContext = this;
sharedContext._canvases.push(canvas);

let isDestroyed = false;
const destroy = function () {
isDestroyed = true;
const index = sharedContext._canvases.indexOf(canvas);
if (-1 !== index) {
sharedContext._canvases.splice(index, 1);
if (sharedContext._autoDestroy && sharedContext._canvases.length === 0) {
sharedContext.destroy();
}
}
};

const beginFrame = function () {
// Ensure the off-screen canvas is at least as large as the on-screen canvas.
const sharedCanvas = sharedContext._context.canvas;

const width = this.drawingBufferWidth;
if (sharedCanvas.width < width) {
sharedCanvas.width = width;
}

const height = this.drawingBufferHeight;
if (sharedCanvas.height < height) {
sharedCanvas.height = height;
}
};

const endFrame = function () {
// Blit the image from the off-screen canvas to the on-screen canvas.
const w = this.drawingBufferWidth;
const h = this.drawingBufferHeight;
const yOffset = sharedContext._context.canvas.height - h; // drawImage has top as Y=0, GL has bottom as Y=0
context2d.drawImage(
sharedContext._context.canvas,
0,
yOffset,
w,
h,
0,
0,
w,
h,
);

// Do normal post-frame cleanup.
sharedContext._context.endFrame();
};

const proxy = new Proxy(this._context, {
get(target, prop, receiver) {
if (prop === "isDestroyed") {
return function () {
return isDestroyed;
};
} else if (isDestroyed) {
//>>includeStart('debug', pragmas.debug);
throw new DeveloperError(
"This object was destroyed, i.e., destroy() was called.",
);
//>>includeEnd('debug');
}

switch (prop) {
case "_canvas":
return canvas;
case "destroy":
return destroy;
// ###TODO: When will this be inaccurate? device pixel ratio? Canvas larger than maximum drawing buffer dimensions supported by WebGL implementation?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to address this now, or in a later PR?
We typically don't merge code into main with TODO comments, and prefer to document them in an issue if they are not addressed.

The max viewport dimensions are available in ContextLimits.maximumViewportWidth/ContextLimits.maximumViewportHeight.

Pixel ratio is less straightforward, as it's "owned" by Scene.frameState. Perhaps it should live in Context instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed here, pixel ratio is tricky and not 100% solved. An API to obtain the dimensions of a canvas (or other element) in integer device pixels solves the problem, except it's not yet supported in Safari.

iTwin.js attempts to account for DPR here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created #12762 to track this outside the code / this PR. I propose removing this TODO from this code, resolving this conversation, possibly merging this PR, and then scheduling some work on that issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ggetz Feel free to resolve your comment here if you are satisfied with the above plan.

case "drawingBufferWidth":
return canvas.width;
case "drawingBufferHeight":
return canvas.height;
case "beginFrame":
return beginFrame;
case "endFrame":
return endFrame;
default:
return Reflect.get(target, prop, receiver);
}
},
});

return proxy;
};

/**
* Destroys the WebGL resources held by this object. Destroying an object allows for deterministic
* release of WebGL resources, instead of relying on the garbage collector to destroy this object.
* <br /><br />
* Once an object is destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
* assign the return value (<code>undefined</code>) to the object as done in the example.
* <br /><br />
* By default, a SharedContext is destroyed automatically once the last Scene using it is destroyed, in which case it
* is not necessary to call this method directly.
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*
* @example
* context = context && context.destroy();
*
* @see SharedContext#isDestroyed
*/
SharedContext.prototype.destroy = function () {
this._context.destroy();
destroyObject(this);
};

/**
* Returns true if this object was destroyed; otherwise, false.
* <br /><br />
* If this object was destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
*
* @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
*
* @see SharedContext#destroy
*/
SharedContext.prototype.isDestroyed = function () {
return false;
};

export default SharedContext;
9 changes: 4 additions & 5 deletions packages/engine/Source/Renderer/Texture.js
Original file line number Diff line number Diff line change
Expand Up @@ -623,13 +623,12 @@ Texture.fromFramebuffer = function (options) {
//>>includeEnd('debug');

const context = options.context;
const gl = context._gl;
const {
pixelFormat = PixelFormat.RGB,
framebufferXOffset = 0,
framebufferYOffset = 0,
width = gl.drawingBufferWidth,
height = gl.drawingBufferHeight,
width = context.drawingBufferWidth,
height = context.drawingBufferHeight,
framebuffer,
} = options;

Expand All @@ -656,12 +655,12 @@ Texture.fromFramebuffer = function (options) {
framebufferYOffset,
0,
);
if (framebufferXOffset + width > gl.drawingBufferWidth) {
if (framebufferXOffset + width > context.drawingBufferWidth) {
throw new DeveloperError(
"framebufferXOffset + width must be less than or equal to drawingBufferWidth",
);
}
if (framebufferYOffset + height > gl.drawingBufferHeight) {
if (framebufferYOffset + height > context.drawingBufferHeight) {
throw new DeveloperError(
"framebufferYOffset + height must be less than or equal to drawingBufferHeight.",
);
Expand Down
Loading