Skip to content
This repository has been archived by the owner on Mar 8, 2023. It is now read-only.

HARP-15210: Wait for texture GPU upload on TileGeometryCreator. #2225

Merged
merged 2 commits into from
Jun 23, 2021
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
53 changes: 36 additions & 17 deletions @here/harp-mapview-decoder/test/TileGeometryLoaderTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ describe("TileGeometryLoader", function () {
let dataSource: DataSource;
let mapView: MapView;
let geometryLoader: TileGeometryLoader;
let sandbox: any;

before(function () {
tileKey = TileKey.fromRowColumnLevel(0, 0, 0);
Expand All @@ -112,11 +111,10 @@ describe("TileGeometryLoader", function () {
dataSource.attach(mapView);
tile = dataSource.getTile(tileKey)!;
geometryLoader = (tile as any).m_tileGeometryLoader!;
sandbox = sinon.createSandbox();
});

afterEach(function () {
sandbox.restore();
sinon.restore();
});

describe("tile preprocessing", function () {
Expand Down Expand Up @@ -157,8 +155,8 @@ describe("TileGeometryLoader", function () {
tile.decodedTile = createFakeDecodedTile();

const geometryCreator = TileGeometryCreator.instance;
const spyProcessTechniques = sandbox.spy(geometryCreator, "processTechniques") as any;
const spySetDecodedTile = sandbox.spy(geometryLoader, "setDecodedTile") as any;
const spyProcessTechniques = sinon.spy(geometryCreator, "processTechniques") as any;
const spySetDecodedTile = sinon.spy(geometryLoader, "setDecodedTile") as any;
expect(spyProcessTechniques.callCount).equal(0);
expect(spySetDecodedTile.callCount).equal(0);

Expand All @@ -181,12 +179,33 @@ describe("TileGeometryLoader", function () {
expect(geometryLoader.isFinished).to.be.true;
});

it("should wait until all textures are ready to render", async function () {
tile.decodedTile = createFakeDecodedTile();

const geometryCreator = TileGeometryCreator.instance;
let makeTexturesReady: () => void | undefined;
const texturesPromise = new Promise<void>(resolve => {
makeTexturesReady = resolve;
});
sinon.stub(geometryCreator, "createAllGeometries").returns(texturesPromise);
geometryLoader!.update();
expect(mapView.taskQueue.numItemsLeft(TileTaskGroups.CREATE)).equal(1);
expect(mapView.taskQueue.processNext(TileTaskGroups.CREATE)).equal(true);

await wait();
expect(geometryLoader.isFinished).to.be.false;

makeTexturesReady!();
await wait();
expect(geometryLoader.isFinished).to.be.true;
});

it("should create geometry for decoded tile only once (via taskqueue)", async function () {
tile.decodedTile = createFakeDecodedTile();

const geometryCreator = TileGeometryCreator.instance;
const spyProcessTechniques = sandbox.spy(geometryCreator, "processTechniques") as any;
const spyCreateGeometries = sandbox.spy(geometryCreator, "createAllGeometries") as any;
const spyProcessTechniques = sinon.spy(geometryCreator, "processTechniques") as any;
const spyCreateGeometries = sinon.spy(geometryCreator, "createAllGeometries") as any;
expect(spyCreateGeometries.callCount).equal(0);
expect(spyProcessTechniques.callCount).equal(0);

Expand All @@ -212,8 +231,8 @@ describe("TileGeometryLoader", function () {
tile.decodedTile = createFakeDecodedTile();

const geometryCreator = TileGeometryCreator.instance;
const spyProcessTechniques = sandbox.spy(geometryCreator, "processTechniques") as any;
const spyCreateGeometries = sandbox.spy(geometryCreator, "createAllGeometries") as any;
const spyProcessTechniques = sinon.spy(geometryCreator, "processTechniques") as any;
const spyCreateGeometries = sinon.spy(geometryCreator, "createAllGeometries") as any;
expect(spyCreateGeometries.callCount).equal(0);
expect(spyProcessTechniques.callCount).equal(0);

Expand All @@ -234,8 +253,8 @@ describe("TileGeometryLoader", function () {
tile.decodedTile = createFakeDecodedTile();

const geometryCreator = TileGeometryCreator.instance;
const spyProcessTechniques = sandbox.spy(geometryCreator, "processTechniques") as any;
const spyCreateGeometries = sandbox.spy(geometryCreator, "createAllGeometries") as any;
const spyProcessTechniques = sinon.spy(geometryCreator, "processTechniques") as any;
const spyCreateGeometries = sinon.spy(geometryCreator, "createAllGeometries") as any;
expect(spyCreateGeometries.callCount).equal(0);
expect(spyProcessTechniques.callCount).equal(0);

Expand All @@ -257,8 +276,8 @@ describe("TileGeometryLoader", function () {
tile.decodedTile = createFakeDecodedTile();

const geometryCreator = TileGeometryCreator.instance;
const spyProcessTechniques = sandbox.spy(geometryCreator, "processTechniques") as any;
const spyCreateGeometries = sandbox.spy(geometryCreator, "createAllGeometries") as any;
const spyProcessTechniques = sinon.spy(geometryCreator, "processTechniques") as any;
const spyCreateGeometries = sinon.spy(geometryCreator, "createAllGeometries") as any;
expect(spyCreateGeometries.callCount).equal(0);
expect(spyProcessTechniques.callCount).equal(0);

Expand Down Expand Up @@ -295,8 +314,8 @@ describe("TileGeometryLoader", function () {
tile.decodedTile = createFakeDecodedTile();

const geometryCreator = TileGeometryCreator.instance;
const spyProcessTechniques = sandbox.spy(geometryCreator, "processTechniques") as any;
const spyCreateGeometries = sandbox.spy(geometryCreator, "createAllGeometries") as any;
const spyProcessTechniques = sinon.spy(geometryCreator, "processTechniques") as any;
const spyCreateGeometries = sinon.spy(geometryCreator, "createAllGeometries") as any;
expect(spyCreateGeometries.callCount).equal(0);
expect(spyProcessTechniques.callCount).equal(0);

Expand Down Expand Up @@ -331,8 +350,8 @@ describe("TileGeometryLoader", function () {
tile.decodedTile = createFakeDecodedTile();

const geometryCreator = TileGeometryCreator.instance;
const spyProcessTechniques = sandbox.spy(geometryCreator, "processTechniques") as any;
const spyCreateGeometries = sandbox.spy(geometryCreator, "createAllGeometries") as any;
const spyProcessTechniques = sinon.spy(geometryCreator, "processTechniques") as any;
const spyCreateGeometries = sinon.spy(geometryCreator, "createAllGeometries") as any;
expect(spyCreateGeometries.callCount).equal(0);
expect(spyProcessTechniques.callCount).equal(0);

Expand Down
155 changes: 81 additions & 74 deletions @here/harp-mapview/lib/DecodedTileHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ function createTextureFromRawImage(
onError: (error: ErrorEvent | string) => void
) {
const properties = textureBuffer.dataTextureProperties;
if (properties !== undefined) {
if (properties) {
const textureDataType: THREE.TextureDataType | undefined = properties.type
? toTextureDataType(properties.type)
: undefined;
Expand All @@ -131,102 +131,106 @@ function createTextureFromRawImage(
}
}

function initTextureProperties(texture: THREE.Texture, properties?: TextureProperties) {
if (!properties) {
return;
}
if (properties.wrapS !== undefined) {
texture.wrapS = toWrappingMode(properties.wrapS);
}
if (properties.wrapT !== undefined) {
texture.wrapT = toWrappingMode(properties.wrapT);
}
if (properties.magFilter !== undefined) {
texture.magFilter = toTextureFilter(properties.magFilter);
}
if (properties.minFilter !== undefined) {
texture.minFilter = toTextureFilter(properties.minFilter);
}
if (properties.flipY !== undefined) {
texture.flipY = properties.flipY;
}
if (properties.repeatU !== undefined) {
texture.repeat.x = properties.repeatU;
}
if (properties.repeatV !== undefined) {
texture.repeat.y = properties.repeatV;
}
}

function createTexture(
nzjony marked this conversation as resolved.
Show resolved Hide resolved
material: THREE.Material,
texturePropertyName: string,
options: MaterialOptions,
textureReadyCallback?: (texture: THREE.Texture) => void
) {
options: MaterialOptions
): Promise<THREE.Texture> | undefined {
const technique = options.technique;
let textureProperty = (technique as any)[texturePropertyName];
if (textureProperty === undefined) {
return;
return undefined;
}

const onLoad = (texture: THREE.Texture) => {
const properties = (technique as any)[
texturePropertyName + "Properties"
] as TextureProperties;
if (properties !== undefined) {
if (properties.wrapS !== undefined) {
texture.wrapS = toWrappingMode(properties.wrapS);
}
if (properties.wrapT !== undefined) {
texture.wrapT = toWrappingMode(properties.wrapT);
}
if (properties.magFilter !== undefined) {
texture.magFilter = toTextureFilter(properties.magFilter);
}
if (properties.minFilter !== undefined) {
texture.minFilter = toTextureFilter(properties.minFilter);
}
if (properties.flipY !== undefined) {
texture.flipY = properties.flipY;
}
if (properties.repeatU !== undefined) {
texture.repeat.x = properties.repeatU;
}
if (properties.repeatV !== undefined) {
texture.repeat.y = properties.repeatV;
}
}
(material as any)[texturePropertyName] = texture;
material.needsUpdate = true;
const texturePromise = new Promise<THREE.Texture>((resolve, reject) => {
const onLoad = (texture: THREE.Texture) => {
const properties: TextureProperties | undefined = (technique as any)[
texturePropertyName + "Properties"
];
initTextureProperties(texture, properties);
(material as any)[texturePropertyName] = texture;
material.needsUpdate = true;
resolve(texture);
};
const onError = (error: ErrorEvent | string) => {
logger.error("#createMaterial: Failed to load texture: ", error);
reject(error);
};

if (textureReadyCallback) {
textureReadyCallback(texture);
}
};

const onError = (error: ErrorEvent | string) => {
logger.error("#createMaterial: Failed to load texture: ", error);
};

if (Expr.isExpr(textureProperty)) {
textureProperty = getPropertyValue(textureProperty, options.env);
if (!textureProperty) {
// Expression may evaluate to a valid texture at any time, create a fake texture to
// avoid shader recompilation.
onLoad(new THREE.Texture());
return;
if (Expr.isExpr(textureProperty)) {
textureProperty = getPropertyValue(textureProperty, options.env);
if (!textureProperty) {
// Expression may evaluate to a valid texture at any time, create a fake texture to
// avoid shader recompilation.
onLoad(new THREE.Texture());
return;
}
}
}

if (typeof textureProperty === "string") {
createTextureFromURL(textureProperty, onLoad, onError, false);
} else if (isTextureBuffer(textureProperty)) {
if (textureProperty.type === "image/raw") {
createTextureFromRawImage(textureProperty, onLoad, onError);
} else {
const textureBlob = new Blob([textureProperty.buffer], {
type: textureProperty.type
});
createTextureFromURL(URL.createObjectURL(textureBlob), onLoad, onError, true);
if (typeof textureProperty === "string") {
createTextureFromURL(textureProperty, onLoad, onError, false);
} else if (isTextureBuffer(textureProperty)) {
if (textureProperty.type === "image/raw") {
createTextureFromRawImage(textureProperty, onLoad, onError);
} else {
const textureBlob = new Blob([textureProperty.buffer], {
type: textureProperty.type
});
createTextureFromURL(URL.createObjectURL(textureBlob), onLoad, onError, true);
}
} else if (
typeof textureProperty === "object" &&
(textureProperty.nodeName === "IMG" || textureProperty.nodeName === "CANVAS")
) {
onLoad(new THREE.CanvasTexture(textureProperty));
}
} else if (
typeof textureProperty === "object" &&
(textureProperty.nodeName === "IMG" || textureProperty.nodeName === "CANVAS")
) {
onLoad(new THREE.CanvasTexture(textureProperty));
}
});
return texturePromise;
}

/**
* Create a material, depending on the rendering technique provided in the options.
*
* @param rendererCapabilities - The capabilities of the renderer that will use the material.
* @param options - The material options the subsequent functions need.
* @param materialUpdateCallback - Optional callback when the material gets updated,
* e.g. after texture loading.
* @param onTextureCreated - Optional callback for each texture created for the material, getting
* a promise that will be resolved once the texture is loaded. Texture is not uploaded to GPU.
*
* @returns new material instance that matches `technique.name`
* @returns new material instance that matches `technique.name`.
*
* @internal
*/
export function createMaterial(
rendererCapabilities: THREE.WebGLCapabilities,
options: MaterialOptions,
textureReadyCallback?: (texture: THREE.Texture) => void
onTextureCreated?: (texture: Promise<THREE.Texture>) => void
): THREE.Material | undefined {
const technique = options.technique;
const Constructor = getMaterialConstructor(technique, options.shadowsEnabled === true);
Expand Down Expand Up @@ -260,9 +264,12 @@ export function createMaterial(
material.depthTest = isExtrudedPolygonTechnique(technique) && technique.depthTest !== false;

if (supportsTextures(technique)) {
TEXTURE_PROPERTY_KEYS.forEach((texturePropertyName: string) =>
createTexture(material, texturePropertyName, options, textureReadyCallback)
);
TEXTURE_PROPERTY_KEYS.forEach((texturePropertyName: string) => {
const texturePromise = createTexture(material, texturePropertyName, options);
if (texturePromise) {
onTextureCreated?.(texturePromise);
}
});
}

if (isShaderTechnique(technique)) {
Expand Down
Loading