mirror of
https://github.com/thorvg/thorvg.git
synced 2025-06-08 05:33:36 +00:00

Streaming model for massive vertex and index creations: minimize memory allocations, range checks and other conditions Reduce number of segments length calculations (sqrt) and bbox (min and max). Update distances and bboxes on a whole buffer and only if necessary. For shapes without strokes compute distances not necessary at all. bbox can be updated only on the final stage of geometry workflow, but not on the each stage. Using stack memory instead of heap. its more cache friendly and did not fragment memory, faster memory allocations (weak place of realization) Using cache for points distances and whole path length. Updates only if necessary Validation of geometry consistency executes only on the final stage of path life cicle. It more friendly for data streaming: no any conditions and branches. Using binary search for strokes trimming Pre-cached circles geometry for caps and joints Refactored strokes elements generation functions. Code is more readable and modifiable in general. Can be easily fixed if some geometry issues will be finded
498 lines
18 KiB
C++
Executable file
498 lines
18 KiB
C++
Executable file
|
|
/*
|
|
* Copyright (c) 2023 - 2024 the ThorVG project. All rights reserved.
|
|
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
* copies or substantial portions of the Software.
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
* SOFTWARE.
|
|
*/
|
|
|
|
#include <algorithm>
|
|
#include "tvgMath.h"
|
|
#include "tvgWgRenderData.h"
|
|
#include "tvgWgShaderTypes.h"
|
|
|
|
//***********************************************************************
|
|
// WgMeshData
|
|
//***********************************************************************
|
|
|
|
void WgMeshData::draw(WgContext& context, WGPURenderPassEncoder renderPassEncoder)
|
|
{
|
|
wgpuRenderPassEncoderSetVertexBuffer(renderPassEncoder, 0, bufferPosition, 0, vertexCount * sizeof(float) * 2);
|
|
wgpuRenderPassEncoderSetIndexBuffer(renderPassEncoder, bufferIndex, WGPUIndexFormat_Uint32, 0, indexCount * sizeof(uint32_t));
|
|
wgpuRenderPassEncoderDrawIndexed(renderPassEncoder, indexCount, 1, 0, 0, 0);
|
|
}
|
|
|
|
|
|
void WgMeshData::drawFan(WgContext& context, WGPURenderPassEncoder renderPassEncoder)
|
|
{
|
|
wgpuRenderPassEncoderSetVertexBuffer(renderPassEncoder, 0, bufferPosition, 0, vertexCount * sizeof(float) * 2);
|
|
wgpuRenderPassEncoderSetIndexBuffer(renderPassEncoder, context.bufferIndexFan, WGPUIndexFormat_Uint32, 0, indexCount * sizeof(uint32_t));
|
|
wgpuRenderPassEncoderDrawIndexed(renderPassEncoder, indexCount, 1, 0, 0, 0);
|
|
}
|
|
|
|
|
|
void WgMeshData::drawImage(WgContext& context, WGPURenderPassEncoder renderPassEncoder)
|
|
{
|
|
wgpuRenderPassEncoderSetVertexBuffer(renderPassEncoder, 0, bufferPosition, 0, vertexCount * sizeof(float) * 2);
|
|
wgpuRenderPassEncoderSetVertexBuffer(renderPassEncoder, 1, bufferTexCoord, 0, vertexCount * sizeof(float) * 2);
|
|
wgpuRenderPassEncoderSetIndexBuffer(renderPassEncoder, bufferIndex, WGPUIndexFormat_Uint32, 0, indexCount * sizeof(uint32_t));
|
|
wgpuRenderPassEncoderDrawIndexed(renderPassEncoder, indexCount, 1, 0, 0, 0);
|
|
};
|
|
|
|
|
|
void WgMeshData::update(WgContext& context, const WgVertexBuffer& vertexBuffer)
|
|
{
|
|
assert(vertexBuffer.vcount > 2);
|
|
vertexCount = vertexBuffer.vcount;
|
|
indexCount = (vertexBuffer.vcount - 2) * 3;
|
|
// buffer position data create and write
|
|
context.allocateBufferVertex(bufferPosition, (float *)&vertexBuffer.vbuff, vertexCount * sizeof(float) * 2);
|
|
// buffer index data create and write
|
|
context.allocateBufferIndexFan(vertexCount);
|
|
}
|
|
|
|
|
|
void WgMeshData::update(WgContext& context, const WgVertexBufferInd& vertexBufferInd)
|
|
{
|
|
assert(vertexBufferInd.vcount > 2);
|
|
vertexCount = vertexBufferInd.vcount;
|
|
indexCount = vertexBufferInd.icount;
|
|
// buffer position data create and write
|
|
if (vertexCount > 0)
|
|
context.allocateBufferVertex(bufferPosition, (float *)&vertexBufferInd.vbuff, vertexCount * sizeof(float) * 2);
|
|
// buffer tex coords data create and write
|
|
if (vertexCount > 0)
|
|
context.allocateBufferVertex(bufferTexCoord, (float *)&vertexBufferInd.tbuff, vertexCount * sizeof(float) * 2);
|
|
// buffer index data create and write
|
|
if (indexCount > 0)
|
|
context.allocateBufferIndex(bufferIndex, vertexBufferInd.ibuff, indexCount * sizeof(uint32_t));
|
|
};
|
|
|
|
|
|
void WgMeshData::update(WgContext& context, const Point pmin, const Point pmax)
|
|
{
|
|
vertexCount = 4;
|
|
indexCount = 6;
|
|
const float data[] = {
|
|
pmin.x, pmin.y, pmax.x, pmin.y,
|
|
pmax.x, pmax.y, pmin.x, pmax.y
|
|
};
|
|
context.allocateBufferVertex(bufferPosition, data, sizeof(data));
|
|
context.allocateBufferIndexFan(vertexCount);
|
|
}
|
|
|
|
|
|
void WgMeshData::release(WgContext& context)
|
|
{
|
|
context.releaseBuffer(bufferIndex);
|
|
context.releaseBuffer(bufferTexCoord);
|
|
context.releaseBuffer(bufferPosition);
|
|
};
|
|
|
|
|
|
//***********************************************************************
|
|
// WgMeshDataPool
|
|
//***********************************************************************
|
|
|
|
WgMeshData* WgMeshDataPool::allocate(WgContext& context)
|
|
{
|
|
WgMeshData* meshData{};
|
|
if (mPool.count > 0) {
|
|
meshData = mPool.last();
|
|
mPool.pop();
|
|
} else {
|
|
meshData = new WgMeshData();
|
|
mList.push(meshData);
|
|
}
|
|
return meshData;
|
|
}
|
|
|
|
|
|
void WgMeshDataPool::free(WgContext& context, WgMeshData* meshData)
|
|
{
|
|
mPool.push(meshData);
|
|
}
|
|
|
|
|
|
void WgMeshDataPool::release(WgContext& context)
|
|
{
|
|
for (uint32_t i = 0; i < mList.count; i++) {
|
|
mList[i]->release(context);
|
|
delete mList[i];
|
|
}
|
|
mPool.clear();
|
|
mList.clear();
|
|
}
|
|
|
|
WgMeshDataPool* WgMeshDataGroup::gMeshDataPool = nullptr;
|
|
|
|
//***********************************************************************
|
|
// WgMeshDataGroup
|
|
//***********************************************************************
|
|
|
|
void WgMeshDataGroup::append(WgContext& context, const WgVertexBuffer& vertexBuffer)
|
|
{
|
|
assert(vertexBuffer.vcount >= 3);
|
|
meshes.push(gMeshDataPool->allocate(context));
|
|
meshes.last()->update(context, vertexBuffer);
|
|
}
|
|
|
|
|
|
void WgMeshDataGroup::append(WgContext& context, const WgVertexBufferInd& vertexBufferInd)
|
|
{
|
|
assert(vertexBufferInd.vcount >= 3);
|
|
meshes.push(gMeshDataPool->allocate(context));
|
|
meshes.last()->update(context, vertexBufferInd);
|
|
}
|
|
|
|
|
|
void WgMeshDataGroup::append(WgContext& context, const Point pmin, const Point pmax)
|
|
{
|
|
meshes.push(gMeshDataPool->allocate(context));
|
|
meshes.last()->update(context, pmin, pmax);
|
|
}
|
|
|
|
|
|
void WgMeshDataGroup::release(WgContext& context)
|
|
{
|
|
for (uint32_t i = 0; i < meshes.count; i++)
|
|
gMeshDataPool->free(context, meshes[i]);
|
|
meshes.clear();
|
|
};
|
|
|
|
|
|
//***********************************************************************
|
|
// WgImageData
|
|
//***********************************************************************
|
|
|
|
void WgImageData::update(WgContext& context, RenderSurface* surface)
|
|
{
|
|
release(context);
|
|
assert(surface);
|
|
texture = context.createTexture(surface->w, surface->h, WGPUTextureFormat_RGBA8Unorm);
|
|
assert(texture);
|
|
textureView = context.createTextureView(texture);
|
|
assert(textureView);
|
|
// update texture data
|
|
WGPUImageCopyTexture imageCopyTexture{ .texture = texture };
|
|
WGPUTextureDataLayout textureDataLayout{ .bytesPerRow = 4 * surface->w, .rowsPerImage = surface->h };
|
|
WGPUExtent3D writeSize{ .width = surface->w, .height = surface->h, .depthOrArrayLayers = 1 };
|
|
wgpuQueueWriteTexture(context.queue, &imageCopyTexture, surface->data, 4 * surface->w * surface->h, &textureDataLayout, &writeSize);
|
|
wgpuQueueSubmit(context.queue, 0, nullptr);
|
|
};
|
|
|
|
|
|
void WgImageData::release(WgContext& context)
|
|
{
|
|
context.releaseTextureView(textureView);
|
|
context.releaseTexture(texture);
|
|
};
|
|
|
|
//***********************************************************************
|
|
// WgRenderSettings
|
|
//***********************************************************************
|
|
|
|
void WgRenderSettings::update(WgContext& context, const Fill* fill, const uint8_t* color, const RenderUpdateFlag flags)
|
|
{
|
|
// setup fill properties
|
|
if ((flags & (RenderUpdateFlag::Gradient)) && fill) {
|
|
rasterType = WgRenderRasterType::Gradient;
|
|
// get gradient transfrom matrix
|
|
Matrix fillTransform = fill->transform();
|
|
Matrix invFillTransform;
|
|
WgShaderTypeMat4x4f gradientTrans; // identity by default
|
|
if (inverse(&fillTransform, &invFillTransform))
|
|
gradientTrans.update(invFillTransform);
|
|
// get gradient rasterisation settings
|
|
WgShaderTypeGradient gradient;
|
|
if (fill->type() == Type::LinearGradient) {
|
|
gradient.update((LinearGradient*)fill);
|
|
fillType = WgRenderSettingsType::Linear;
|
|
} else if (fill->type() == Type::RadialGradient) {
|
|
gradient.update((RadialGradient*)fill);
|
|
fillType = WgRenderSettingsType::Radial;
|
|
}
|
|
// update gpu assets
|
|
bool bufferGradientSettingsChanged = context.allocateBufferUniform(bufferGroupGradient, &gradient.settings, sizeof(gradient.settings));
|
|
bool bufferGradientTransformChanged = context.allocateBufferUniform(bufferGroupTransfromGrad, &gradientTrans.mat, sizeof(gradientTrans.mat));
|
|
bool textureGradientChanged = context.allocateTexture(texGradient, WG_TEXTURE_GRADIENT_SIZE, 1, WGPUTextureFormat_RGBA8Unorm, gradient.texData);
|
|
if (bufferGradientSettingsChanged || textureGradientChanged || bufferGradientTransformChanged) {
|
|
// update texture view
|
|
context.releaseTextureView(texViewGradient);
|
|
texViewGradient = context.createTextureView(texGradient);
|
|
// get sampler by spread type
|
|
WGPUSampler sampler = context.samplerLinearClamp;
|
|
if (fill->spread() == FillSpread::Reflect) sampler = context.samplerLinearMirror;
|
|
if (fill->spread() == FillSpread::Repeat) sampler = context.samplerLinearRepeat;
|
|
// update bind group
|
|
context.pipelines->layouts.releaseBindGroup(bindGroupGradient);
|
|
bindGroupGradient = context.pipelines->layouts.createBindGroupTexSampledBuff2Un(
|
|
sampler, texViewGradient, bufferGroupGradient, bufferGroupTransfromGrad);
|
|
}
|
|
skip = false;
|
|
} else if ((flags & (RenderUpdateFlag::Color)) && !fill) {
|
|
rasterType = WgRenderRasterType::Solid;
|
|
WgShaderTypeSolidColor solidColor(color);
|
|
if (context.allocateBufferUniform(bufferGroupSolid, &solidColor, sizeof(solidColor))) {
|
|
context.pipelines->layouts.releaseBindGroup(bindGroupSolid);
|
|
bindGroupSolid = context.pipelines->layouts.createBindGroupBuffer1Un(bufferGroupSolid);
|
|
}
|
|
fillType = WgRenderSettingsType::Solid;
|
|
skip = (color[3] == 0);
|
|
}
|
|
};
|
|
|
|
|
|
void WgRenderSettings::release(WgContext& context)
|
|
{
|
|
context.pipelines->layouts.releaseBindGroup(bindGroupSolid);
|
|
context.pipelines->layouts.releaseBindGroup(bindGroupGradient);
|
|
context.releaseBuffer(bufferGroupSolid);
|
|
context.releaseBuffer(bufferGroupGradient);
|
|
context.releaseBuffer(bufferGroupTransfromGrad);
|
|
context.releaseTexture(texGradient);
|
|
context.releaseTextureView(texViewGradient);
|
|
};
|
|
|
|
//***********************************************************************
|
|
// WgRenderDataPaint
|
|
//***********************************************************************
|
|
|
|
void WgRenderDataPaint::release(WgContext& context)
|
|
{
|
|
context.pipelines->layouts.releaseBindGroup(bindGroupPaint);
|
|
context.releaseBuffer(bufferModelMat);
|
|
context.releaseBuffer(bufferBlendSettings);
|
|
clips.clear();
|
|
};
|
|
|
|
|
|
void WgRenderDataPaint::update(WgContext& context, const tvg::Matrix& transform, tvg::ColorSpace cs, uint8_t opacity)
|
|
{
|
|
WgShaderTypeMat4x4f modelMat(transform);
|
|
WgShaderTypeBlendSettings blendSettings(cs, opacity);
|
|
bool bufferModelMatChanged = context.allocateBufferUniform(bufferModelMat, &modelMat, sizeof(modelMat));
|
|
bool bufferBlendSettingsChanged = context.allocateBufferUniform(bufferBlendSettings, &blendSettings, sizeof(blendSettings));
|
|
if (bufferModelMatChanged || bufferBlendSettingsChanged) {
|
|
context.pipelines->layouts.releaseBindGroup(bindGroupPaint);
|
|
bindGroupPaint = context.pipelines->layouts.createBindGroupBuffer2Un(bufferModelMat, bufferBlendSettings);
|
|
}
|
|
}
|
|
|
|
|
|
void WgRenderDataPaint::updateClips(tvg::Array<tvg::RenderData> &clips) {
|
|
this->clips.clear();
|
|
for (uint32_t i = 0; i < clips.count; i++)
|
|
if (clips[i])
|
|
this->clips.push((WgRenderDataPaint*)clips[i]);
|
|
}
|
|
|
|
//***********************************************************************
|
|
// WgRenderDataShape
|
|
//***********************************************************************
|
|
|
|
void WgRenderDataShape::appendShape(WgContext context, const WgVertexBuffer& vertexBuffer)
|
|
{
|
|
if (vertexBuffer.vcount < 3) return;
|
|
Point pmin{}, pmax{};
|
|
vertexBuffer.getMinMax(pmin, pmax);
|
|
meshGroupShapes.append(context, vertexBuffer);
|
|
meshGroupShapesBBox.append(context, pmin, pmax);
|
|
updateBBox(pmin, pmax);
|
|
}
|
|
|
|
|
|
void WgRenderDataShape::appendStroke(WgContext context, const WgVertexBufferInd& vertexBufferInd)
|
|
{
|
|
if (vertexBufferInd.vcount < 3) return;
|
|
Point pmin{}, pmax{};
|
|
vertexBufferInd.getMinMax(pmin, pmax);
|
|
meshGroupStrokes.append(context, vertexBufferInd);
|
|
meshGroupStrokesBBox.append(context, pmin, pmax);
|
|
updateBBox(pmin, pmax);
|
|
}
|
|
|
|
|
|
void WgRenderDataShape::updateBBox(Point pmin, Point pmax)
|
|
{
|
|
pMin.x = std::min(pMin.x, pmin.x);
|
|
pMin.y = std::min(pMin.y, pmin.y);
|
|
pMax.x = std::max(pMax.x, pmax.x);
|
|
pMax.y = std::max(pMax.y, pmax.y);
|
|
}
|
|
|
|
|
|
void WgRenderDataShape::updateAABB(const Matrix& tr) {
|
|
Point p0 = Point{pMin.x, pMin.y} * tr;
|
|
Point p1 = Point{pMax.x, pMin.y} * tr;
|
|
Point p2 = Point{pMin.x, pMax.y} * tr;
|
|
Point p3 = Point{pMax.x, pMax.y} * tr;
|
|
aabb.x = std::min({p0.x, p1.x, p2.x, p3.x});
|
|
aabb.y = std::min({p0.y, p1.y, p2.y, p3.y});
|
|
aabb.w = std::max({p0.x, p1.x, p2.x, p3.x}) - aabb.x;
|
|
aabb.h = std::max({p0.y, p1.y, p2.y, p3.y}) - aabb.y;
|
|
}
|
|
|
|
|
|
void WgRenderDataShape::updateMeshes(WgContext &context, const RenderShape &rshape, const Matrix& tr)
|
|
{
|
|
releaseMeshes(context);
|
|
strokeFirst = rshape.stroke ? rshape.stroke->strokeFirst : false;
|
|
|
|
// path decoded vertex buffer
|
|
WgVertexBuffer pbuff;
|
|
// append shape without strokes
|
|
if (!rshape.stroke) {
|
|
pbuff.decodePath(rshape, false, [&](const WgVertexBuffer& path_buff) {
|
|
appendShape(context, path_buff);
|
|
});
|
|
// append shape with strokes
|
|
} else if (rshape.stroke->trim.simultaneous) {
|
|
float tbeg{}, tend{};
|
|
if (!rshape.stroke->strokeTrim(tbeg, tend)) { tbeg = 0.0f; tend = 1.0f; }
|
|
if (tbeg == tend) return;
|
|
pbuff.decodePath(rshape, true, [&](const WgVertexBuffer& path_buff) {
|
|
appendShape(context, path_buff);
|
|
proceedStrokes(context, rshape.stroke, tbeg, tend, path_buff);
|
|
});
|
|
// append shape with strokes with simultaneous flag
|
|
} else {
|
|
float totalLen = 0.0f;
|
|
// append shapes
|
|
pbuff.decodePath(rshape, true, [&](const WgVertexBuffer& path_buff) {
|
|
appendShape(context, path_buff);
|
|
totalLen += path_buff.total();
|
|
});
|
|
// append strokes
|
|
float tbeg{}, tend{};
|
|
if (!rshape.stroke->strokeTrim(tbeg, tend)) { tbeg = 0.0f; tend = 1.0f; }
|
|
if (tbeg == tend) return;
|
|
float len_beg = totalLen * tbeg; // trim length begin
|
|
float len_end = totalLen * tend; // trim length end
|
|
float len_acc = 0.0; // accumulated length
|
|
// append strokes
|
|
pbuff.decodePath(rshape, true, [&](const WgVertexBuffer& path_buff) {
|
|
float len_path = path_buff.total(); // current path length
|
|
float tbeg = ((len_acc <= len_beg) && (len_acc + len_path > len_beg)) ? (len_beg - len_acc) / len_path : 0.0f;
|
|
float tend = ((len_acc <= len_end) && (len_acc + len_path > len_end)) ? (len_end - len_acc) / len_path : 1.0f;
|
|
if ((len_acc + len_path >= len_beg) && (len_acc <= len_end))
|
|
proceedStrokes(context, rshape.stroke, tbeg, tend, path_buff);
|
|
len_acc += len_path;
|
|
});
|
|
}
|
|
// update shapes bbox
|
|
updateAABB(tr);
|
|
meshDataBBox.update(context, pMin, pMax);
|
|
}
|
|
|
|
|
|
void WgRenderDataShape::proceedStrokes(WgContext context, const RenderStroke* rstroke, float tbeg, float tend, const WgVertexBuffer& buff)
|
|
{
|
|
assert(rstroke);
|
|
WgVertexBufferInd stroke_buff;
|
|
// trim -> dash -> stroke
|
|
if ((tbeg != 0.0f) || (tend != 1.0f)) {
|
|
if (tbeg == tend) return;
|
|
WgVertexBuffer trimed_buff;
|
|
trimed_buff.trim(buff, tbeg, tend);
|
|
trimed_buff.updateDistances();
|
|
// trim ->dash -> stroke
|
|
if (rstroke->dashPattern) stroke_buff.appendStrokesDashed(trimed_buff, rstroke);
|
|
// trim -> stroke
|
|
else stroke_buff.appendStrokes(trimed_buff, rstroke);
|
|
} else
|
|
// dash -> stroke
|
|
if (rstroke->dashPattern) {
|
|
stroke_buff.appendStrokesDashed(buff, rstroke);
|
|
// stroke
|
|
} else
|
|
stroke_buff.appendStrokes(buff, rstroke);
|
|
appendStroke(context, stroke_buff);
|
|
}
|
|
|
|
|
|
void WgRenderDataShape::releaseMeshes(WgContext &context)
|
|
{
|
|
meshGroupStrokesBBox.release(context);
|
|
meshGroupStrokes.release(context);
|
|
meshGroupShapesBBox.release(context);
|
|
meshGroupShapes.release(context);
|
|
pMin = {FLT_MAX, FLT_MAX};
|
|
pMax = {0.0f, 0.0f};
|
|
clips.clear();
|
|
}
|
|
|
|
|
|
void WgRenderDataShape::release(WgContext& context)
|
|
{
|
|
releaseMeshes(context);
|
|
meshDataBBox.release(context);
|
|
renderSettingsStroke.release(context);
|
|
renderSettingsShape.release(context);
|
|
WgRenderDataPaint::release(context);
|
|
};
|
|
|
|
//***********************************************************************
|
|
// WgRenderDataShapePool
|
|
//***********************************************************************
|
|
|
|
WgRenderDataShape* WgRenderDataShapePool::allocate(WgContext& context)
|
|
{
|
|
WgRenderDataShape* dataShape{};
|
|
if (mPool.count > 0) {
|
|
dataShape = mPool.last();
|
|
mPool.pop();
|
|
} else {
|
|
dataShape = new WgRenderDataShape();
|
|
mList.push(dataShape);
|
|
}
|
|
return dataShape;
|
|
}
|
|
|
|
|
|
void WgRenderDataShapePool::free(WgContext& context, WgRenderDataShape* dataShape)
|
|
{
|
|
dataShape->meshGroupShapes.release(context);
|
|
dataShape->meshGroupShapesBBox.release(context);
|
|
dataShape->meshGroupStrokes.release(context);
|
|
dataShape->meshGroupStrokesBBox.release(context);
|
|
dataShape->clips.clear();
|
|
mPool.push(dataShape);
|
|
}
|
|
|
|
|
|
void WgRenderDataShapePool::release(WgContext& context)
|
|
{
|
|
for (uint32_t i = 0; i < mList.count; i++) {
|
|
mList[i]->release(context);
|
|
delete mList[i];
|
|
}
|
|
mPool.clear();
|
|
mList.clear();
|
|
}
|
|
|
|
//***********************************************************************
|
|
// WgRenderDataPicture
|
|
//***********************************************************************
|
|
|
|
void WgRenderDataPicture::release(WgContext& context)
|
|
{
|
|
imageData.release(context);
|
|
context.pipelines->layouts.releaseBindGroup(bindGroupPicture);
|
|
WgRenderDataPaint::release(context);
|
|
}
|