| /* |
| * MVKWatermark.mm |
| * |
| * Copyright (c) 2015-2022 The Brenwill Workshop Ltd. (http://www.brenwill.com) |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| |
| #include "MVKWatermark.h" |
| #include "MVKOSExtensions.h" |
| #include "MTLTextureDescriptor+MoltenVK.h" |
| #include "MVKEnvironment.h" |
| |
| |
| /** The structure to hold shader uniforms. */ |
| typedef struct { |
| float mvpMtx[16]; |
| MVKWatermarkColor color; |
| } MVKWatermarkUniforms; |
| |
| |
| #define kMVKWatermarkUniformBufferIndex 0 |
| #define kMVKWatermarkVertexUniformBufferLength (sizeof(MVKWatermarkUniforms)) |
| |
| #define kMVKWatermarkVertexContentBufferIndex 1 |
| #define kMVKWatermarkVertexContentBufferLength (sizeof(float) * 4 * 4) |
| #define kMVKWatermarkVertexIndexBufferLength (sizeof(uint16_t) * 6) |
| |
| #define kMVKWatermarkTextureIndex 0 |
| |
| |
| #pragma mark - |
| #pragma mark MVKWatermark |
| |
| void MVKWatermark::setPosition(MVKWatermarkPosition position) { |
| if ( (position.x == _position.x) && (position.y == _position.y) ) { return; } |
| _position = position; |
| markUniformsDirty(); |
| } |
| |
| void MVKWatermark::setSize(MVKWatermarkSize size) { |
| if ( (size.width == _size.width) && (size.height == _size.height) ) { return; } |
| _size = size; |
| markUniformsDirty(); |
| } |
| |
| void MVKWatermark::setOpacity(float opacity) { |
| if (opacity == _color.a) { return; } |
| _color.a = opacity; |
| markUniformsDirty(); |
| } |
| |
| void MVKWatermark::markUniformsDirty() { _isUniformsDirty = true; } |
| |
| void MVKWatermark::markRenderPipelineStateDirty() { |
| [_mtlRenderPipelineState release]; |
| _mtlRenderPipelineState = nil; |
| } |
| |
| id<MTLRenderPipelineState> MVKWatermark::mtlRenderPipelineState() { |
| if ( !_mtlRenderPipelineState ) { _mtlRenderPipelineState = newRenderPipelineState(); } // retained |
| return _mtlRenderPipelineState; |
| } |
| |
| id<MTLRenderPipelineState> MVKWatermark::newRenderPipelineState() { |
| MTLRenderPipelineDescriptor* plDesc = [MTLRenderPipelineDescriptor new]; // temp retained |
| plDesc.label = _mtlName; |
| |
| plDesc.vertexFunction = _mtlFunctionVertex; |
| plDesc.fragmentFunction = _mtlFunctionFragment; |
| |
| plDesc.depthAttachmentPixelFormat = _mtlDepthFormat; |
| plDesc.stencilAttachmentPixelFormat = _mtlStencilFormat; |
| plDesc.sampleCount = _sampleCount; |
| plDesc.rasterizationEnabled = true; |
| |
| MTLRenderPipelineColorAttachmentDescriptor* colorDesc = plDesc.colorAttachments[0]; |
| colorDesc.pixelFormat = _mtlColorFormat; |
| colorDesc.writeMask = MTLColorWriteMaskAll; |
| colorDesc.blendingEnabled = true; |
| colorDesc.rgbBlendOperation = MTLBlendOperationAdd; |
| colorDesc.alphaBlendOperation = MTLBlendOperationMax; |
| colorDesc.sourceRGBBlendFactor = MTLBlendFactorSourceAlpha; |
| colorDesc.destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha; |
| colorDesc.sourceAlphaBlendFactor = MTLBlendFactorOne; |
| colorDesc.destinationAlphaBlendFactor = MTLBlendFactorZero; |
| |
| MTLVertexDescriptor* vtxDesc = plDesc.vertexDescriptor; |
| |
| // Vertex attribute descriptors |
| MTLVertexAttributeDescriptorArray* vaDescArray = vtxDesc.attributes; |
| MTLVertexAttributeDescriptor* vaDesc; |
| NSUInteger vtxStride = 0; |
| |
| // Vertex location |
| vaDesc = vaDescArray[0]; |
| vaDesc.format = MTLVertexFormatFloat2; |
| vaDesc.bufferIndex = kMVKWatermarkVertexContentBufferIndex; |
| vaDesc.offset = vtxStride; |
| vtxStride += sizeof(float) * 2; |
| |
| // Vertex texture coords |
| vaDesc = vaDescArray[1]; |
| vaDesc.format = MTLVertexFormatFloat2; |
| vaDesc.bufferIndex = kMVKWatermarkVertexContentBufferIndex; |
| vaDesc.offset = vtxStride; |
| vtxStride += sizeof(float) * 2; |
| |
| // Vertex attribute buffer. |
| MTLVertexBufferLayoutDescriptorArray* vbDescArray = vtxDesc.layouts; |
| MTLVertexBufferLayoutDescriptor* vbDesc = vbDescArray[kMVKWatermarkVertexContentBufferIndex]; |
| vbDesc.stepFunction = MTLVertexStepFunctionPerVertex; |
| vbDesc.stepRate = 1; |
| vbDesc.stride = vtxStride; |
| |
| NSError* err = nil; |
| id<MTLRenderPipelineState> rps = [_mtlDevice newRenderPipelineStateWithDescriptor: plDesc error: &err]; // retained |
| MVKAssert( !err, "Could not create watermark pipeline state (Error code %li)\n%s", (long)err.code, err.localizedDescription.UTF8String); |
| [plDesc release]; // temp released |
| return rps; |
| } |
| |
| |
| #pragma mark Rendering |
| |
| void MVKWatermark::render(id<MTLTexture> mtlTexture, id<MTLCommandBuffer> mtlCommandBuffer, double frameInterval) { |
| |
| updateRenderState(mtlTexture); |
| |
| MTLRenderPassDescriptor* mtlRPDesc = getMTLRenderPassDescriptor(); |
| MTLRenderPassColorAttachmentDescriptor* mtlColorAttDesc = mtlRPDesc.colorAttachments[0]; |
| mtlColorAttDesc.texture = mtlTexture; |
| |
| id<MTLRenderCommandEncoder> mtlRendEnc = [mtlCommandBuffer renderCommandEncoderWithDescriptor: mtlRPDesc]; |
| mtlRendEnc.label = _mtlRendEncName; |
| render(mtlRendEnc, frameInterval); |
| [mtlRendEnc endEncoding]; |
| } |
| |
| void MVKWatermark::updateRenderState(id<MTLTexture> mtlTexture) { |
| |
| MTLPixelFormat mtlColorFormat = mtlTexture.pixelFormat; |
| if (_mtlColorFormat != mtlColorFormat) { |
| _mtlColorFormat = mtlColorFormat; |
| markRenderPipelineStateDirty(); |
| } |
| |
| MTLPixelFormat mtlDepthFormat = MTLPixelFormatInvalid; |
| if (_mtlDepthFormat != mtlDepthFormat) { |
| _mtlDepthFormat = mtlDepthFormat; |
| markRenderPipelineStateDirty(); |
| } |
| |
| MTLPixelFormat mtlStencilFormat = MTLPixelFormatInvalid; |
| if (_mtlStencilFormat != mtlStencilFormat) { |
| _mtlStencilFormat = mtlStencilFormat; |
| markRenderPipelineStateDirty(); |
| } |
| |
| NSUInteger sampleCount = mtlTexture.sampleCount; |
| if (_sampleCount != sampleCount) { |
| _sampleCount = sampleCount; |
| markRenderPipelineStateDirty(); |
| } |
| } |
| |
| void MVKWatermark::render(id<MTLRenderCommandEncoder> mtlEncoder, double frameInterval) { |
| |
| updateUniforms(); |
| |
| [mtlEncoder pushDebugGroup: _mtlName]; |
| |
| [mtlEncoder setRenderPipelineState: mtlRenderPipelineState()]; |
| [mtlEncoder setCullMode: MTLCullModeBack]; |
| [mtlEncoder setVertexBuffer: _mtlVertexContentBuffer offset: 0 atIndex: kMVKWatermarkVertexContentBufferIndex]; |
| [mtlEncoder setVertexBuffer: _mtlVertexUniformBuffer offset: 0 atIndex: kMVKWatermarkUniformBufferIndex]; |
| [mtlEncoder setFragmentTexture: _mtlTexture atIndex: kMVKWatermarkTextureIndex]; |
| [mtlEncoder setFragmentSamplerState: _mtlSamplerState atIndex: kMVKWatermarkTextureIndex]; |
| |
| [mtlEncoder drawIndexedPrimitives: MTLPrimitiveTypeTriangle |
| indexCount: 6 |
| indexType: MTLIndexTypeUInt16 |
| indexBuffer: _mtlVertexIndexBuffer |
| indexBufferOffset: 0]; |
| |
| [mtlEncoder popDebugGroup]; |
| } |
| |
| /** Updates the shader uniforms structure from the properties of this instance. */ |
| void MVKWatermark::updateUniforms() { |
| if ( !_isUniformsDirty ) { return; } |
| |
| MVKWatermarkUniforms* pUniforms = (MVKWatermarkUniforms*)_mtlVertexUniformBuffer.contents; |
| |
| // Populate the MVP matrix uniform |
| // The matrix is specified in clip-space coordinates (-1.0 < v < 1.0 for each axis). |
| float* mvpMtx = (float*)&(pUniforms->mvpMtx); |
| mvpMtx[0] = _size.width; |
| mvpMtx[1] = 0.0; |
| mvpMtx[2] = 0.0; |
| mvpMtx[3] = 0.0; |
| |
| mvpMtx[4] = 0.0; |
| mvpMtx[5] = _size.height; |
| mvpMtx[6] = 0.0; |
| mvpMtx[7] = 0.0; |
| |
| mvpMtx[8] = 0.0; |
| mvpMtx[9] = 0.0; |
| mvpMtx[10] = 1.0; |
| mvpMtx[11] = 0.0; |
| |
| mvpMtx[12] = _position.x; |
| mvpMtx[13] = _position.y; |
| mvpMtx[14] = 0.0; |
| mvpMtx[15] = 1.0; |
| |
| // Populate the opacity uniform |
| pUniforms->color = _color; |
| |
| _isUniformsDirty = false; |
| } |
| |
| // Returns a MTLRenderPassDescriptor that can be used to render this watermark. |
| MTLRenderPassDescriptor* MVKWatermark::getMTLRenderPassDescriptor() { |
| if ( !_mtlRenderPassDescriptor ) { |
| _mtlRenderPassDescriptor = [[MTLRenderPassDescriptor renderPassDescriptor] retain]; // retained |
| MTLRenderPassColorAttachmentDescriptor* mtlColorAttDesc = _mtlRenderPassDescriptor.colorAttachments[0]; |
| mtlColorAttDesc.loadAction = MTLLoadActionLoad; |
| mtlColorAttDesc.storeAction = MTLStoreActionStore; |
| } |
| return _mtlRenderPassDescriptor; |
| } |
| |
| |
| #pragma mark Instance creation |
| |
| MVKWatermark::MVKWatermark(id<MTLDevice> mtlDevice, |
| unsigned char* textureContent, |
| uint32_t textureWidth, |
| uint32_t textureHeight, |
| MTLPixelFormat textureFormat, |
| NSUInteger textureBytesPerRow, |
| const char* mslSourceCode) : _position(0, 0), _size(1, 1), _color(1, 1, 1, 0.25) { |
| _mtlColorFormat = MTLPixelFormatInvalid; |
| _mtlDepthFormat = MTLPixelFormatInvalid; |
| _mtlStencilFormat = MTLPixelFormatInvalid; |
| _sampleCount = 1; |
| _mtlName = [@"License Watermark" retain]; // retained |
| _mtlRendEncName = [@"License Watermark RenderEncoder" retain]; // retained |
| _isUniformsDirty = true; |
| |
| _mtlDevice = [mtlDevice retain]; // retained |
| initTexture(textureContent, textureWidth, textureHeight, textureFormat, textureBytesPerRow); |
| initShaders(mslSourceCode); |
| initBuffers(); |
| _mtlRenderPipelineState = nil; |
| _mtlRenderPassDescriptor = nil; |
| } |
| |
| // Initialize the texture to use for rendering the watermark |
| void MVKWatermark::initTexture(unsigned char* textureContent, |
| uint32_t textureWidth, |
| uint32_t textureHeight, |
| MTLPixelFormat textureFormat, |
| NSUInteger textureBytesPerRow) { |
| |
| MTLTextureDescriptor* texDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat: textureFormat |
| width: textureWidth |
| height: textureHeight |
| mipmapped: NO]; |
| texDesc.usageMVK = MTLTextureUsageShaderRead; |
| #if MVK_IOS |
| texDesc.storageModeMVK = MTLStorageModeShared; |
| #endif |
| #if MVK_MACOS |
| texDesc.storageModeMVK = MTLStorageModeManaged; |
| #endif |
| |
| _mtlTexture = [_mtlDevice newTextureWithDescriptor: texDesc]; // retained |
| [_mtlTexture replaceRegion: MTLRegionMake2D(0, 0, textureWidth, textureHeight) |
| mipmapLevel: 0 |
| slice: 0 |
| withBytes: textureContent |
| bytesPerRow: textureBytesPerRow |
| bytesPerImage: 0]; |
| |
| MTLSamplerDescriptor* sampDesc = [MTLSamplerDescriptor new]; // temp retained |
| sampDesc.minFilter = MTLSamplerMinMagFilterLinear; |
| _mtlSamplerState = [_mtlDevice newSamplerStateWithDescriptor: sampDesc]; // retained |
| [sampDesc release]; // temp released |
| } |
| |
| // Initialize the shader functions for rendering the watermark |
| void MVKWatermark::initShaders(const char* mslSourceCode) { |
| NSError* err = nil; |
| NSString* nsSrc = [[NSString alloc] initWithUTF8String: mslSourceCode]; // temp retained |
| id<MTLLibrary> mtlLib = [_mtlDevice newLibraryWithSource: nsSrc |
| options: nil |
| error: &err]; // temp retained |
| MVKAssert( !err, "Could not compile watermark shaders (Error code %li):\n%s", (long)err.code, err.localizedDescription.UTF8String); |
| |
| _mtlFunctionVertex = [mtlLib newFunctionWithName: @"watermarkVertex"]; // retained |
| _mtlFunctionFragment = [mtlLib newFunctionWithName: @"watermarkFragment"]; // retained |
| |
| [nsSrc release]; // temp released |
| [mtlLib release]; // temp released |
| } |
| |
| // Initialize the vertex buffers to use for rendering the watermark |
| void MVKWatermark::initBuffers() { |
| _mtlVertexUniformBuffer = [_mtlDevice newBufferWithLength: kMVKWatermarkVertexUniformBufferLength |
| options: MTLResourceOptionCPUCacheModeDefault]; // retained |
| |
| _mtlVertexContentBuffer = [_mtlDevice newBufferWithLength: kMVKWatermarkVertexContentBufferLength |
| options: MTLResourceOptionCPUCacheModeDefault]; // retained |
| |
| float* vtxContents = (float*)_mtlVertexContentBuffer.contents; |
| uint32_t idx = 0; |
| |
| // Bottom left |
| vtxContents[idx++] = -1.0; // Location X |
| vtxContents[idx++] = -1.0; // Location Y |
| vtxContents[idx++] = 0.0; // TexCoord X |
| vtxContents[idx++] = 1.0; // TexCoord Y |
| |
| // Bottom right |
| vtxContents[idx++] = 1.0; // Location X |
| vtxContents[idx++] = -1.0; // Location Y |
| vtxContents[idx++] = 1.0; // TexCoord X |
| vtxContents[idx++] = 1.0; // TexCoord Y |
| |
| // Top left |
| vtxContents[idx++] = -1.0; // Location X |
| vtxContents[idx++] = 1.0; // Location Y |
| vtxContents[idx++] = 0.0; // TexCoord X |
| vtxContents[idx++] = 0.0; // TexCoord Y |
| |
| // Top right |
| vtxContents[idx++] = 1.0; // Location X |
| vtxContents[idx++] = 1.0; // Location Y |
| vtxContents[idx++] = 1.0; // TexCoord X |
| vtxContents[idx++] = 0.0; // TexCoord Y |
| |
| _mtlVertexIndexBuffer = [_mtlDevice newBufferWithLength: kMVKWatermarkVertexIndexBufferLength |
| options: MTLResourceOptionCPUCacheModeDefault]; // retained |
| uint16_t* vtxIndices = (uint16_t*)_mtlVertexIndexBuffer.contents; |
| idx = 0; |
| vtxIndices[idx++] = 0; // First face |
| vtxIndices[idx++] = 2; |
| vtxIndices[idx++] = 3; |
| |
| vtxIndices[idx++] = 3; // Second face |
| vtxIndices[idx++] = 1; |
| vtxIndices[idx++] = 0; |
| } |
| |
| MVKWatermark::~MVKWatermark() { |
| [_mtlName release]; |
| [_mtlRendEncName release]; |
| [_mtlDevice release]; |
| [_mtlTexture release]; |
| [_mtlSamplerState release]; |
| [_mtlFunctionVertex release]; |
| [_mtlFunctionFragment release]; |
| [_mtlRenderPipelineState release]; |
| [_mtlVertexContentBuffer release]; |
| [_mtlVertexIndexBuffer release]; |
| [_mtlVertexUniformBuffer release]; |
| [_mtlRenderPassDescriptor release]; |
| } |
| |
| |
| #pragma mark - |
| #pragma mark MVKWatermarkRandom |
| |
| static inline uint32_t randomUInt() { return arc4random(); } |
| static inline uint32_t randomUIntBelow(uint32_t max) { return randomUInt() % max; } |
| static inline float randomFloat() { return (float)randomUInt() / (float)(1LL << 32); } |
| static inline double randomFloatBetween(float min, float max) { return min + (randomFloat() * (max - min)); } |
| |
| |
| void MVKWatermarkRandom::updateRenderState(id<MTLTexture> mtlTexture) { |
| |
| MVKWatermark::updateRenderState(mtlTexture); |
| |
| // Calculate the size of the watermark as a portion of the size of the framebuffer. |
| // The watermark is displayed in clip-space coordinates, with the coordinate origin |
| // at the center of the framebuffer, and positive coordinates to the right and up |
| // and negative coordinates to the left and down. |
| float sideLen = _scale; |
| double renderAspect = (double)mtlTexture.width / (double)mtlTexture.height; |
| sideLen = MIN(sideLen, sideLen * renderAspect); |
| setSize(MVKWatermarkSize(sideLen / renderAspect, sideLen)); |
| } |
| |
| void MVKWatermarkRandom::render(id<MTLRenderCommandEncoder> mtlEncoder, double frameInterval) { |
| |
| // Determine the opacity |
| float opacity = _color.a + (_opacityVelocity * frameInterval); |
| BOOL isFadedOut = (opacity < _minOpacity); |
| if (opacity < _minOpacity) { |
| opacity = _minOpacity; |
| _opacityVelocity = ABS(_opacityVelocity); |
| } |
| if (opacity > _maxOpacity) { |
| opacity = _maxOpacity; |
| _opacityVelocity = -ABS(_opacityVelocity); |
| } |
| setOpacity(opacity); |
| |
| // Determine the position in clip-space coordinates. |
| MVKWatermarkPosition newPos = _position; |
| switch (_positionMode) { |
| case kMVKWatermarkPositionModeTeleport: { |
| if (isFadedOut) { |
| // Move to a new position somewhere on the screen before fading back in |
| newPos.x = randomFloatBetween(-_maxPosition, _maxPosition); |
| newPos.y = randomFloatBetween(-_maxPosition, _maxPosition); |
| } |
| break; |
| } |
| case kMVKWatermarkPositionModeBounce: { |
| // Bounce around the screen, always staying with the screen bounds |
| newPos.x = _position.x + (_positionVelocity.x * frameInterval); |
| newPos.y = _position.y + (_positionVelocity.y * frameInterval); |
| |
| if (newPos.x < -_maxPosition) { |
| newPos.x = -_maxPosition; |
| _positionVelocity.x = ABS(_positionVelocity.x); |
| } |
| if (newPos.x > _maxPosition) { |
| newPos.x = _maxPosition; |
| _positionVelocity.x = -ABS(_positionVelocity.x); |
| } |
| if (newPos.y < -_maxPosition) { |
| newPos.y = -_maxPosition; |
| _positionVelocity.y = ABS(_positionVelocity.y); |
| } |
| if (newPos.y > _maxPosition) { |
| newPos.y = _maxPosition; |
| _positionVelocity.y = -ABS(_positionVelocity.y); |
| } |
| break; |
| } |
| } |
| setPosition(newPos); |
| |
| MVKWatermark::render(mtlEncoder, frameInterval); |
| } |
| |
| MVKWatermarkRandom::MVKWatermarkRandom(id<MTLDevice> mtlDevice, |
| unsigned char* textureContent, |
| uint32_t textureWidth, |
| uint32_t textureHeight, |
| MTLPixelFormat textureFormat, |
| NSUInteger textureBytesPerRow, |
| const char* mslSourceCode) : MVKWatermark(mtlDevice, |
| textureContent, |
| textureWidth, |
| textureHeight, |
| textureFormat, |
| textureBytesPerRow, |
| mslSourceCode), _positionVelocity(0, 0) { |
| // Randomly select a position movement mode, but favour bounce mode |
| _positionMode = ( (randomUIntBelow(3) == kMVKWatermarkPositionModeTeleport) |
| ? kMVKWatermarkPositionModeTeleport |
| : kMVKWatermarkPositionModeBounce); |
| switch (_positionMode) { |
| case kMVKWatermarkPositionModeBounce: |
| _minOpacity = 0.25; |
| _maxOpacity = 0.75; |
| break; |
| case kMVKWatermarkPositionModeTeleport: |
| _minOpacity = 0.0; |
| _maxOpacity = 0.75; |
| break; |
| } |
| _opacityVelocity = (_maxOpacity - _minOpacity) / 2.5; |
| setOpacity(_minOpacity); |
| |
| _scale = 0.2; |
| _maxPosition = 1.0 - _scale; |
| _positionVelocity = MVKWatermarkPosition(_maxPosition / 3.0, _maxPosition / 4.0); |
| setPosition(MVKWatermarkPosition(randomFloatBetween(-_maxPosition, _maxPosition), |
| randomFloatBetween(-_maxPosition, _maxPosition))); |
| } |
| |