@luma.gl/core
Version:
The luma.gl core Device API
472 lines • 19.5 kB
JavaScript
// luma.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import { Resource } from "./resource.js";
import { Sampler } from "./sampler.js";
import { log } from "../../utils/log.js";
import { textureFormatDecoder } from "../../shadertypes/texture-types/texture-format-decoder.js";
const BASE_DIMENSIONS = {
'1d': '1d',
'2d': '2d',
'2d-array': '2d',
cube: '2d',
'cube-array': '2d',
'3d': '3d'
};
/**
* Abstract Texture interface
* Texture Object
* https://gpuweb.github.io/gpuweb/#gputexture
*/
export class Texture extends Resource {
/** The texture can be bound for use as a sampled texture in a shader */
static SAMPLE = 0x04;
/** The texture can be bound for use as a storage texture in a shader */
static STORAGE = 0x08;
/** The texture can be used as a color or depth/stencil attachment in a render pass */
static RENDER = 0x10;
/** The texture can be used as the source of a copy operation */
static COPY_SRC = 0x01;
/** he texture can be used as the destination of a copy or write operation */
static COPY_DST = 0x02;
/** @deprecated Use Texture.SAMPLE */
static TEXTURE = 0x04;
/** @deprecated Use Texture.RENDER */
static RENDER_ATTACHMENT = 0x10;
/** dimension of this texture */
dimension;
/** base dimension of this texture */
baseDimension;
/** format of this texture */
format;
/** width in pixels of this texture */
width;
/** height in pixels of this texture */
height;
/** depth of this texture */
depth;
/** mip levels in this texture */
mipLevels;
/** sample count */
samples;
/** Rows are multiples of this length, padded with extra bytes if needed */
byteAlignment;
/** The ready promise is always resolved. It is provided for type compatibility with DynamicTexture. */
ready = Promise.resolve(this);
/** isReady is always true. It is provided for type compatibility with DynamicTexture. */
isReady = true;
/** "Time" of last update. Monotonically increasing timestamp. TODO move to DynamicTexture? */
updateTimestamp;
get [Symbol.toStringTag]() {
return 'Texture';
}
toString() {
return `Texture(${this.id},${this.format},${this.width}x${this.height})`;
}
/** Do not use directly. Create with device.createTexture() */
constructor(device, props, backendProps) {
props = Texture.normalizeProps(device, props);
super(device, props, Texture.defaultProps);
this.dimension = this.props.dimension;
this.baseDimension = BASE_DIMENSIONS[this.dimension];
this.format = this.props.format;
// Size
this.width = this.props.width;
this.height = this.props.height;
this.depth = this.props.depth;
this.mipLevels = this.props.mipLevels;
this.samples = this.props.samples || 1;
if (this.dimension === 'cube') {
this.depth = 6;
}
// Calculate size, if not provided
if (this.props.width === undefined || this.props.height === undefined) {
if (device.isExternalImage(props.data)) {
const size = device.getExternalImageSize(props.data);
this.width = size?.width || 1;
this.height = size?.height || 1;
}
else {
this.width = 1;
this.height = 1;
if (this.props.width === undefined || this.props.height === undefined) {
log.warn(`${this} created with undefined width or height. This is deprecated. Use DynamicTexture instead.`)();
}
}
}
this.byteAlignment = backendProps?.byteAlignment || 1;
// TODO - perhaps this should be set on async write completion?
this.updateTimestamp = device.incrementTimestamp();
}
/**
* Create a new texture with the same parameters and optionally a different size
* @note Textures are immutable and cannot be resized after creation, but we can create a similar texture with the same parameters but a new size.
* @note Does not copy contents of the texture
*/
clone(size) {
return this.device.createTexture({ ...this.props, ...size });
}
/** Set sampler props associated with this texture */
setSampler(sampler) {
this.sampler = sampler instanceof Sampler ? sampler : this.device.createSampler(sampler);
}
/**
* Copy raw image data (bytes) into the texture.
*
* @note Deprecated compatibility wrapper over {@link writeData}.
* @note Uses the same layout defaults and alignment rules as {@link writeData}.
* @note Tightly packed CPU uploads can omit `bytesPerRow` and `rowsPerImage`.
* @note If the CPU source rows are padded, pass explicit `bytesPerRow` and `rowsPerImage`.
* @deprecated Use writeData()
*/
copyImageData(options) {
const { data, depth, ...writeOptions } = options;
this.writeData(data, {
...writeOptions,
depthOrArrayLayers: writeOptions.depthOrArrayLayers ?? depth
});
}
/**
* Calculates the memory layout of the texture, required when reading and writing data.
* @return the backend-aligned linear layout, in particular bytesPerRow which includes any required padding for buffer copy/read paths
*/
computeMemoryLayout(options_ = {}) {
const options = this._normalizeTextureReadOptions(options_);
const { width = this.width, height = this.height, depthOrArrayLayers = this.depth } = options;
const { format, byteAlignment } = this;
// TODO - does the overriding above make sense?
// return textureFormatDecoder.computeMemoryLayout(this);
return textureFormatDecoder.computeMemoryLayout({
format,
width,
height,
depth: depthOrArrayLayers,
byteAlignment
});
}
/**
* Read the contents of a texture into a GPU Buffer.
* @returns A Buffer containing the texture data.
*
* @note The memory layout of the texture data is determined by the texture format and dimensions.
* @note The application can call Texture.computeMemoryLayout() to compute the backend-aligned layout.
* @note The application can call Buffer.readAsync() to read the returned buffer on the CPU.
* @note The destination buffer must be supplied by the caller and must be large enough for the requested region.
* @note On WebGPU this corresponds to a texture-to-buffer copy and uses buffer-copy alignment rules.
* @note On WebGL, luma.gl emulates the same logical readback behavior.
*/
readBuffer(options, buffer) {
throw new Error('readBuffer not implemented');
}
/**
* Reads data from a texture into an ArrayBuffer.
* @returns An ArrayBuffer containing the texture data.
*
* @note The memory layout of the texture data is determined by the texture format and dimensions.
* @note The application can call Texture.computeMemoryLayout() to compute the layout.
* @deprecated Use Texture.readBuffer() with an explicit destination buffer, or DynamicTexture.readAsync() for convenience readback.
*/
readDataAsync(options) {
throw new Error('readBuffer not implemented');
}
/**
* Writes a GPU Buffer into a texture.
*
* @param buffer - Source GPU buffer.
* @param options - Destination subresource, extent, and source layout options.
* @note The memory layout of the texture data is determined by the texture format and dimensions.
* @note The application can call Texture.computeMemoryLayout() to compute the backend-aligned layout.
* @note On WebGPU this corresponds to a buffer-to-texture copy and uses buffer-copy alignment rules.
* @note On WebGL, luma.gl emulates the same destination and layout semantics.
*/
writeBuffer(buffer, options) {
throw new Error('readBuffer not implemented');
}
/**
* Writes an array buffer into a texture.
*
* @param data - Source texel data.
* @param options - Destination subresource, extent, and source layout options.
* @note If `bytesPerRow` and `rowsPerImage` are omitted, luma.gl computes a tightly packed CPU-memory layout for the requested region.
* @note On WebGPU this corresponds to `GPUQueue.writeTexture()` and does not implicitly pad rows to 256 bytes.
* @note On WebGL, padded CPU data is supported via the same `bytesPerRow` and `rowsPerImage` options.
*/
writeData(data, options) {
throw new Error('readBuffer not implemented');
}
// IMPLEMENTATION SPECIFIC
/**
* WebGL can read data synchronously.
* @note While it is convenient, the performance penalty is very significant
*/
readDataSyncWebGL(options) {
throw new Error('readDataSyncWebGL not available');
}
/** Generate mipmaps (WebGL only) */
generateMipmapsWebGL() {
throw new Error('generateMipmapsWebGL not available');
}
// HELPERS
/** Ensure we have integer coordinates */
static normalizeProps(device, props) {
const newProps = { ...props };
// Ensure we have integer coordinates
const { width, height } = newProps;
if (typeof width === 'number') {
newProps.width = Math.max(1, Math.ceil(width));
}
if (typeof height === 'number') {
newProps.height = Math.max(1, Math.ceil(height));
}
return newProps;
}
/** Initialize texture with supplied props */
// eslint-disable-next-line max-statements
_initializeData(data) {
// Store opts for accessors
if (this.device.isExternalImage(data)) {
this.copyExternalImage({
image: data,
width: this.width,
height: this.height,
depth: this.depth,
mipLevel: 0,
x: 0,
y: 0,
z: 0,
aspect: 'all',
colorSpace: 'srgb',
premultipliedAlpha: false,
flipY: false
});
}
else if (data) {
this.copyImageData({
data,
// width: this.width,
// height: this.height,
// depth: this.depth,
mipLevel: 0,
x: 0,
y: 0,
z: 0,
aspect: 'all'
});
}
}
_normalizeCopyImageDataOptions(options_) {
const { data, depth, ...writeOptions } = options_;
const options = this._normalizeTextureWriteOptions({
...writeOptions,
depthOrArrayLayers: writeOptions.depthOrArrayLayers ?? depth
});
return { data, depth: options.depthOrArrayLayers, ...options };
}
_normalizeCopyExternalImageOptions(options_) {
const optionsWithoutUndefined = Texture._omitUndefined(options_);
const mipLevel = optionsWithoutUndefined.mipLevel ?? 0;
const mipLevelSize = this._getMipLevelSize(mipLevel);
const size = this.device.getExternalImageSize(options_.image);
const options = {
...Texture.defaultCopyExternalImageOptions,
...mipLevelSize,
...size,
...optionsWithoutUndefined
};
// WebGL will error if we try to copy outside the bounds of the texture
options.width = Math.min(options.width, mipLevelSize.width - options.x);
options.height = Math.min(options.height, mipLevelSize.height - options.y);
options.depth = Math.min(options.depth, mipLevelSize.depthOrArrayLayers - options.z);
return options;
}
_normalizeTextureReadOptions(options_) {
const optionsWithoutUndefined = Texture._omitUndefined(options_);
const mipLevel = optionsWithoutUndefined.mipLevel ?? 0;
const mipLevelSize = this._getMipLevelSize(mipLevel);
const options = {
...Texture.defaultTextureReadOptions,
...mipLevelSize,
...optionsWithoutUndefined
};
// WebGL will error if we try to copy outside the bounds of the texture
options.width = Math.min(options.width, mipLevelSize.width - options.x);
options.height = Math.min(options.height, mipLevelSize.height - options.y);
options.depthOrArrayLayers = Math.min(options.depthOrArrayLayers, mipLevelSize.depthOrArrayLayers - options.z);
return options;
}
/**
* Normalizes a texture read request and validates the color-only readback contract used by the
* current texture read APIs. Supported dimensions are `2d`, `cube`, `cube-array`,
* `2d-array`, and `3d`.
*
* @throws if the texture format, aspect, or dimension is not supported by the first-pass
* color-read implementation.
*/
_getSupportedColorReadOptions(options_) {
const options = this._normalizeTextureReadOptions(options_);
const formatInfo = textureFormatDecoder.getInfo(this.format);
this._validateColorReadAspect(options);
this._validateColorReadFormat(formatInfo);
switch (this.dimension) {
case '2d':
case 'cube':
case 'cube-array':
case '2d-array':
case '3d':
return options;
default:
throw new Error(`${this} color readback does not support ${this.dimension} textures`);
}
}
/** Validates that a read request targets the full color aspect of the texture. */
_validateColorReadAspect(options) {
if (options.aspect !== 'all') {
throw new Error(`${this} color readback only supports aspect 'all'`);
}
}
/** Validates that a read request targets an uncompressed color-renderable texture format. */
_validateColorReadFormat(formatInfo) {
if (formatInfo.compressed) {
throw new Error(`${this} color readback does not support compressed formats (${this.format})`);
}
switch (formatInfo.attachment) {
case 'color':
return;
case 'depth':
throw new Error(`${this} color readback does not support depth formats (${this.format})`);
case 'stencil':
throw new Error(`${this} color readback does not support stencil formats (${this.format})`);
case 'depth-stencil':
throw new Error(`${this} color readback does not support depth-stencil formats (${this.format})`);
default:
throw new Error(`${this} color readback does not support format ${this.format}`);
}
}
_normalizeTextureWriteOptions(options_) {
const optionsWithoutUndefined = Texture._omitUndefined(options_);
const mipLevel = optionsWithoutUndefined.mipLevel ?? 0;
const mipLevelSize = this._getMipLevelSize(mipLevel);
const options = {
...Texture.defaultTextureWriteOptions,
...mipLevelSize,
...optionsWithoutUndefined
};
options.width = Math.min(options.width, mipLevelSize.width - options.x);
options.height = Math.min(options.height, mipLevelSize.height - options.y);
options.depthOrArrayLayers = Math.min(options.depthOrArrayLayers, mipLevelSize.depthOrArrayLayers - options.z);
const layout = textureFormatDecoder.computeMemoryLayout({
format: this.format,
width: options.width,
height: options.height,
depth: options.depthOrArrayLayers,
byteAlignment: this.byteAlignment
});
const minimumBytesPerRow = layout.bytesPerPixel * options.width;
options.bytesPerRow = optionsWithoutUndefined.bytesPerRow ?? layout.bytesPerRow;
options.rowsPerImage = optionsWithoutUndefined.rowsPerImage ?? options.height;
if (options.bytesPerRow < minimumBytesPerRow) {
throw new Error(`bytesPerRow (${options.bytesPerRow}) must be at least ${minimumBytesPerRow} for ${this.format}`);
}
if (options.rowsPerImage < options.height) {
throw new Error(`rowsPerImage (${options.rowsPerImage}) must be at least ${options.height} for ${this.format}`);
}
const bytesPerPixel = this.device.getTextureFormatInfo(this.format).bytesPerPixel;
if (bytesPerPixel && options.bytesPerRow % bytesPerPixel !== 0) {
throw new Error(`bytesPerRow (${options.bytesPerRow}) must be a multiple of bytesPerPixel (${bytesPerPixel}) for ${this.format}`);
}
return options;
}
_getMipLevelSize(mipLevel) {
const width = Math.max(1, this.width >> mipLevel);
const height = this.baseDimension === '1d' ? 1 : Math.max(1, this.height >> mipLevel);
const depthOrArrayLayers = this.dimension === '3d' ? Math.max(1, this.depth >> mipLevel) : this.depth;
return { width, height, depthOrArrayLayers };
}
getAllocatedByteLength() {
let allocatedByteLength = 0;
for (let mipLevel = 0; mipLevel < this.mipLevels; mipLevel++) {
const { width, height, depthOrArrayLayers } = this._getMipLevelSize(mipLevel);
allocatedByteLength += textureFormatDecoder.computeMemoryLayout({
format: this.format,
width,
height,
depth: depthOrArrayLayers,
byteAlignment: 1
}).byteLength;
}
return allocatedByteLength * this.samples;
}
static _omitUndefined(options) {
return Object.fromEntries(Object.entries(options).filter(([, value]) => value !== undefined));
}
static defaultProps = {
...Resource.defaultProps,
data: null,
dimension: '2d',
format: 'rgba8unorm',
usage: Texture.SAMPLE | Texture.RENDER | Texture.COPY_DST,
width: undefined,
height: undefined,
depth: 1,
mipLevels: 1,
samples: undefined,
sampler: {},
view: undefined
};
static defaultCopyDataOptions = {
data: undefined,
byteOffset: 0,
bytesPerRow: undefined,
rowsPerImage: undefined,
width: undefined,
height: undefined,
depthOrArrayLayers: undefined,
depth: 1,
mipLevel: 0,
x: 0,
y: 0,
z: 0,
aspect: 'all'
};
/** Default options */
static defaultCopyExternalImageOptions = {
image: undefined,
sourceX: 0,
sourceY: 0,
width: undefined,
height: undefined,
depth: 1,
mipLevel: 0,
x: 0,
y: 0,
z: 0,
aspect: 'all',
colorSpace: 'srgb',
premultipliedAlpha: false,
flipY: false
};
static defaultTextureReadOptions = {
x: 0,
y: 0,
z: 0,
width: undefined,
height: undefined,
depthOrArrayLayers: 1,
mipLevel: 0,
aspect: 'all'
};
static defaultTextureWriteOptions = {
byteOffset: 0,
bytesPerRow: undefined,
rowsPerImage: undefined,
x: 0,
y: 0,
z: 0,
width: undefined,
height: undefined,
depthOrArrayLayers: 1,
mipLevel: 0,
aspect: 'all'
};
}
//# sourceMappingURL=texture.js.map