axmol/core/renderer/backend/metal/CommandBufferMTL.mm

604 lines
20 KiB
Plaintext
Raw Normal View History

2019-11-23 20:27:39 +08:00
/****************************************************************************
Copyright (c) 2018-2019 Xiamen Yaji Software Co., Ltd.
2021-09-30 08:18:01 +08:00
Copyright (c) 2021 Bytedance Inc.
2019-11-23 20:27:39 +08:00
2022-10-01 16:24:52 +08:00
https://axmolengine.github.io/
2019-11-23 20:27:39 +08:00
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.
****************************************************************************/
2019-11-23 20:27:39 +08:00
#include "CommandBufferMTL.h"
#include "BufferMTL.h"
#include "DeviceMTL.h"
#include "RenderPipelineMTL.h"
#include "TextureMTL.h"
2020-09-11 00:10:44 +08:00
#include "UtilsMTL.h"
2019-11-23 20:27:39 +08:00
#include "../Macros.h"
#include "BufferManager.h"
#include "DepthStencilStateMTL.h"
#include "RenderTargetMTL.h"
2019-11-23 20:27:39 +08:00
NS_AX_BACKEND_BEGIN
2019-11-23 20:27:39 +08:00
namespace
{
#define byte(n) ((n)*8)
#define bit(n) (n)
static uint8_t getBitsPerElementMTL(MTLPixelFormat pixleFormat)
{
switch (pixleFormat)
{
case MTLPixelFormatDepth32Float_Stencil8:
return byte(8);
case MTLPixelFormatBGRA8Unorm:
case MTLPixelFormatRGBA8Unorm:
case MTLPixelFormatDepth32Float:
return byte(4);
2022-07-16 10:43:05 +08:00
#if (AX_TARGET_PLATFORM == AX_PLATFORM_MAC)
case MTLPixelFormatDepth24Unorm_Stencil8:
return byte(4);
#else
case MTLPixelFormatABGR4Unorm:
case MTLPixelFormatBGR5A1Unorm:
case MTLPixelFormatB5G6R5Unorm:
case MTLPixelFormatA1BGR5Unorm:
return byte(2);
#endif
case MTLPixelFormatA8Unorm:
case MTLPixelFormatR8Unorm:
return byte(1);
default:
assert(false);
break;
}
return 0;
}
static MTLWinding toMTLWinding(Winding winding)
{
if (Winding::CLOCK_WISE == winding)
return MTLWindingClockwise;
else
return MTLWindingCounterClockwise;
}
static MTLPrimitiveType toMTLPrimitive(PrimitiveType primitiveType)
{
MTLPrimitiveType ret = MTLPrimitiveTypeTriangle;
switch (primitiveType)
2021-12-31 11:00:35 +08:00
{
case PrimitiveType::POINT:
ret = MTLPrimitiveTypePoint;
break;
case PrimitiveType::LINE:
ret = MTLPrimitiveTypeLine;
break;
case PrimitiveType::LINE_STRIP:
ret = MTLPrimitiveTypeLineStrip;
break;
case PrimitiveType::TRIANGLE:
ret = MTLPrimitiveTypeTriangle;
break;
case PrimitiveType::TRIANGLE_STRIP:
ret = MTLPrimitiveTypeTriangleStrip;
break;
default:
break;
2021-12-31 11:00:35 +08:00
}
return ret;
}
static MTLIndexType toMTLIndexType(IndexFormat indexFormat)
{
if (IndexFormat::U_SHORT == indexFormat)
return MTLIndexTypeUInt16;
else
return MTLIndexTypeUInt32;
}
static MTLCullMode toMTLCullMode(CullMode mode)
{
switch (mode)
2021-12-31 11:00:35 +08:00
{
case CullMode::NONE:
return MTLCullModeNone;
case CullMode::FRONT:
return MTLCullModeFront;
case CullMode::BACK:
return MTLCullModeBack;
}
}
static MTLRenderPassDescriptor* toMTLRenderPassDescriptor(const RenderTarget* rt, const RenderPassDescriptor& desc)
{
MTLRenderPassDescriptor* mtlDescritpor = [MTLRenderPassDescriptor renderPassDescriptor];
auto rtMTL = static_cast<const RenderTargetMTL*>(rt);
rtMTL->applyRenderPassAttachments(desc, mtlDescritpor);
return mtlDescritpor;
}
static id<MTLTexture> getMTLTexture(TextureBackend* texture, int index)
{
return reinterpret_cast<id<MTLTexture>>(texture->getHandler(index));
}
static id<MTLSamplerState> getMTLSamplerState(TextureBackend* texture)
{
switch (texture->getTextureType())
{
case TextureType::TEXTURE_2D:
return static_cast<TextureMTL*>(texture)->getMTLSamplerState();
case TextureType::TEXTURE_CUBE:
return static_cast<TextureCubeMTL*>(texture)->getMTLSamplerState();
default:
assert(false);
return nil;
2021-12-31 11:00:35 +08:00
}
}
inline int clamp(int value, int min, int max)
2021-12-31 11:00:35 +08:00
{
return std::min(max, std::max(min, value));
2021-12-31 11:00:35 +08:00
}
}
CommandBufferMTL::CommandBufferMTL(DeviceMTL* deviceMTL)
: _mtlCommandQueue(deviceMTL->getMTLCommandQueue())
, _frameBoundarySemaphore(dispatch_semaphore_create(MAX_INFLIGHT_BUFFER))
{}
2019-11-23 20:27:39 +08:00
CommandBufferMTL::~CommandBufferMTL()
{
2020-09-10 18:12:46 +08:00
// Wait for all frames to finish by submitting and waiting on a dummy command buffer.
flush();
id<MTLCommandBuffer> oneOffBuffer = [_mtlCommandQueue commandBuffer];
[oneOffBuffer commit];
[oneOffBuffer waitUntilCompleted];
2019-11-23 20:27:39 +08:00
dispatch_semaphore_signal(_frameBoundarySemaphore);
}
void CommandBufferMTL::setDepthStencilState(DepthStencilState* depthStencilState)
{
_depthStencilStateMTL = static_cast<DepthStencilStateMTL*>(depthStencilState);
}
void CommandBufferMTL::setRenderPipeline(RenderPipeline* renderPipeline)
{
_renderPipelineMTL = static_cast<RenderPipelineMTL*>(renderPipeline);
}
bool CommandBufferMTL::beginFrame()
2019-11-23 20:27:39 +08:00
{
_autoReleasePool = [[NSAutoreleasePool alloc] init];
dispatch_semaphore_wait(_frameBoundarySemaphore, DISPATCH_TIME_FOREVER);
2019-11-23 20:27:39 +08:00
_mtlCommandBuffer = [_mtlCommandQueue commandBuffer];
// [_mtlCommandBuffer enqueue];
// commit will enqueue automatically
[_mtlCommandBuffer retain];
2019-11-23 20:27:39 +08:00
BufferManager::beginFrame();
return true;
2019-11-23 20:27:39 +08:00
}
void CommandBufferMTL::updateRenderCommandEncoder(const RenderTarget* renderTarget,
const RenderPassDescriptor& renderPassDesc)
2019-11-23 20:27:39 +08:00
{
if (_mtlRenderEncoder != nil && _currentRenderPassDesc == renderPassDesc && _currentRenderTarget == renderTarget &&
2022-06-24 14:18:48 +08:00
_currentRenderTargetFlags == renderTarget->getTargetFlags() &&
!renderTarget->isDirty())
2019-11-23 20:27:39 +08:00
{
2022-08-02 21:48:16 +08:00
return;
2019-11-23 20:27:39 +08:00
}
_currentRenderTarget = renderTarget;
_currentRenderPassDesc = renderPassDesc;
_currentRenderTargetFlags = renderTarget->getTargetFlags();
if (_mtlRenderEncoder != nil)
2019-11-23 20:27:39 +08:00
{
[_mtlRenderEncoder endEncoding];
[_mtlRenderEncoder release];
_mtlRenderEncoder = nil;
}
auto mtlDescriptor = toMTLRenderPassDescriptor(renderTarget, renderPassDesc);
_renderTargetWidth = (unsigned int)mtlDescriptor.colorAttachments[0].texture.width;
2019-11-23 20:27:39 +08:00
_renderTargetHeight = (unsigned int)mtlDescriptor.colorAttachments[0].texture.height;
_mtlRenderEncoder = [_mtlCommandBuffer renderCommandEncoderWithDescriptor:mtlDescriptor];
[_mtlRenderEncoder retain];
2019-11-23 20:27:39 +08:00
}
void CommandBufferMTL::beginRenderPass(const RenderTarget* renderTarget, const RenderPassDescriptor& renderPassDesc)
2019-11-23 20:27:39 +08:00
{
updateRenderCommandEncoder(renderTarget, renderPassDesc);
// [_mtlRenderEncoder setFrontFacingWinding:MTLWindingCounterClockwise];
2019-11-23 20:27:39 +08:00
}
void CommandBufferMTL::updateDepthStencilState(const DepthStencilDescriptor& descriptor)
2019-11-23 20:27:39 +08:00
{
_depthStencilStateMTL->update(descriptor);
}
void CommandBufferMTL::updatePipelineState(const RenderTarget* rt, const PipelineDescriptor& descriptor)
{
_renderPipelineMTL->update(rt, descriptor);
2019-11-23 20:27:39 +08:00
[_mtlRenderEncoder setRenderPipelineState:_renderPipelineMTL->getMTLRenderPipelineState()];
}
void CommandBufferMTL::setViewport(int x, int y, unsigned int w, unsigned int h)
{
MTLViewport viewport;
viewport.originX = x;
viewport.originY = (int)(_renderTargetHeight - y - h);
viewport.width = w;
viewport.height = h;
viewport.znear = 0;
viewport.zfar = 1;
2019-11-23 20:27:39 +08:00
[_mtlRenderEncoder setViewport:viewport];
}
void CommandBufferMTL::setCullMode(CullMode mode)
{
[_mtlRenderEncoder setCullMode:toMTLCullMode(mode)];
}
void CommandBufferMTL::setWinding(Winding winding)
{
[_mtlRenderEncoder setFrontFacingWinding:toMTLWinding(winding)];
}
void CommandBufferMTL::setVertexBuffer(Buffer* buffer)
{
2023-08-13 00:24:35 +08:00
// Vertex buffer is bound in index 0.
[_mtlRenderEncoder setVertexBuffer:static_cast<BufferMTL*>(buffer)->getMTLBuffer() offset:0 atIndex:0];
2023-07-31 10:03:24 +08:00
}
2019-11-23 20:27:39 +08:00
void CommandBufferMTL::setProgramState(ProgramState* programState)
{
2022-07-16 10:43:05 +08:00
AX_SAFE_RETAIN(programState);
AX_SAFE_RELEASE(_programState);
2019-11-23 20:27:39 +08:00
_programState = programState;
}
void CommandBufferMTL::setIndexBuffer(Buffer* buffer)
{
assert(buffer != nullptr);
if (!buffer)
return;
2019-11-23 20:27:39 +08:00
_mtlIndexBuffer = static_cast<BufferMTL*>(buffer)->getMTLBuffer();
[_mtlIndexBuffer retain];
}
void CommandBufferMTL::drawArrays(PrimitiveType primitiveType, std::size_t start, std::size_t count, bool wireframe /* unused */)
2019-11-23 20:27:39 +08:00
{
prepareDrawing();
[_mtlRenderEncoder drawPrimitives:toMTLPrimitive(primitiveType) vertexStart:start vertexCount:count];
2019-11-23 20:27:39 +08:00
}
void CommandBufferMTL::drawElements(PrimitiveType primitiveType,
IndexFormat indexType,
std::size_t count,
std::size_t offset,
2023-08-13 00:24:35 +08:00
bool wireframe /* unused */)
2019-11-23 20:27:39 +08:00
{
prepareDrawing();
[_mtlRenderEncoder drawIndexedPrimitives:toMTLPrimitive(primitiveType)
indexCount:count
indexType:toMTLIndexType(indexType)
indexBuffer:_mtlIndexBuffer
indexBufferOffset:offset];
}
void CommandBufferMTL::endRenderPass()
{
afterDraw();
}
void CommandBufferMTL::readPixels(RenderTarget* rt, std::function<void(const PixelBufferDescriptor&)> callback)
2019-11-23 20:27:39 +08:00
{
auto rtMTL = static_cast<RenderTargetMTL*>(rt);
// we only read form color attachment 0
// if it's nullptr, will regard as screen to perform capture
auto texture = rtMTL->_color[0].texture;
2022-07-16 10:43:05 +08:00
AX_SAFE_RETAIN(texture);
2020-09-11 00:10:44 +08:00
_captureCallbacks.emplace_back(texture, std::move(callback));
2019-11-23 20:27:39 +08:00
}
void CommandBufferMTL::endFrame()
{
[_mtlRenderEncoder endEncoding];
[_mtlRenderEncoder release];
_mtlRenderEncoder = nil;
auto currentDrawable = DeviceMTL::getCurrentDrawable();
[_mtlCommandBuffer presentDrawable:currentDrawable];
_drawableTexture = currentDrawable.texture;
2019-11-23 20:27:39 +08:00
[_mtlCommandBuffer addCompletedHandler:^(id<MTLCommandBuffer> commandBuffer) {
// GPU work is complete
// Signal the semaphore to start the CPU work
dispatch_semaphore_signal(_frameBoundarySemaphore);
2019-11-23 20:27:39 +08:00
}];
2020-09-10 18:12:46 +08:00
flush();
2019-11-23 20:27:39 +08:00
DeviceMTL::resetCurrentDrawable();
[_autoReleasePool drain];
}
2022-11-01 16:02:13 +08:00
void CommandBufferMTL::endEncoding()
{
if (_mtlRenderEncoder) {
[_mtlRenderEncoder endEncoding];
[_mtlRenderEncoder release];
}
_mtlRenderEncoder = nil;
}
2020-09-10 18:12:46 +08:00
void CommandBufferMTL::flush()
{
if (_mtlCommandBuffer)
{
2020-09-10 18:12:46 +08:00
assert(_mtlCommandBuffer.status != MTLCommandBufferStatusCommitted);
[_mtlCommandBuffer commit];
2020-09-11 00:10:44 +08:00
flushCaptureCommands();
2020-09-10 18:12:46 +08:00
[_mtlCommandBuffer release];
_mtlCommandBuffer = nil;
}
}
2020-09-11 00:10:44 +08:00
void CommandBufferMTL::flushCaptureCommands()
{
if (!_captureCallbacks.empty())
{
// !!!important, if have capture request, must wait pending commandBuffer finish at this frame,
// because readPixels require sync operation to get screen pixels properly without data race issue,
// otherwise, will lead dead-lock
// !!!Notes, MTL is mutli-threading, all GPU handler is dispatch at GPU threads
2020-09-11 00:10:44 +08:00
[_mtlCommandBuffer waitUntilCompleted];
2020-09-11 00:10:44 +08:00
PixelBufferDescriptor screenPixelData;
for (auto& cb : _captureCallbacks)
{
if (cb.first == nil)
{ // screen capture
if (!screenPixelData)
{
CommandBufferMTL::readPixels(_drawableTexture, 0, 0, [_drawableTexture width],
[_drawableTexture height], screenPixelData);
// screen framebuffer copied, restore screen framebuffer only to true
backend::Device::getInstance()->setFrameBufferOnly(true);
2020-09-11 00:10:44 +08:00
}
cb.second(screenPixelData);
}
else
{
2020-09-11 00:10:44 +08:00
PixelBufferDescriptor pixelData;
auto texture = cb.first;
assert(texture != nullptr);
CommandBufferMTL::readPixels(texture, 0, 0, texture->getWidth(), texture->getHeight(), pixelData);
2022-07-16 10:43:05 +08:00
AX_SAFE_RELEASE(texture);
2020-09-11 00:10:44 +08:00
cb.second(pixelData);
}
}
_captureCallbacks.clear();
}
}
2019-11-23 20:27:39 +08:00
void CommandBufferMTL::afterDraw()
{
if (_mtlIndexBuffer)
{
[_mtlIndexBuffer release];
_mtlIndexBuffer = nullptr;
}
2022-07-16 10:43:05 +08:00
AX_SAFE_RELEASE_NULL(_programState);
2019-11-23 20:27:39 +08:00
}
void CommandBufferMTL::prepareDrawing() const
{
setUniformBuffer();
setTextures();
auto mtlDepthStencilState = _depthStencilStateMTL->getMTLDepthStencilState();
if (mtlDepthStencilState)
2019-11-23 20:27:39 +08:00
{
[_mtlRenderEncoder setDepthStencilState:mtlDepthStencilState];
2019-11-23 20:27:39 +08:00
[_mtlRenderEncoder setStencilFrontReferenceValue:_stencilReferenceValueFront
backReferenceValue:_stencilReferenceValueBack];
}
}
void CommandBufferMTL::setTextures() const
{
if (_programState)
{
doSetTextures(true);
doSetTextures(false);
}
}
void CommandBufferMTL::doSetTextures(bool isVertex) const
{
const auto& bindTextureInfos =
(isVertex) ? _programState->getVertexTextureInfos() : _programState->getFragmentTextureInfos();
2019-11-23 20:27:39 +08:00
for (const auto& iter : bindTextureInfos)
2019-11-23 20:27:39 +08:00
{
/* About mutli textures support
* a. TODO: sampler2DArray, not implemented in Metal Renderer currently
* b. texture slot, one BackendTexture, multi GPU texture handlers, used by etc1, restrict: textures must have
* same size c. Bind multi BackendTexture to 1 Shader Program, see the ShaderTest d. iter.second.slots not used
* for Metal Renderer
*/
auto location = iter.first;
2020-08-29 16:56:48 +08:00
auto& textures = iter.second.textures;
auto& indexs = iter.second.indexs;
2020-08-29 19:51:42 +08:00
auto texture = textures[0];
auto index = indexs[0];
2020-08-29 19:51:42 +08:00
if (isVertex)
{
[_mtlRenderEncoder setVertexTexture:getMTLTexture(texture, index) atIndex:location];
[_mtlRenderEncoder setVertexSamplerState:getMTLSamplerState(texture) atIndex:location];
2020-08-29 19:51:42 +08:00
}
else
{
[_mtlRenderEncoder setFragmentTexture:getMTLTexture(texture, index) atIndex:location];
[_mtlRenderEncoder setFragmentSamplerState:getMTLSamplerState(texture) atIndex:location];
}
2019-11-23 20:27:39 +08:00
}
}
void CommandBufferMTL::setUniformBuffer() const
{
if (_programState)
{
auto& callbackUniforms = _programState->getCallbackUniforms();
for (auto& cb : callbackUniforms)
cb.second(_programState, cb.first);
2023-08-13 00:24:35 +08:00
// Uniform buffer is bound to index 1.
2019-11-23 20:27:39 +08:00
std::size_t bufferSize = 0;
2023-08-13 00:24:35 +08:00
char* vertexBuffer = nullptr;
_programState->getVertexUniformBuffer(&vertexBuffer, bufferSize);
if (vertexBuffer)
2019-11-23 20:27:39 +08:00
{
2023-08-13 00:24:35 +08:00
[_mtlRenderEncoder setVertexBytes:vertexBuffer length:bufferSize atIndex:1];
2019-11-23 20:27:39 +08:00
}
2023-08-13 00:24:35 +08:00
char* fragmentBuffer = nullptr;
_programState->getFragmentUniformBuffer(&fragmentBuffer, bufferSize);
if (fragmentBuffer)
2019-11-23 20:27:39 +08:00
{
2023-08-13 00:24:35 +08:00
[_mtlRenderEncoder setFragmentBytes:fragmentBuffer length:bufferSize atIndex:1];
2019-11-23 20:27:39 +08:00
}
}
}
void CommandBufferMTL::setLineWidth(float lineWidth) {}
2019-11-23 20:27:39 +08:00
void CommandBufferMTL::setScissorRect(bool isEnabled, float x, float y, float width, float height)
{
MTLScissorRect scissorRect;
if (isEnabled)
2019-11-23 20:27:39 +08:00
{
y = _renderTargetHeight - height - y;
int minX = clamp((int)x, 0, (int)_renderTargetWidth);
int minY = clamp((int)y, 0, (int)_renderTargetHeight);
int maxX = clamp((int)(x + width), 0, (int)_renderTargetWidth);
int maxY = clamp((int)(y + height), 0, (int)_renderTargetHeight);
scissorRect.x = minX;
scissorRect.y = minY;
scissorRect.width = maxX - minX;
2019-11-23 20:27:39 +08:00
scissorRect.height = maxY - minY;
if (scissorRect.width == 0 || scissorRect.height == 0)
{
scissorRect.width = 0;
2019-11-23 20:27:39 +08:00
scissorRect.height = 0;
}
}
else
{
scissorRect.x = 0;
scissorRect.y = 0;
scissorRect.width = _renderTargetWidth;
2019-11-23 20:27:39 +08:00
scissorRect.height = _renderTargetHeight;
}
[_mtlRenderEncoder setScissorRect:scissorRect];
}
void CommandBufferMTL::readPixels(TextureBackend* texture,
std::size_t origX,
std::size_t origY,
std::size_t rectWidth,
std::size_t rectHeight,
PixelBufferDescriptor& pbd)
{
CommandBufferMTL::readPixels(reinterpret_cast<id<MTLTexture>>(texture->getHandler()), origX, origY, rectWidth,
rectHeight, pbd);
}
void CommandBufferMTL::readPixels(id<MTLTexture> texture,
std::size_t origX,
std::size_t origY,
std::size_t rectWidth,
std::size_t rectHeight,
PixelBufferDescriptor& pbd)
{
NSUInteger texWidth = texture.width;
NSUInteger texHeight = texture.height;
MTLRegion region = MTLRegionMake2D(0, 0, texWidth, texHeight);
MTLRegion imageRegion = MTLRegionMake2D(origX, origY, rectWidth, rectHeight);
MTLTextureDescriptor* textureDescriptor =
[MTLTextureDescriptor texture2DDescriptorWithPixelFormat:[texture pixelFormat]
width:texWidth
height:texHeight
mipmapped:NO];
id<MTLDevice> device = static_cast<DeviceMTL*>(DeviceMTL::getInstance())->getMTLDevice();
id<MTLTexture> readPixelsTexture = [device newTextureWithDescriptor:textureDescriptor];
id<MTLCommandQueue> commandQueue = static_cast<DeviceMTL*>(DeviceMTL::getInstance())->getMTLCommandQueue();
auto commandBuffer = [commandQueue commandBuffer];
// [commandBuffer enqueue];
id<MTLBlitCommandEncoder> blitCommandEncoder = [commandBuffer blitCommandEncoder];
[blitCommandEncoder copyFromTexture:texture
sourceSlice:0
sourceLevel:0
sourceOrigin:region.origin
sourceSize:region.size
toTexture:readPixelsTexture
destinationSlice:0
destinationLevel:0
destinationOrigin:region.origin];
2022-07-16 10:43:05 +08:00
#if (AX_TARGET_PLATFORM == AX_PLATFORM_MAC)
[blitCommandEncoder synchronizeResource:readPixelsTexture];
#endif
[blitCommandEncoder endEncoding];
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> commandBufferMTL) {
auto bytePerRow = rectWidth * getBitsPerElementMTL(texture.pixelFormat) / 8;
auto texelData = pbd._data.resize(bytePerRow * rectHeight);
if (texelData != nullptr)
{
[readPixelsTexture getBytes:texelData bytesPerRow:bytePerRow fromRegion:imageRegion mipmapLevel:0];
UtilsMTL::swizzleImage(texelData, rectWidth, rectHeight, readPixelsTexture.pixelFormat);
pbd._width = static_cast<int>(rectWidth);
pbd._height = static_cast<int>(rectHeight);
}
[readPixelsTexture release];
}];
[commandBuffer commit];
[commandBuffer waitUntilCompleted];
}
2019-11-23 20:27:39 +08:00
NS_AX_BACKEND_END