Browse Source

Add asynchronous texture read pixels

Popov72 4 years ago
parent
commit
2497802cb7

+ 3 - 3
src/Engines/Extensions/engine.readTexture.ts

@@ -5,11 +5,11 @@ import { Nullable } from '../../types';
 declare module "../../Engines/thinEngine" {
     export interface ThinEngine {
         /** @hidden */
-        _readTexturePixels(texture: InternalTexture, width: number, height: number, faceIndex?: number, level?: number, buffer?: Nullable<ArrayBufferView>): ArrayBufferView;
+        _readTexturePixels(texture: InternalTexture, width: number, height: number, faceIndex?: number, level?: number, buffer?: Nullable<ArrayBufferView>): Promise<ArrayBufferView>;
     }
 }
 
-ThinEngine.prototype._readTexturePixels = function(texture: InternalTexture, width: number, height: number, faceIndex = -1, level = 0, buffer: Nullable<ArrayBufferView> = null): ArrayBufferView {
+ThinEngine.prototype._readTexturePixels = function(texture: InternalTexture, width: number, height: number, faceIndex = -1, level = 0, buffer: Nullable<ArrayBufferView> = null): Promise<ArrayBufferView> {
     let gl = this._gl;
     if (!gl) {
         throw new Error ("Engine does not have gl rendering context.");
@@ -51,5 +51,5 @@ ThinEngine.prototype._readTexturePixels = function(texture: InternalTexture, wid
     gl.readPixels(0, 0, width, height, gl.RGBA, readType, <DataView>buffer);
     gl.bindFramebuffer(gl.FRAMEBUFFER, this._currentFramebuffer);
 
-    return buffer;
+    return Promise.resolve(buffer);
 };

+ 70 - 8
src/Engines/WebGPU/webgpuBufferManager.ts

@@ -76,17 +76,79 @@ export class WebGPUBufferManager {
         this._device.defaultQueue.writeBuffer(buffer, dstByteOffset + offset, src.buffer, chunkStart + offset, byteLength - offset);
     }
 
-    public readDataFromBuffer(buffer: GPUBuffer, size: number, offset = 0, destroyBuffer = true): Promise<Uint8Array> {
+    private _FromHalfFloat(value: number): number {
+        const s = (value & 0x8000) >> 15;
+        const e = (value & 0x7C00) >> 10;
+        const f = value & 0x03FF;
+
+        if (e === 0) {
+            return (s ? -1 : 1) * Math.pow(2, -14) * (f / Math.pow(2, 10));
+        } else if (e == 0x1F) {
+            return f ? NaN : ((s ? -1 : 1) * Infinity);
+        }
+
+        return (s ? -1 : 1) * Math.pow(2, e - 15) * (1 + (f / Math.pow(2, 10)));
+    }
+
+    private _GetHalfFloatAsFloatRGBAArrayBuffer(width: number, height: number, dataOffset: number, dataLength: number, arrayBuffer: ArrayBuffer, destArray?: Float32Array): Float32Array {
+        if (!destArray) {
+            destArray = new Float32Array(dataLength);
+        }
+        const srcData = new Uint16Array(arrayBuffer, dataOffset);
+        let index = 0;
+        for (let y = 0; y < height; y++) {
+            for (let x = 0; x < width; x++) {
+                const srcPos = (x + y * width) * 4;
+                destArray[index] = this._FromHalfFloat(srcData[srcPos]);
+                destArray[index + 1] = this._FromHalfFloat(srcData[srcPos + 1]);
+                destArray[index + 2] = this._FromHalfFloat(srcData[srcPos + 2]);
+                destArray[index + 3] = this._FromHalfFloat(srcData[srcPos + 3]);
+                index += 4;
+            }
+        }
+
+        return destArray;
+    }
+
+    public readDataFromBuffer(gpuBuffer: GPUBuffer, size: number, width: number, height: number, floatFormat = 0, offset = 0, buffer: Nullable<ArrayBufferView> = null, destroyBuffer = true): Promise<ArrayBufferView> {
         return new Promise((resolve, reject) => {
-            buffer.mapAsync(GPUMapMode.READ, offset, size).then(() => {
-                const copyArrayBuffer = buffer.getMappedRange(offset, size);
-                const data = new Uint8Array(size);
-                data.set(new Uint8Array(copyArrayBuffer));
-                buffer.unmap();
+            gpuBuffer.mapAsync(GPUMapMode.READ, offset, size).then(() => {
+                const copyArrayBuffer = gpuBuffer.getMappedRange(offset, size);
+                let data: Nullable<ArrayBufferView> | Uint8Array | Float32Array = buffer;
+                if (data === null) {
+                    switch (floatFormat) {
+                        case 0: // byte format
+                            data = new Uint8Array(size);
+                            (data as Uint8Array).set(new Uint8Array(copyArrayBuffer));
+                            break;
+                        case 1: // half float
+                            data = this._GetHalfFloatAsFloatRGBAArrayBuffer(width, height, 0, size / 2, copyArrayBuffer);
+                            break;
+                        case 2: // float
+                            data = new Float32Array(size / 4);
+                            (data as Float32Array).set(new Float32Array(copyArrayBuffer));
+                            break;
+                    }
+                } else {
+                    switch (floatFormat) {
+                        case 0: // byte format
+                            data = new Uint8Array(data.buffer);
+                            (data as Uint8Array).set(new Uint8Array(copyArrayBuffer));
+                            break;
+                        case 1: // half float
+                            data = this._GetHalfFloatAsFloatRGBAArrayBuffer(width, height, 0, size / 2, copyArrayBuffer, buffer as Float32Array);
+                            break;
+                        case 2: // float
+                            data = new Float32Array(data.buffer);
+                            (data as Float32Array).set(new Float32Array(copyArrayBuffer));
+                            break;
+                    }
+                }
+                gpuBuffer.unmap();
                 if (destroyBuffer) {
-                    buffer.destroy();
+                    this.releaseBuffer(gpuBuffer);
                 }
-                resolve(data);
+                resolve(data!);
             }, (reason) => reject(reason));
         });
     }

+ 203 - 15
src/Engines/WebGPU/webgpuTextureHelper.ts

@@ -20,6 +20,8 @@
 import * as WebGPUConstants from './webgpuConstants';
 import { Scalar } from '../../Maths/math.scalar';
 import { WebGPUBufferManager } from './webgpuBufferManager';
+import { Constants } from '../constants';
+import { Nullable } from '../../types';
 
 // TODO WEBGPU improve mipmap generation by not using the OutputAttachment flag
 // see https://github.com/toji/web-texture-tool/tree/main/src
@@ -261,30 +263,174 @@ export class WebGPUTextureHelper {
         }
     }
 
-    private _getBlockInformationFromFormat(format: GPUTextureFormat): { width: number, height: number, length: number } {
-        // TODO WEBGPU support other formats?
+    private _getTextureTypeFromFormat(format: GPUTextureFormat): number {
         switch (format) {
+            // One Component = 8 bits
+            case WebGPUConstants.TextureFormat.R8Unorm:
+            case WebGPUConstants.TextureFormat.R8Snorm:
+            case WebGPUConstants.TextureFormat.R8Uint:
+            case WebGPUConstants.TextureFormat.R8Sint:
+            case WebGPUConstants.TextureFormat.RG8Unorm:
+            case WebGPUConstants.TextureFormat.RG8Snorm:
+            case WebGPUConstants.TextureFormat.RG8Uint:
+            case WebGPUConstants.TextureFormat.RG8Sint:
+            case WebGPUConstants.TextureFormat.RGBA8Unorm:
+            case WebGPUConstants.TextureFormat.RGBA8UnormSRGB:
+            case WebGPUConstants.TextureFormat.RGBA8Snorm:
+            case WebGPUConstants.TextureFormat.RGBA8Uint:
+            case WebGPUConstants.TextureFormat.RGBA8Sint:
+            case WebGPUConstants.TextureFormat.BGRA8Unorm:
+            case WebGPUConstants.TextureFormat.BGRA8UnormSRGB:
+            case WebGPUConstants.TextureFormat.RGB10A2Unorm: // composite format - let's say it's byte...
+            case WebGPUConstants.TextureFormat.RGB9E5UFloat: // composite format - let's say it's byte...
+            case WebGPUConstants.TextureFormat.RG11B10UFloat: // composite format - let's say it's byte...
+            case WebGPUConstants.TextureFormat.Depth24UnormStencil8: // composite format - let's say it's byte...
+            case WebGPUConstants.TextureFormat.Depth32FloatStencil8: // composite format - let's say it's byte...
             case WebGPUConstants.TextureFormat.BC7RGBAUnorm:
             case WebGPUConstants.TextureFormat.BC7RGBAUnormSRGB:
-                return { width: 4, height: 4, length: 16 };
             case WebGPUConstants.TextureFormat.BC6HRGBUFloat:
-                return { width: 4, height: 4, length: 16 };
             case WebGPUConstants.TextureFormat.BC6HRGBSFloat:
-                return { width: 4, height: 4, length: 16 };
+            case WebGPUConstants.TextureFormat.BC5RGUnorm:
+            case WebGPUConstants.TextureFormat.BC5RGSnorm:
             case WebGPUConstants.TextureFormat.BC3RGBAUnorm:
             case WebGPUConstants.TextureFormat.BC3RGBAUnormSRGB:
-                return { width: 4, height: 4, length: 16 };
             case WebGPUConstants.TextureFormat.BC2RGBAUnorm:
             case WebGPUConstants.TextureFormat.BC2RGBAUnormSRGB:
-                return { width: 4, height: 4, length: 16 };
+            case WebGPUConstants.TextureFormat.BC4RUnorm:
+            case WebGPUConstants.TextureFormat.BC4RSnorm:
             case WebGPUConstants.TextureFormat.BC1RGBAUNorm:
             case WebGPUConstants.TextureFormat.BC1RGBAUnormSRGB:
-                return { width: 4, height: 4, length: 8 };
+                return Constants.TEXTURETYPE_UNSIGNED_BYTE;
+
+            // One component = 16 bits
+            case WebGPUConstants.TextureFormat.R16Uint:
+            case WebGPUConstants.TextureFormat.R16Sint:
+            case WebGPUConstants.TextureFormat.RG16Uint:
+            case WebGPUConstants.TextureFormat.RG16Sint:
+            case WebGPUConstants.TextureFormat.RGBA16Uint:
+            case WebGPUConstants.TextureFormat.RGBA16Sint:
+            case WebGPUConstants.TextureFormat.Depth16Unorm:
+                return Constants.TEXTURETYPE_UNSIGNED_SHORT;
+
+            case WebGPUConstants.TextureFormat.R16Float:
+            case WebGPUConstants.TextureFormat.RG16Float:
+            case WebGPUConstants.TextureFormat.RGBA16Float:
+                return Constants.TEXTURETYPE_HALF_FLOAT;
+
+            // One component = 32 bits
+            case WebGPUConstants.TextureFormat.R32Uint:
+            case WebGPUConstants.TextureFormat.R32Sint:
+            case WebGPUConstants.TextureFormat.RG32Uint:
+            case WebGPUConstants.TextureFormat.RG32Sint:
+            case WebGPUConstants.TextureFormat.RGBA32Uint:
+            case WebGPUConstants.TextureFormat.RGBA32Sint:
+                return Constants.TEXTURETYPE_UNSIGNED_INTEGER;
+
+            case WebGPUConstants.TextureFormat.R32Float:
+            case WebGPUConstants.TextureFormat.RG32Float:
+            case WebGPUConstants.TextureFormat.RGBA32Float:
+            case WebGPUConstants.TextureFormat.Depth32Float:
+                return Constants.TEXTURETYPE_FLOAT;
+
+            case WebGPUConstants.TextureFormat.Stencil8:
+                throw "No fixed size for Stencil8 format!";
+            case WebGPUConstants.TextureFormat.Depth24Plus:
+                throw "No fixed size for Depth24Plus format!";
+            case WebGPUConstants.TextureFormat.Depth24PlusStencil8:
+                throw "No fixed size for Depth24PlusStencil8 format!";
+        }
+
+        return Constants.TEXTURETYPE_UNSIGNED_BYTE;
+    }
 
+    private _getBlockInformationFromFormat(format: GPUTextureFormat): { width: number, height: number, length: number } {
+        switch (format) {
+            // 8 bits formats
+            case WebGPUConstants.TextureFormat.R8Unorm:
+            case WebGPUConstants.TextureFormat.R8Snorm:
+            case WebGPUConstants.TextureFormat.R8Uint:
+            case WebGPUConstants.TextureFormat.R8Sint:
+                return { width: 1, height: 1, length: 1 };
+
+            // 16 bits formats
+            case WebGPUConstants.TextureFormat.R16Uint:
+            case WebGPUConstants.TextureFormat.R16Sint:
+            case WebGPUConstants.TextureFormat.R16Float:
+            case WebGPUConstants.TextureFormat.RG8Unorm:
+            case WebGPUConstants.TextureFormat.RG8Snorm:
+            case WebGPUConstants.TextureFormat.RG8Uint:
+            case WebGPUConstants.TextureFormat.RG8Sint:
+                return { width: 1, height: 1, length: 2 };
+
+            // 32 bits formats
+            case WebGPUConstants.TextureFormat.R32Uint:
+            case WebGPUConstants.TextureFormat.R32Sint:
+            case WebGPUConstants.TextureFormat.R32Float:
+            case WebGPUConstants.TextureFormat.RG16Uint:
+            case WebGPUConstants.TextureFormat.RG16Sint:
+            case WebGPUConstants.TextureFormat.RG16Float:
+            case WebGPUConstants.TextureFormat.RGBA8Unorm:
+            case WebGPUConstants.TextureFormat.RGBA8UnormSRGB:
+            case WebGPUConstants.TextureFormat.RGBA8Snorm:
+            case WebGPUConstants.TextureFormat.RGBA8Uint:
+            case WebGPUConstants.TextureFormat.RGBA8Sint:
+            case WebGPUConstants.TextureFormat.BGRA8Unorm:
+            case WebGPUConstants.TextureFormat.BGRA8UnormSRGB:
+            case WebGPUConstants.TextureFormat.RGB9E5UFloat:
+            case WebGPUConstants.TextureFormat.RGB10A2Unorm:
+            case WebGPUConstants.TextureFormat.RG11B10UFloat:
+                return { width: 1, height: 1, length: 4 };
+
+            // 64 bits formats
+            case WebGPUConstants.TextureFormat.RG32Uint:
+            case WebGPUConstants.TextureFormat.RG32Sint:
+            case WebGPUConstants.TextureFormat.RG32Float:
+            case WebGPUConstants.TextureFormat.RGBA16Uint:
+            case WebGPUConstants.TextureFormat.RGBA16Sint:
             case WebGPUConstants.TextureFormat.RGBA16Float:
                 return { width: 1, height: 1, length: 8 };
+
+            // 128 bits formats
+            case WebGPUConstants.TextureFormat.RGBA32Uint:
+            case WebGPUConstants.TextureFormat.RGBA32Sint:
             case WebGPUConstants.TextureFormat.RGBA32Float:
                 return { width: 1, height: 1, length: 16 };
+
+            // Depth and stencil formats
+            case WebGPUConstants.TextureFormat.Stencil8:
+                throw "No fixed size for Stencil8 format!";
+            case WebGPUConstants.TextureFormat.Depth16Unorm:
+                return { width: 1, height: 1, length: 2 };
+            case WebGPUConstants.TextureFormat.Depth24Plus:
+                throw "No fixed size for Depth24Plus format!";
+            case WebGPUConstants.TextureFormat.Depth24PlusStencil8:
+                throw "No fixed size for Depth24PlusStencil8 format!";
+            case WebGPUConstants.TextureFormat.Depth32Float:
+                return { width: 1, height: 1, length: 4 };
+            case WebGPUConstants.TextureFormat.Depth24UnormStencil8:
+                return { width: 1, height: 1, length: 4 };
+            case WebGPUConstants.TextureFormat.Depth32FloatStencil8:
+                return { width: 1, height: 1, length: 5 };
+
+            // BC compressed formats usable if "texture-compression-bc" is both
+            // supported by the device/user agent and enabled in requestDevice.
+            case WebGPUConstants.TextureFormat.BC7RGBAUnorm:
+            case WebGPUConstants.TextureFormat.BC7RGBAUnormSRGB:
+            case WebGPUConstants.TextureFormat.BC6HRGBUFloat:
+            case WebGPUConstants.TextureFormat.BC6HRGBSFloat:
+            case WebGPUConstants.TextureFormat.BC5RGUnorm:
+            case WebGPUConstants.TextureFormat.BC5RGSnorm:
+            case WebGPUConstants.TextureFormat.BC3RGBAUnorm:
+            case WebGPUConstants.TextureFormat.BC3RGBAUnormSRGB:
+            case WebGPUConstants.TextureFormat.BC2RGBAUnorm:
+            case WebGPUConstants.TextureFormat.BC2RGBAUnormSRGB:
+                return { width: 4, height: 4, length: 16 };
+
+            case WebGPUConstants.TextureFormat.BC4RUnorm:
+            case WebGPUConstants.TextureFormat.BC4RSnorm:
+            case WebGPUConstants.TextureFormat.BC1RGBAUNorm:
+            case WebGPUConstants.TextureFormat.BC1RGBAUnormSRGB:
+                return { width: 4, height: 4, length: 8 };
         }
 
         return { width: 1, height: 1, length: 4 };
@@ -304,6 +450,12 @@ export class WebGPUTextureHelper {
     public updateTexture(imageBitmap: ImageBitmap | Uint8Array, gpuTexture: GPUTexture, width: number, height: number, format: GPUTextureFormat, faceIndex: number = 0, mipLevel: number = 0, invertY = false, premultiplyAlpha = false, offsetX = 0, offsetY = 0,
         commandEncoder?: GPUCommandEncoder): void
     {
+        const useOwnCommandEncoder = commandEncoder === undefined;
+
+        if (useOwnCommandEncoder) {
+            commandEncoder = this._device.createCommandEncoder({});
+        }
+
         const blockInformation = this._getBlockInformationFromFormat(format);
 
         const textureCopyView: GPUTextureCopyView = {
@@ -338,18 +490,18 @@ export class WebGPUTextureHelper {
 
                 buffer.unmap();
 
-                // TODO WEBGPU should we reuse the passed in commandEncoder (if defined)? If yes, we must delay the destroying of the buffer until after the commandEncoder has been submitted...
-                const copyCommandEncoder = this._device.createCommandEncoder({});
-
-                copyCommandEncoder.copyBufferToTexture({
+                commandEncoder!.copyBufferToTexture({
                     buffer: buffer,
                     offset: 0,
-                    bytesPerRow: Math.ceil(width / blockInformation.width) * blockInformation.length
+                    bytesPerRow
                 }, textureCopyView, textureExtent);
 
-                this._device.defaultQueue.submit([copyCommandEncoder.finish()]);
+                if (useOwnCommandEncoder) {
+                    this._device.defaultQueue.submit([commandEncoder!.finish()]);
+                    commandEncoder = null as any;
+                }
 
-                buffer.destroy();
+                this._bufferManager.releaseBuffer(buffer);
             } else {
                 this._device.defaultQueue.writeTexture(textureCopyView, imageBitmap, {
                     offset: 0,
@@ -368,4 +520,40 @@ export class WebGPUTextureHelper {
             }
         }
     }
+
+    public readPixels(texture: GPUTexture, x: number, y: number, width: number, height: number, format: GPUTextureFormat, faceIndex: number = 0, mipLevel: number = 0, buffer: Nullable<ArrayBufferView> = null): Promise<ArrayBufferView> {
+        const blockInformation = this._getBlockInformationFromFormat(format);
+
+        const bytesPerRow = Math.ceil(width / blockInformation.width) * blockInformation.length;
+        const size = bytesPerRow * height;
+
+        const gpuBuffer = this._bufferManager.createRawBuffer(size, GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST);
+
+        const commandEncoder = this._device.createCommandEncoder({});
+
+        commandEncoder.copyTextureToBuffer({
+            texture,
+            mipLevel,
+            origin: {
+                x,
+                y,
+                z: Math.max(faceIndex, 0)
+            }
+        }, {
+            buffer: gpuBuffer,
+            offset: 0,
+            bytesPerRow
+        }, {
+            width,
+            height,
+            depth: 1
+        });
+
+        this._device.defaultQueue.submit([commandEncoder!.finish()]);
+
+        const type = this._getTextureTypeFromFormat(format);
+        const floatFormat = type === Constants.TEXTURETYPE_FLOAT ? 2 : type === Constants.TEXTURETYPE_HALF_FLOAT ? 1 : 0;
+
+        return this._bufferManager.readDataFromBuffer(gpuBuffer, size, width, height, floatFormat, 0, buffer);
+    }
 }

+ 3 - 3
src/Engines/thinEngine.ts

@@ -4414,14 +4414,14 @@ export class ThinEngine {
      * @param width defines the width of the rectangle where pixels must be read
      * @param height defines the height of the rectangle where pixels must be read
      * @param hasAlpha defines whether the output should have alpha or not (defaults to true)
-     * @returns a Uint8Array containing RGBA colors
+     * @returns a ArrayBufferView (Uint8Array) containing RGBA colors
      */
-    public readPixels(x: number, y: number, width: number, height: number, hasAlpha = true): Promise<Uint8Array> | Uint8Array {
+    public readPixels(x: number, y: number, width: number, height: number, hasAlpha = true): Promise<ArrayBufferView> {
         const numChannels = hasAlpha ? 4 : 3;
         const format = hasAlpha ? this._gl.RGBA : this._gl.RGB;
         const data = new Uint8Array(height * width * numChannels);
         this._gl.readPixels(x, y, width, height, format, this._gl.UNSIGNED_BYTE, data);
-        return data;
+        return Promise.resolve(data);
     }
 
     // Statics

+ 5 - 54
src/Engines/webgpuEngine.ts

@@ -2098,64 +2098,15 @@ export class WebGPUEngine extends Engine {
         }
     }
 
-    public readPixels(x: number, y: number, width: number, height: number, hasAlpha = true): Promise<Uint8Array> | Uint8Array {
-        const numChannels = 4; // no RGB format in WebGPU
-        const size = height * width * numChannels;
-
-        const buffer = this._bufferManager.createRawBuffer(size, GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST);
-
-        const commandEncoder = this._device.createCommandEncoder({});
-
-        commandEncoder.copyTextureToBuffer({
-            texture: this._swapChainTexture,
-            mipLevel: 0,
-            origin: {
-                x,
-                y,
-                z: 0
-            }
-        }, {
-            buffer: buffer,
-            offset: 0,
-            bytesPerRow: width * numChannels,
-            rowsPerImage: height
-        }, {
-            width,
-            height,
-            depth: 1
-        });
-
-        this._device.defaultQueue.submit([commandEncoder!.finish()]);
-
-        return this._bufferManager.readDataFromBuffer(buffer, size);
+    public readPixels(x: number, y: number, width: number, height: number, hasAlpha = true): Promise<ArrayBufferView> {
+        return this._textureHelper.readPixels(this._swapChainTexture, x, y, width, height, this._options.swapChainFormat!);
     }
 
     /** @hidden */
-    public _readTexturePixels(texture: InternalTexture, width: number, height: number, faceIndex = -1, level = 0, buffer: Nullable<ArrayBufferView> = null): ArrayBufferView {
-        // TODO WEBGPU Implement the method, the problem being it is "synchronous" in the webgl case...
-        if (dbgShowWarningsNotImplemented) {
-            console.warn("_readTexturePixels not implemented yet in WebGPU");
-        }
-
-        return null as any;
-        /*let readType = (texture.type !== undefined) ? this._getWebGLTextureType(texture.type) : gl.UNSIGNED_BYTE;
-
-        switch (readType) {
-            case gl.UNSIGNED_BYTE:
-                if (!buffer) {
-                    buffer = new Uint8Array(4 * width * height);
-                }
-                readType = gl.UNSIGNED_BYTE;
-                break;
-            default:
-                if (!buffer) {
-                    buffer = new Float32Array(4 * width * height);
-                }
-                readType = gl.FLOAT;
-                break;
-        }
+    public _readTexturePixels(texture: InternalTexture, width: number, height: number, faceIndex = -1, level = 0, buffer: Nullable<ArrayBufferView> = null): Promise<ArrayBufferView> {
+        let gpuTextureWrapper = texture._hardwareTexture as WebGPUHardwareTexture;
 
-        gl.readPixels(0, 0, width, height, gl.RGBA, readType, <DataView>buffer);*/
+        return this._textureHelper.readPixels(gpuTextureWrapper.underlyingResource!, 0, 0, width, height, gpuTextureWrapper.format, faceIndex, level, buffer);
     }
 
     //------------------------------------------------------------------------------

+ 12 - 5
src/Materials/Textures/Procedurals/proceduralTexture.ts

@@ -102,7 +102,7 @@ export class ProceduralTexture extends Texture {
     private _cachedDefines: Nullable<string> = null;
 
     private _contentUpdateId = -1;
-    private _contentData: Nullable<ArrayBufferView>;
+    private _contentData: Nullable<Promise<ArrayBufferView>>;
 
     /**
      * Instantiates a new procedural texture.
@@ -171,15 +171,22 @@ export class ProceduralTexture extends Texture {
 
     /**
      * Gets texture content (Use this function wisely as reading from a texture can be slow)
-     * @returns an ArrayBufferView (Uint8Array or Float32Array)
+     * @returns an ArrayBufferView promise (Uint8Array or Float32Array)
      */
-    public getContent(): Nullable<ArrayBufferView> {
+    public getContent(): Nullable<Promise<ArrayBufferView>> {
         if (this._contentData && this._frameId === this._contentUpdateId) {
             return this._contentData;
         }
 
-        this._contentData = this.readPixels(0, 0, this._contentData);
-        this._contentUpdateId = this._frameId;
+        if (this._contentData) {
+            this._contentData.then((buffer) => {
+                this._contentData = this.readPixels(0, 0, buffer);
+                this._contentUpdateId = this._frameId;
+            });
+        } else {
+            this._contentData = this.readPixels(0, 0);
+            this._contentUpdateId = this._frameId;
+        }
 
         return this._contentData;
     }

+ 14 - 4
src/Materials/Textures/baseTexture.polynomial.ts

@@ -17,14 +17,24 @@ declare module "./baseTexture" {
 Object.defineProperty(BaseTexture.prototype, "sphericalPolynomial", {
     get: function(this: BaseTexture) {
         if (this._texture) {
-            if (this._texture._sphericalPolynomial) {
+            if (this._texture._sphericalPolynomial || this._texture._sphericalPolynomialComputed) {
                 return this._texture._sphericalPolynomial;
             }
 
             if (this._texture.isReady) {
-                this._texture._sphericalPolynomial =
-                    CubeMapToSphericalPolynomialTools.ConvertCubeMapTextureToSphericalPolynomial(this);
-                return this._texture._sphericalPolynomial;
+                if (!this._texture._sphericalPolynomialPromise) {
+                    this._texture._sphericalPolynomialPromise = CubeMapToSphericalPolynomialTools.ConvertCubeMapTextureToSphericalPolynomial(this);
+                    if (this._texture._sphericalPolynomialPromise === null) {
+                        this._texture._sphericalPolynomialComputed = true;
+                    } else {
+                        this._texture._sphericalPolynomialPromise.then((sphericalPolynomial) => {
+                            this._texture!._sphericalPolynomial = sphericalPolynomial;
+                            this._texture!._sphericalPolynomialComputed = true;
+                        });
+                    }
+                }
+
+                return null;
             }
         }
 

+ 2 - 2
src/Materials/Textures/baseTexture.ts

@@ -698,9 +698,9 @@ export class BaseTexture implements IAnimatable {
      * @param faceIndex defines the face of the texture to read (in case of cube texture)
      * @param level defines the LOD level of the texture to read (in case of Mip Maps)
      * @param buffer defines a user defined buffer to fill with data (can be null)
-     * @returns The Array buffer containing the pixels data.
+     * @returns The Array buffer promise containing the pixels data.
      */
-    public readPixels(faceIndex = 0, level = 0, buffer: Nullable<ArrayBufferView> = null): Nullable<ArrayBufferView> {
+    public readPixels(faceIndex = 0, level = 0, buffer: Nullable<ArrayBufferView> = null): Nullable<Promise<ArrayBufferView>> {
         if (!this._texture) {
             return null;
         }

+ 2 - 0
src/Materials/Textures/internalTexture.ts

@@ -219,6 +219,8 @@ export class InternalTexture {
     public _comparisonFunction: number = 0;
     /** @hidden */
     public _sphericalPolynomial: Nullable<SphericalPolynomial> = null;
+    public _sphericalPolynomialPromise: Nullable<Promise<SphericalPolynomial>> = null;
+    public _sphericalPolynomialComputed = false;
     /** @hidden */
     public _lodGenerationScale: number = 0;
     /** @hidden */

+ 2 - 0
src/Materials/Textures/renderTargetCreationOptions.ts

@@ -21,4 +21,6 @@ export class RenderTargetCreationOptions {
     samplingMode?: number;
     /** Defines format (RGBA by default) */
     format?: number;
+    /** Defines sample count (1 by default) */
+    samples?: number;
 }

+ 29 - 25
src/Misc/HighDynamicRange/cubemapToSphericalPolynomial.ts

@@ -44,29 +44,29 @@ export class CubeMapToSphericalPolynomialTools {
      * @param texture The texture to extract the information from.
      * @return The Spherical Polynomial data.
      */
-    public static ConvertCubeMapTextureToSphericalPolynomial(texture: BaseTexture) {
+    public static ConvertCubeMapTextureToSphericalPolynomial(texture: BaseTexture): Nullable<Promise<SphericalPolynomial>> {
         if (!texture.isCube) {
             // Only supports cube Textures currently.
             return null;
         }
 
         var size = texture.getSize().width;
-        var right = texture.readPixels(0);
-        var left = texture.readPixels(1);
+        var rightPromise = texture.readPixels(0);
+        var leftPromise = texture.readPixels(1);
 
-        var up: Nullable<ArrayBufferView>;
-        var down: Nullable<ArrayBufferView>;
+        var upPromise: Nullable<Promise<ArrayBufferView>>;
+        var downPromise: Nullable<Promise<ArrayBufferView>>;
         if (texture.isRenderTarget) {
-            up = texture.readPixels(3);
-            down = texture.readPixels(2);
+            upPromise = texture.readPixels(3);
+            downPromise = texture.readPixels(2);
         }
         else {
-            up = texture.readPixels(2);
-            down = texture.readPixels(3);
+            upPromise = texture.readPixels(2);
+            downPromise = texture.readPixels(3);
         }
 
-        var front = texture.readPixels(4);
-        var back = texture.readPixels(5);
+        var frontPromise = texture.readPixels(4);
+        var backPromise = texture.readPixels(5);
 
         var gammaSpace = texture.gammaSpace;
         // Always read as RGBA.
@@ -76,20 +76,24 @@ export class CubeMapToSphericalPolynomialTools {
             type = Constants.TEXTURETYPE_FLOAT;
         }
 
-        var cubeInfo: CubeMapInfo = {
-            size,
-            right,
-            left,
-            up,
-            down,
-            front,
-            back,
-            format,
-            type,
-            gammaSpace,
-        };
-
-        return this.ConvertCubeMapToSphericalPolynomial(cubeInfo);
+        return new Promise((resolve, reject) => {
+            Promise.all([leftPromise, rightPromise, upPromise, downPromise, frontPromise, backPromise]).then(([left, right, up, down, front, back]) => {
+                var cubeInfo: CubeMapInfo = {
+                    size,
+                    right,
+                    left,
+                    up,
+                    down,
+                    front,
+                    back,
+                    format,
+                    type,
+                    gammaSpace,
+                };
+
+                resolve(this.ConvertCubeMapToSphericalPolynomial(cubeInfo));
+            });
+        });
     }
 
     /**

+ 3 - 4
src/Misc/environmentTextureTools.ts

@@ -146,7 +146,7 @@ export class EnvironmentTextureTools {
      * @param texture defines the cube texture to convert in env file
      * @return a promise containing the environment data if succesfull.
      */
-    public static CreateEnvTextureAsync(texture: BaseTexture): Promise<ArrayBuffer> {
+    public static async CreateEnvTextureAsync(texture: BaseTexture): Promise<ArrayBuffer> {
         let internalTexture = texture.getInternalTexture();
         if (!internalTexture) {
             return Promise.reject("The cube texture is invalid.");
@@ -180,14 +180,13 @@ export class EnvironmentTextureTools {
         let promises: Promise<void>[] = [];
 
         // Read and collect all mipmaps data from the cube.
-        let mipmapsCount = Scalar.Log2(internalTexture.width);
-        mipmapsCount = Math.round(mipmapsCount);
+        let mipmapsCount = Math.floor(Scalar.ILog2(internalTexture.width));
         for (let i = 0; i <= mipmapsCount; i++) {
             let faceWidth = Math.pow(2, mipmapsCount - i);
 
             // All faces of the cube.
             for (let face = 0; face < 6; face++) {
-                let data = texture.readPixels(face, i);
+                let data = await texture.readPixels(face, i);
 
                 // Creates a temp texture with the face data.
                 let tempTexture = engine.createRawTexture(data, faceWidth, faceWidth, Constants.TEXTUREFORMAT_RGBA, false, false, Constants.TEXTURE_NEAREST_SAMPLINGMODE, null, textureType);

+ 13 - 20
src/Misc/screenshotTools.ts

@@ -34,6 +34,8 @@ export class ScreenshotTools {
     public static CreateScreenshot(engine: Engine, camera: Camera, size: IScreenshotSize | number, successCallback?: (data: string) => void, mimeType: string = "image/png"): void {
         const { height, width } = ScreenshotTools._getScreenshotSize(engine, camera, size);
 
+        // TODO WEBGPU use engine.readPixels to get the back buffer
+
         if (!(height && width)) {
             Logger.Error("Invalid 'size' parameter !");
             return;
@@ -132,36 +134,27 @@ export class ScreenshotTools {
             scene.activeCamera = camera;
         }
 
-        var renderCanvas = engine.getRenderingCanvas();
-        if (!renderCanvas) {
-            Logger.Error("No rendering canvas found !");
-            return;
-        }
-
-        var originalSize = { width: renderCanvas.width, height: renderCanvas.height };
-        engine.setSize(width, height);
-        scene.render();
-
         // At this point size can be a number, or an object (according to engine.prototype.createRenderTargetTexture method)
-        var texture = new RenderTargetTexture("screenShot", targetTextureSize, scene, false, false, Constants.TEXTURETYPE_UNSIGNED_INT, false, Texture.NEAREST_SAMPLINGMODE, undefined, enableStencilBuffer);
+        var texture = new RenderTargetTexture("screenShot", targetTextureSize, scene, false, false, Constants.TEXTURETYPE_UNSIGNED_INT, false, Texture.NEAREST_SAMPLINGMODE, undefined, enableStencilBuffer, undefined, undefined, undefined, samples);
         texture.renderList = null;
         texture.samples = samples;
         texture.renderSprites = renderSprites;
-        texture.onAfterRenderObservable.add(() => {
-            Tools.DumpFramebuffer(width, height, engine, successCallback, mimeType, fileName);
+        engine.onEndFrameObservable.addOnce(() => {
+            texture.readPixels()!.then((data) => {
+                Tools.DumpData(width, height, data, successCallback, mimeType, fileName, true);
+                texture.dispose();
+
+                if (previousCamera) {
+                    scene.activeCamera = previousCamera;
+                }
+                camera.getProjectionMatrix(true); // Force cache refresh;
+            });
         });
 
         const renderToTexture = () => {
             scene.incrementRenderId();
             scene.resetCachedMaterial();
             texture.render(true);
-            texture.dispose();
-
-            if (previousCamera) {
-                scene.activeCamera = previousCamera;
-            }
-            engine.setSize(originalSize.width, originalSize.height);
-            camera.getProjectionMatrix(true); // Force cache refresh;
         };
 
         if (antialiasing) {

+ 56 - 26
src/Misc/tools.ts

@@ -580,13 +580,9 @@ export class Tools {
         var halfHeight = height / 2;
 
         engine.onEndFrameObservable.addOnce(async () => {
-            let data = engine.readPixels(0, 0, width, height);
+            let bufferView = await engine.readPixels(0, 0, width, height);
 
-            if ((data as Uint8Array).byteLength === undefined) {
-                data = await data;
-            }
-
-            data = data as Uint8Array;
+            const data = new Uint8Array(bufferView.buffer);
 
             // To flip image on Y axis.
             for (var i = 0; i < halfHeight; i++) {
@@ -611,27 +607,60 @@ export class Tools {
                 }
             }
 
-            // Create a 2D canvas to store the result
-            if (!Tools._ScreenshotCanvas) {
-                Tools._ScreenshotCanvas = document.createElement('canvas');
-            }
-            Tools._ScreenshotCanvas.width = width;
-            Tools._ScreenshotCanvas.height = height;
-            var context = Tools._ScreenshotCanvas.getContext('2d');
-
-            if (context) {
-                // Copy the pixels to a 2D canvas
-                var imageData = context.createImageData(width, height);
-                var castData = <any>(imageData.data);
-                castData.set(data);
-                context.putImageData(imageData, 0, 0);
-
-                Tools.EncodeScreenshotCanvasData(successCallback, mimeType, fileName);
-            }
+            Tools.DumpData(width, height, data, successCallback, mimeType, fileName);
         });
     }
 
     /**
+     * Dumps an array buffer
+     * @param width defines the rendering width
+     * @param height defines the rendering height
+     * @param data the data array
+     * @param successCallback defines the callback triggered once the data are available
+     * @param mimeType defines the mime type of the result
+     * @param fileName defines the filename to download. If present, the result will automatically be downloaded
+     * @param invertY true to invert the picture in the Y dimension
+     */
+    public static DumpData(width: number, height: number, data: ArrayBufferView, successCallback?: (data: string) => void, mimeType: string = "image/png", fileName?: string, invertY = false) {
+        // Create a 2D canvas to store the result
+        if (!Tools._ScreenshotCanvas) {
+            Tools._ScreenshotCanvas = document.createElement('canvas');
+        }
+        Tools._ScreenshotCanvas.width = width;
+        Tools._ScreenshotCanvas.height = height;
+        var context = Tools._ScreenshotCanvas.getContext('2d');
+
+        if (context) {
+            // Copy the pixels to a 2D canvas
+            var imageData = context.createImageData(width, height);
+            var castData = <any>(imageData.data);
+            castData.set(data);
+            context.putImageData(imageData, 0, 0);
+
+            let canvas = Tools._ScreenshotCanvas;
+
+            if (invertY) {
+                var canvas2 = document.createElement('canvas');
+                canvas2.width = width;
+                canvas2.height = height;
+
+                var ctx2 = canvas2.getContext('2d');
+                if (!ctx2) {
+                    return;
+                }
+
+                ctx2.translate(0, height);
+                ctx2.scale(1, -1);
+                ctx2.drawImage(Tools._ScreenshotCanvas, 0, 0);
+
+                canvas = canvas2;
+            }
+
+            Tools.EncodeScreenshotCanvasData(successCallback, mimeType, fileName, canvas);
+        }
+    }
+
+    /**
      * Converts the canvas data to blob.
      * This acts as a polyfill for browsers not supporting the to blob function.
      * @param canvas Defines the canvas to extract the data from
@@ -665,14 +694,15 @@ export class Tools {
      * @param successCallback defines the callback triggered once the data are available
      * @param mimeType defines the mime type of the result
      * @param fileName defines he filename to download. If present, the result will automatically be downloaded
+     * @param canvas canvas to get the data from. If not provided, use the default screenshot canvas
      */
-    static EncodeScreenshotCanvasData(successCallback?: (data: string) => void, mimeType: string = "image/png", fileName?: string): void {
+    static EncodeScreenshotCanvasData(successCallback?: (data: string) => void, mimeType: string = "image/png", fileName?: string, canvas?: HTMLCanvasElement): void {
         if (successCallback) {
-            var base64Image = Tools._ScreenshotCanvas.toDataURL(mimeType);
+            var base64Image = (canvas ?? Tools._ScreenshotCanvas).toDataURL(mimeType);
             successCallback(base64Image);
         }
         else {
-            this.ToBlob(Tools._ScreenshotCanvas, function(blob) {
+            this.ToBlob(canvas ?? Tools._ScreenshotCanvas, function(blob) {
                 //Creating a link if the browser have the download attribute on the a tag, to automatically start download generated image.
                 if (("download" in document.createElement("a"))) {
                     if (!fileName) {

+ 4 - 8
src/PostProcesses/RenderPipeline/Pipelines/standardRenderingPipeline.ts

@@ -929,14 +929,10 @@ export class StandardRenderingPipeline extends PostProcessRenderPipeline impleme
                 pp.onAfterRender = () => {
                     var pixel = scene.getEngine().readPixels(0, 0, 1, 1);
                     var bit_shift = new Vector4(1.0 / (255.0 * 255.0 * 255.0), 1.0 / (255.0 * 255.0), 1.0 / 255.0, 1.0);
-                    if ((pixel as Uint8Array).byteLength !== undefined) {
-                        pixel = pixel as Uint8Array;
-                        this._hdrCurrentLuminance = (pixel[0] * bit_shift.x + pixel[1] * bit_shift.y + pixel[2] * bit_shift.z + pixel[3] * bit_shift.w) / 100.0;
-                    } else {
-                        (pixel as Promise<Uint8Array>).then((pixel) => {
-                            this._hdrCurrentLuminance = (pixel[0] * bit_shift.x + pixel[1] * bit_shift.y + pixel[2] * bit_shift.z + pixel[3] * bit_shift.w) / 100.0;
-                        });
-                    }
+                    pixel.then((pixel) => {
+                        const data = new Uint8Array(pixel.buffer);
+                        this._hdrCurrentLuminance = (data[0] * bit_shift.x + data[1] * bit_shift.y + data[2] * bit_shift.z + data[3] * bit_shift.w) / 100.0;
+                    });
                 };
             }