UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

439 lines (436 loc) 20.8 kB
import { TRACEID_RENDER_QUEUE } from '../../../core/constants.js'; import { Debug, DebugHelper } from '../../../core/debug.js'; import { math } from '../../../core/math/math.js'; import { isCompressedPixelFormat, PIXELFORMAT_DEPTHSTENCIL, SAMPLETYPE_DEPTH, SAMPLETYPE_INT, SAMPLETYPE_UINT, SAMPLETYPE_UNFILTERABLE_FLOAT, PIXELFORMAT_RGBA32F, PIXELFORMAT_RGBA16F, isIntegerPixelFormat, pixelFormatInfo, BUFFERUSAGE_READ, BUFFERUSAGE_COPY_DST, ADDRESS_REPEAT, ADDRESS_CLAMP_TO_EDGE, ADDRESS_MIRRORED_REPEAT, FILTER_NEAREST, FILTER_LINEAR, FILTER_NEAREST_MIPMAP_NEAREST, FILTER_NEAREST_MIPMAP_LINEAR, FILTER_LINEAR_MIPMAP_NEAREST, FILTER_LINEAR_MIPMAP_LINEAR } from '../constants.js'; import { TextureUtils } from '../texture-utils.js'; import { WebgpuDebug } from './webgpu-debug.js'; import { gpuTextureFormats } from './constants.js'; /** * @import { Texture } from '../texture.js' * @import { WebgpuGraphicsDevice } from './webgpu-graphics-device.js' */ // map of ADDRESS_*** to GPUAddressMode var gpuAddressModes = []; gpuAddressModes[ADDRESS_REPEAT] = 'repeat'; gpuAddressModes[ADDRESS_CLAMP_TO_EDGE] = 'clamp-to-edge'; gpuAddressModes[ADDRESS_MIRRORED_REPEAT] = 'mirror-repeat'; // map of FILTER_*** to GPUFilterMode for level and mip sampling var gpuFilterModes = []; gpuFilterModes[FILTER_NEAREST] = { level: 'nearest', mip: 'nearest' }; gpuFilterModes[FILTER_LINEAR] = { level: 'linear', mip: 'nearest' }; gpuFilterModes[FILTER_NEAREST_MIPMAP_NEAREST] = { level: 'nearest', mip: 'nearest' }; gpuFilterModes[FILTER_NEAREST_MIPMAP_LINEAR] = { level: 'nearest', mip: 'linear' }; gpuFilterModes[FILTER_LINEAR_MIPMAP_NEAREST] = { level: 'linear', mip: 'nearest' }; gpuFilterModes[FILTER_LINEAR_MIPMAP_LINEAR] = { level: 'linear', mip: 'linear' }; var dummyUse = (thingOne)=>{ // so lint thinks we're doing something with thingOne }; /** * A WebGPU implementation of the Texture. * * @ignore */ class WebgpuTexture { create(device) { var texture = this.texture; var wgpu = device.wgpu; var numLevels = texture.numLevels; Debug.assert(texture.width > 0 && texture.height > 0, "Invalid texture dimensions " + texture.width + "x" + texture.height + " for texture " + texture.name, texture); this.desc = { size: { width: texture.width, height: texture.height, depthOrArrayLayers: texture.cubemap ? 6 : texture.array ? texture.arrayLength : 1 }, format: this.format, mipLevelCount: numLevels, sampleCount: 1, dimension: texture.volume ? '3d' : '2d', // TODO: use only required usage flags // COPY_SRC - probably only needed on render target textures, to support copyRenderTarget (grab pass needs it) // RENDER_ATTACHMENT - needed for mipmap generation usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | (isCompressedPixelFormat(texture.format) ? 0 : GPUTextureUsage.RENDER_ATTACHMENT) | (texture.storage ? GPUTextureUsage.STORAGE_BINDING : 0) }; WebgpuDebug.validate(device); this.gpuTexture = wgpu.createTexture(this.desc); DebugHelper.setLabel(this.gpuTexture, "" + texture.name + (texture.cubemap ? '[cubemap]' : '') + (texture.volume ? '[3d]' : '')); WebgpuDebug.end(device, 'Texture creation', { desc: this.desc, texture }); // default texture view descriptor var viewDescr; // some format require custom default texture view if (this.texture.format === PIXELFORMAT_DEPTHSTENCIL) { // we expose the depth part of the format viewDescr = { format: 'depth24plus', aspect: 'depth-only' }; } this.view = this.createView(viewDescr); } destroy(device) {} propertyChanged(flag) { // samplers need to be recreated this.samplers.length = 0; } /** * @param {any} device - The Graphics Device. * @returns {any} - Returns the view. */ getView(device) { this.uploadImmediate(device, this.texture); Debug.assert(this.view); return this.view; } createView(viewDescr) { var options = viewDescr != null ? viewDescr : {}; var textureDescr = this.desc; var texture = this.texture; // '1d', '2d', '2d-array', 'cube', 'cube-array', '3d' var defaultViewDimension = ()=>{ if (texture.cubemap) return 'cube'; if (texture.volume) return '3d'; if (texture.array) return '2d-array'; return '2d'; }; var _options_format, _options_dimension, _options_aspect, _options_baseMipLevel, _options_mipLevelCount, _options_baseArrayLayer, _options_arrayLayerCount; /** @type {GPUTextureViewDescriptor} */ var desc = { format: (_options_format = options.format) != null ? _options_format : textureDescr.format, dimension: (_options_dimension = options.dimension) != null ? _options_dimension : defaultViewDimension(), aspect: (_options_aspect = options.aspect) != null ? _options_aspect : 'all', baseMipLevel: (_options_baseMipLevel = options.baseMipLevel) != null ? _options_baseMipLevel : 0, mipLevelCount: (_options_mipLevelCount = options.mipLevelCount) != null ? _options_mipLevelCount : textureDescr.mipLevelCount, baseArrayLayer: (_options_baseArrayLayer = options.baseArrayLayer) != null ? _options_baseArrayLayer : 0, arrayLayerCount: (_options_arrayLayerCount = options.arrayLayerCount) != null ? _options_arrayLayerCount : textureDescr.depthOrArrayLayers }; var view = this.gpuTexture.createView(desc); DebugHelper.setLabel(view, (viewDescr ? "CustomView" + JSON.stringify(viewDescr) : 'DefaultView') + ":" + this.texture.name); return view; } // TODO: share a global map of samplers. Possibly even use shared samplers for bind group, // or maybe even have some attached in view bind group and use globally /** * @param {any} device - The Graphics Device. * @param {number} [sampleType] - A sample type for the sampler, SAMPLETYPE_*** constant. If not * specified, the sampler type is based on the texture format / texture sampling type. * @returns {any} - Returns the sampler. */ getSampler(device, sampleType) { var sampler = this.samplers[sampleType]; if (!sampler) { var texture = this.texture; var label; /** @type GPUSamplerDescriptor */ var desc = { addressModeU: gpuAddressModes[texture.addressU], addressModeV: gpuAddressModes[texture.addressV], addressModeW: gpuAddressModes[texture.addressW] }; // default for compare sampling of texture if (!sampleType && texture.compareOnRead) { sampleType = SAMPLETYPE_DEPTH; } if (sampleType === SAMPLETYPE_DEPTH || sampleType === SAMPLETYPE_INT || sampleType === SAMPLETYPE_UINT) { // depth compare sampling desc.compare = 'less'; desc.magFilter = 'linear'; desc.minFilter = 'linear'; label = 'Compare'; } else if (sampleType === SAMPLETYPE_UNFILTERABLE_FLOAT) { desc.magFilter = 'nearest'; desc.minFilter = 'nearest'; desc.mipmapFilter = 'nearest'; label = 'Unfilterable'; } else { // if the device cannot filter float textures, force nearest filtering var forceNearest = !device.textureFloatFilterable && (texture.format === PIXELFORMAT_RGBA32F || texture.format === PIXELFORMAT_RGBA16F); if (forceNearest || this.texture.format === PIXELFORMAT_DEPTHSTENCIL || isIntegerPixelFormat(this.texture.format)) { desc.magFilter = 'nearest'; desc.minFilter = 'nearest'; desc.mipmapFilter = 'nearest'; label = 'Nearest'; } else { desc.magFilter = gpuFilterModes[texture.magFilter].level; desc.minFilter = gpuFilterModes[texture.minFilter].level; desc.mipmapFilter = gpuFilterModes[texture.minFilter].mip; Debug.call(()=>{ label = "Texture:" + texture.magFilter + "-" + texture.minFilter + "-" + desc.mipmapFilter; }); } } // ensure anisotropic filtering is only set when filtering is correctly // set up var allLinear = desc.minFilter === 'linear' && desc.magFilter === 'linear' && desc.mipmapFilter === 'linear'; desc.maxAnisotropy = allLinear ? math.clamp(Math.round(texture._anisotropy), 1, device.maxTextureAnisotropy) : 1; sampler = device.wgpu.createSampler(desc); DebugHelper.setLabel(sampler, label); this.samplers[sampleType] = sampler; } return sampler; } loseContext() {} /** * @param {WebgpuGraphicsDevice} device - The graphics device. * @param {Texture} texture - The texture. */ uploadImmediate(device, texture) { if (texture._needsUpload || texture._needsMipmapsUpload) { this.uploadData(device); texture._needsUpload = false; texture._needsMipmapsUpload = false; } } /** * @param {WebgpuGraphicsDevice} device - The graphics * device. */ uploadData(device) { var texture = this.texture; if (texture._levels) { // upload texture data if any var anyUploads = false; var anyLevelMissing = false; var requiredMipLevels = texture.numLevels; for(var mipLevel = 0; mipLevel < requiredMipLevels; mipLevel++){ var mipObject = texture._levels[mipLevel]; if (mipObject) { if (texture.cubemap) { for(var face = 0; face < 6; face++){ var faceSource = mipObject[face]; if (faceSource) { if (this.isExternalImage(faceSource)) { this.uploadExternalImage(device, faceSource, mipLevel, face); anyUploads = true; } else if (ArrayBuffer.isView(faceSource)) { this.uploadTypedArrayData(device, faceSource, mipLevel, face); anyUploads = true; } else { Debug.error('Unsupported texture source data for cubemap face', faceSource); } } else { anyLevelMissing = true; } } } else if (texture._volume) { Debug.warn('Volume texture data upload is not supported yet', this.texture); } else if (texture.array) { if (texture.arrayLength === mipObject.length) { for(var index = 0; index < texture._arrayLength; index++){ var arraySource = mipObject[index]; if (this.isExternalImage(arraySource)) { this.uploadExternalImage(device, arraySource, mipLevel, index); anyUploads = true; } else if (ArrayBuffer.isView(arraySource)) { this.uploadTypedArrayData(device, arraySource, mipLevel, index); anyUploads = true; } else { Debug.error('Unsupported texture source data for texture array entry', arraySource); } } } else { anyLevelMissing = true; } } else { if (this.isExternalImage(mipObject)) { this.uploadExternalImage(device, mipObject, mipLevel, 0); anyUploads = true; } else if (ArrayBuffer.isView(mipObject)) { this.uploadTypedArrayData(device, mipObject, mipLevel, 0); anyUploads = true; } else { Debug.error('Unsupported texture source data', mipObject); } } } else { anyLevelMissing = true; } } if (anyUploads && anyLevelMissing && texture.mipmaps && !isCompressedPixelFormat(texture.format) && !isIntegerPixelFormat(texture.format)) { device.mipmapRenderer.generate(this); } // update vram stats if (texture._gpuSize) { texture.adjustVramSizeTracking(device._vram, -texture._gpuSize); } texture._gpuSize = texture.gpuSize; texture.adjustVramSizeTracking(device._vram, texture._gpuSize); } } // image types supported by copyExternalImageToTexture isExternalImage(image) { return image instanceof ImageBitmap || image instanceof HTMLVideoElement || image instanceof HTMLCanvasElement || image instanceof OffscreenCanvas; } uploadExternalImage(device, image, mipLevel, index) { Debug.assert(mipLevel < this.desc.mipLevelCount, "Accessing mip level " + mipLevel + " of texture with " + this.desc.mipLevelCount + " mip levels", this); var src = { source: image, origin: [ 0, 0 ], flipY: false }; var dst = { texture: this.gpuTexture, mipLevel: mipLevel, origin: [ 0, 0, index ], aspect: 'all' // can be: "all", "stencil-only", "depth-only" }; var copySize = { width: this.desc.size.width, height: this.desc.size.height, depthOrArrayLayers: 1 // single layer }; // submit existing scheduled commands to the queue before copying to preserve the order device.submit(); // create 2d context so webgpu can upload the texture dummyUse(image instanceof HTMLCanvasElement && image.getContext('2d')); Debug.trace(TRACEID_RENDER_QUEUE, "IMAGE-TO-TEX: mip:" + mipLevel + " index:" + index + " " + this.texture.name); device.wgpu.queue.copyExternalImageToTexture(src, dst, copySize); } uploadTypedArrayData(device, data, mipLevel, index) { var texture = this.texture; var wgpu = device.wgpu; /** @type {GPUImageCopyTexture} */ var dest = { texture: this.gpuTexture, origin: [ 0, 0, index ], mipLevel: mipLevel }; // texture dimensions at the specified mip level var width = TextureUtils.calcLevelDimension(texture.width, mipLevel); var height = TextureUtils.calcLevelDimension(texture.height, mipLevel); // data sizes var byteSize = TextureUtils.calcLevelGpuSize(width, height, 1, texture.format); Debug.assert(byteSize === data.byteLength, "Error uploading data to texture, the data byte size of " + data.byteLength + " does not match required " + byteSize, texture); var formatInfo = pixelFormatInfo.get(texture.format); Debug.assert(formatInfo); /** @type {GPUImageDataLayout} */ var dataLayout; var size; if (formatInfo.size) { // uncompressed format dataLayout = { offset: 0, bytesPerRow: formatInfo.size * width, rowsPerImage: height }; size = { width: width, height: height }; } else if (formatInfo.blockSize) { // compressed format var blockDim = (size)=>{ return Math.floor((size + 3) / 4); }; dataLayout = { offset: 0, bytesPerRow: formatInfo.blockSize * blockDim(width), rowsPerImage: blockDim(height) }; size = { width: Math.max(4, width), height: Math.max(4, height) }; } else { Debug.assert(false, "WebGPU does not yet support texture format " + formatInfo.name + " for texture " + texture.name, texture); } // submit existing scheduled commands to the queue before copying to preserve the order device.submit(); Debug.trace(TRACEID_RENDER_QUEUE, "WRITE-TEX: mip:" + mipLevel + " index:" + index + " " + this.texture.name); wgpu.queue.writeTexture(dest, data, dataLayout, size); } read(x, y, width, height, options) { var _options_mipLevel; var mipLevel = (_options_mipLevel = options.mipLevel) != null ? _options_mipLevel : 0; var _options_face; var face = (_options_face = options.face) != null ? _options_face : 0; var _options_data; var data = (_options_data = options.data) != null ? _options_data : null; var _options_immediate; var immediate = (_options_immediate = options.immediate) != null ? _options_immediate : false; var texture = this.texture; var formatInfo = pixelFormatInfo.get(texture.format); Debug.assert(formatInfo); Debug.assert(formatInfo.size); var bytesPerRow = width * formatInfo.size; // bytesPerRow must be a multiple of 256 var paddedBytesPerRow = math.roundUp(bytesPerRow, 256); var size = paddedBytesPerRow * height; // create a temporary staging buffer /** @type {WebgpuGraphicsDevice} */ var device = texture.device; var stagingBuffer = device.createBufferImpl(BUFFERUSAGE_READ | BUFFERUSAGE_COPY_DST); stagingBuffer.allocate(device, size); var src = { texture: this.gpuTexture, mipLevel: mipLevel, origin: [ x, y, face ] }; var dst = { buffer: stagingBuffer.buffer, offset: 0, bytesPerRow: paddedBytesPerRow }; var copySize = { width, height, depthOrArrayLayers: 1 // single layer }; // copy the GPU texture to the staging buffer var commandEncoder = device.getCommandEncoder(); commandEncoder.copyTextureToBuffer(src, dst, copySize); // async read data from the staging buffer to a temporary array return device.readBuffer(stagingBuffer, size, null, immediate).then((temp)=>{ // remove the 256 alignment padding from the end of each row data != null ? data : data = new Uint8Array(height * bytesPerRow); for(var i = 0; i < height; i++){ var srcOffset = i * paddedBytesPerRow; var dstOffset = i * bytesPerRow; var sub = temp.subarray(srcOffset, srcOffset + bytesPerRow); data.set(sub, dstOffset); } return data; }); } constructor(texture){ var _pixelFormatInfo_get; /** * An array of samplers, addressed by SAMPLETYPE_*** constant, allowing texture to be sampled * using different samplers. Most textures are sampled as interpolated floats, but some can * additionally be sampled using non-interpolated floats (raw data) or compare sampling * (shadow maps). * * @type {GPUSampler[]} * @private */ this.samplers = []; /** @type {Texture} */ this.texture = texture; this.format = gpuTextureFormats[texture.format]; Debug.assert(this.format !== '', "WebGPU does not support texture format " + texture.format + " [" + ((_pixelFormatInfo_get = pixelFormatInfo.get(texture.format)) == null ? void 0 : _pixelFormatInfo_get.name) + "] for texture " + texture.name, texture); this.create(texture.device); } } export { WebgpuTexture };