mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-02 04:21:41 +01:00
- Metal: when a layer has no drawable to give (it can take a second to say so), don't ask again in the same frame for every pass to it. - Swapchains other than the primary are created, resized and destroyed between frames (Metal asserts it), and destroyed with the last reference to them (ReleaseAssertLast, Metal and Vulkan). - BindFramebufferSamplesAsTexture asserts it isn't given a swapchain, like BindFramebufferAsTexture. - An offscreen Metal swapchain can't be resized, asserted. - VulkanRenderManager asserts that no additional swapchain outlives it. - Native_RemoveOutputAndWait asserts it isn't called on the thread that handles the requests, where it would always time out. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1365 lines
47 KiB
Plaintext
1365 lines
47 KiB
Plaintext
// Copyright (c) 2012- PPSSPP Project.
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, version 2.0 or later versions.
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License 2.0 for more details.
|
|
|
|
// A copy of the GPL 2.0 should have been included with the program.
|
|
// If not, see http://www.gnu.org/licenses/
|
|
|
|
// Official git repository and contact information can be found at
|
|
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
|
|
|
#include "ppsspp_config.h"
|
|
|
|
#include <algorithm>
|
|
#include <objc/runtime.h>
|
|
|
|
#include "Common/GPU/Metal/MetalRenderManager.h"
|
|
#include "Common/GPU/Vulkan/VulkanLoader.h"
|
|
#include "Common/GPU/Vulkan/VulkanContext.h"
|
|
#include "Common/Log.h"
|
|
#include "Common/StringUtils.h"
|
|
#include "Common/Thread/ThreadUtil.h"
|
|
#include "Common/Thread/ThreadManager.h"
|
|
|
|
#include "ext/SPIRV-Cross/spirv_msl.hpp"
|
|
|
|
void MetalPushBuffer::Init(id<MTLDevice> device, size_t blockSize, const char *tag) {
|
|
device_ = device;
|
|
blockSize_ = blockSize;
|
|
tag_ = tag;
|
|
curBlock_ = 0;
|
|
offset_ = 0;
|
|
}
|
|
|
|
void MetalPushBuffer::Destroy() {
|
|
blocks_.clear();
|
|
device_ = nil;
|
|
}
|
|
|
|
void MetalPushBuffer::Reset() {
|
|
// Keep a single block around if we grew, merge the rest away to save memory.
|
|
if (blocks_.size() > 1) {
|
|
size_t total = TotalSize();
|
|
blocks_.clear();
|
|
blockSize_ = std::max(blockSize_, total);
|
|
}
|
|
curBlock_ = 0;
|
|
offset_ = 0;
|
|
}
|
|
|
|
size_t MetalPushBuffer::TotalSize() const {
|
|
size_t total = 0;
|
|
for (const Block &block : blocks_) {
|
|
total += block.size;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
MetalPushAlloc MetalPushBuffer::Allocate(size_t size, size_t align) {
|
|
// The vertex decoder can write a bit past the end of its output (see VertexDecoder::DecodeVerts), so keep
|
|
// some room after every allocation, like the Vulkan backend's push pool does.
|
|
const size_t slack = 256;
|
|
while (true) {
|
|
if (curBlock_ < (int)blocks_.size()) {
|
|
Block &block = blocks_[curBlock_];
|
|
size_t offset = (offset_ + align - 1) & ~(align - 1);
|
|
if (offset + size + slack <= block.size) {
|
|
offset_ = offset + size;
|
|
MetalPushAlloc alloc;
|
|
alloc.buffer = block.buffer;
|
|
alloc.offset = (uint32_t)offset;
|
|
alloc.ptr = block.ptr + offset;
|
|
return alloc;
|
|
}
|
|
curBlock_++;
|
|
offset_ = 0;
|
|
continue;
|
|
}
|
|
Block block;
|
|
block.size = std::max(blockSize_, size + slack);
|
|
block.buffer = [device_ newBufferWithLength:block.size options:MTLResourceStorageModeShared];
|
|
_assert_msg_(block.buffer, "Failed to allocate push buffer block of %d bytes", (int)block.size);
|
|
block.buffer.label = [NSString stringWithUTF8String:tag_.c_str()];
|
|
block.ptr = (uint8_t *)block.buffer.contents;
|
|
blocks_.push_back(block);
|
|
curBlock_ = (int)blocks_.size() - 1;
|
|
offset_ = 0;
|
|
}
|
|
}
|
|
|
|
const char *MetalPixelFormatToString(MTLPixelFormat fmt) {
|
|
switch (fmt) {
|
|
case MTLPixelFormatInvalid: return "Invalid";
|
|
case MTLPixelFormatRGBA8Unorm: return "RGBA8Unorm";
|
|
case MTLPixelFormatBGRA8Unorm: return "BGRA8Unorm";
|
|
case MTLPixelFormatR8Unorm: return "R8Unorm";
|
|
case MTLPixelFormatDepth32Float: return "Depth32Float";
|
|
case MTLPixelFormatDepth32Float_Stencil8: return "Depth32Float_Stencil8";
|
|
case MTLPixelFormatStencil8: return "Stencil8";
|
|
default: return "(other)";
|
|
}
|
|
}
|
|
|
|
bool TranslateVulkanGLSLToMSL(ShaderStage stage, const char *glsl, MetalTranslatedShader *out, std::string *errorMessage) {
|
|
VkShaderStageFlagBits vkStage;
|
|
spv::ExecutionModel model;
|
|
switch (stage) {
|
|
case ShaderStage::Vertex: vkStage = VK_SHADER_STAGE_VERTEX_BIT; model = spv::ExecutionModelVertex; break;
|
|
case ShaderStage::Fragment: vkStage = VK_SHADER_STAGE_FRAGMENT_BIT; model = spv::ExecutionModelFragment; break;
|
|
case ShaderStage::Compute: vkStage = VK_SHADER_STAGE_COMPUTE_BIT; model = spv::ExecutionModelGLCompute; break;
|
|
default: return false;
|
|
}
|
|
|
|
std::vector<uint32_t> spirv;
|
|
if (!GLSLtoSPV(vkStage, glsl, GLSLVariant::VULKAN, spirv, errorMessage)) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
spirv_cross::CompilerMSL msl(std::move(spirv));
|
|
spirv_cross::CompilerMSL::Options options = msl.get_msl_options();
|
|
#if PPSSPP_PLATFORM(IOS)
|
|
options.platform = spirv_cross::CompilerMSL::Options::iOS;
|
|
#else
|
|
options.platform = spirv_cross::CompilerMSL::Options::macOS;
|
|
#endif
|
|
// macOS 12 and iOS 15 are the minimum, and both have MSL 2.3.
|
|
options.msl_version = spirv_cross::CompilerMSL::Options::make_msl_version(2, 3);
|
|
options.pad_fragment_output_components = true;
|
|
// Point size is only written for point primitives, which we don't draw (they're expanded to quads).
|
|
options.enable_point_size_builtin = false;
|
|
options.use_framebuffer_fetch_subpasses = true;
|
|
msl.set_msl_options(options);
|
|
|
|
spirv_cross::CompilerGLSL::Options common = msl.get_common_options();
|
|
// Vulkan's clip space has y pointing down, Metal's has it pointing up. Both have the framebuffer origin
|
|
// at the top left, so flipping in the vertex shader makes everything else match Vulkan exactly.
|
|
common.vertex.flip_vert_y = true;
|
|
msl.set_common_options(common);
|
|
|
|
spirv_cross::ShaderResources resources = msl.get_shader_resources();
|
|
out->arrayTextureMask = 0;
|
|
out->textureMask = 0;
|
|
out->bufferMask = 0;
|
|
|
|
for (const auto &resource : resources.uniform_buffers) {
|
|
spirv_cross::MSLResourceBinding binding;
|
|
binding.stage = model;
|
|
binding.desc_set = msl.get_decoration(resource.id, spv::DecorationDescriptorSet);
|
|
binding.binding = msl.get_decoration(resource.id, spv::DecorationBinding);
|
|
binding.msl_buffer = binding.binding;
|
|
msl.add_msl_resource_binding(binding);
|
|
out->bufferMask |= 1 << binding.binding;
|
|
}
|
|
for (const auto &resource : resources.storage_buffers) {
|
|
spirv_cross::MSLResourceBinding binding;
|
|
binding.stage = model;
|
|
binding.desc_set = msl.get_decoration(resource.id, spv::DecorationDescriptorSet);
|
|
binding.binding = msl.get_decoration(resource.id, spv::DecorationBinding);
|
|
binding.msl_buffer = binding.binding;
|
|
msl.add_msl_resource_binding(binding);
|
|
out->bufferMask |= 1 << binding.binding;
|
|
}
|
|
auto addImage = [&](const spirv_cross::Resource &resource) {
|
|
spirv_cross::MSLResourceBinding binding;
|
|
binding.stage = model;
|
|
binding.desc_set = msl.get_decoration(resource.id, spv::DecorationDescriptorSet);
|
|
binding.binding = msl.get_decoration(resource.id, spv::DecorationBinding);
|
|
binding.msl_texture = binding.binding;
|
|
binding.msl_sampler = binding.binding;
|
|
msl.add_msl_resource_binding(binding);
|
|
const spirv_cross::SPIRType &type = msl.get_type(resource.type_id);
|
|
if (type.image.arrayed) {
|
|
out->arrayTextureMask |= 1 << binding.binding;
|
|
}
|
|
out->textureMask |= 1 << binding.binding;
|
|
};
|
|
for (const auto &resource : resources.sampled_images) {
|
|
addImage(resource);
|
|
}
|
|
for (const auto &resource : resources.storage_images) {
|
|
addImage(resource);
|
|
}
|
|
if (!resources.push_constant_buffers.empty()) {
|
|
spirv_cross::MSLResourceBinding binding;
|
|
binding.stage = model;
|
|
binding.desc_set = spirv_cross::kPushConstDescSet;
|
|
binding.binding = spirv_cross::kPushConstBinding;
|
|
binding.msl_buffer = METAL_PUSH_CONSTANT_INDEX;
|
|
msl.add_msl_resource_binding(binding);
|
|
}
|
|
out->readsFramebuffer = !resources.subpass_inputs.empty();
|
|
|
|
out->msl = msl.compile();
|
|
out->entryPoint = msl.get_cleansed_entry_point_name("main", model);
|
|
} catch (const std::exception &e) {
|
|
*errorMessage = StringFromFormat("SPIRV-Cross MSL error: %s", e.what());
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
id<MTLFunction> CreateMetalFunction(id<MTLDevice> device, const MetalTranslatedShader &shader, const char *tag, std::string *errorMessage) {
|
|
MTLCompileOptions *options = [MTLCompileOptions new];
|
|
options.languageVersion = MTLLanguageVersion2_3;
|
|
// Multipass effects depend on identical positions from different vertex shaders (equal depth tests).
|
|
options.preserveInvariance = YES;
|
|
NSError *error = nil;
|
|
id<MTLLibrary> library = [device newLibraryWithSource:[NSString stringWithUTF8String:shader.msl.c_str()] options:options error:&error];
|
|
if (!library) {
|
|
*errorMessage = StringFromFormat("MSL compile failed (%s): %s", tag, error ? [[error localizedDescription] UTF8String] : "?");
|
|
return nil;
|
|
}
|
|
id<MTLFunction> function = [library newFunctionWithName:[NSString stringWithUTF8String:shader.entryPoint.c_str()]];
|
|
if (!function) {
|
|
*errorMessage = StringFromFormat("Entry point %s not found (%s)", shader.entryPoint.c_str(), tag);
|
|
return nil;
|
|
}
|
|
function.label = [NSString stringWithUTF8String:tag];
|
|
return function;
|
|
}
|
|
|
|
class MetalCompileTask : public Task {
|
|
public:
|
|
MetalCompileTask(std::function<void()> work) : work_(std::move(work)) {}
|
|
TaskType Type() const override { return TaskType::DEDICATED_THREAD; }
|
|
TaskPriority Priority() const override { return TaskPriority::NORMAL; }
|
|
void Run() override { @autoreleasepool { work_(); } }
|
|
|
|
private:
|
|
std::function<void()> work_;
|
|
};
|
|
|
|
void RunOnCompileThread(std::function<void()> work) {
|
|
g_threadManager.EnqueueTask(new MetalCompileTask(std::move(work)));
|
|
}
|
|
|
|
@implementation MetalShaderFunction {
|
|
id<MTLDevice> device_;
|
|
MetalTranslatedShader shader_;
|
|
std::string tag_;
|
|
std::once_flag once_;
|
|
id<MTLFunction> function_;
|
|
}
|
|
|
|
- (instancetype)initWithDevice:(id<MTLDevice>)device shader:(const MetalTranslatedShader &)shader tag:(const std::string &)tag {
|
|
if (self = [super init]) {
|
|
device_ = device;
|
|
shader_ = shader;
|
|
tag_ = tag;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (id<MTLFunction>)function {
|
|
std::call_once(once_, [self] {
|
|
std::string errorMessage;
|
|
function_ = CreateMetalFunction(device_, shader_, tag_.c_str(), &errorMessage);
|
|
if (!function_) {
|
|
ERROR_LOG(Log::G3D, "%s\n%s", errorMessage.c_str(), LineNumberString(shader_.msl).c_str());
|
|
}
|
|
// Not needed anymore.
|
|
shader_.msl.clear();
|
|
});
|
|
return function_;
|
|
}
|
|
@end
|
|
|
|
@implementation MetalPipelineState {
|
|
std::mutex mutex_;
|
|
std::condition_variable cond_;
|
|
std::atomic<bool> ready_;
|
|
id<MTLRenderPipelineState> pipeline_;
|
|
}
|
|
|
|
- (void)setPipeline:(id<MTLRenderPipelineState>)pipeline {
|
|
{
|
|
std::lock_guard<std::mutex> guard(mutex_);
|
|
pipeline_ = pipeline;
|
|
ready_.store(true, std::memory_order_release);
|
|
}
|
|
cond_.notify_all();
|
|
}
|
|
|
|
- (id<MTLRenderPipelineState>)wait {
|
|
if (!ready_.load(std::memory_order_acquire)) {
|
|
std::unique_lock<std::mutex> lock(mutex_);
|
|
cond_.wait(lock, [self] { return ready_.load(std::memory_order_acquire); });
|
|
}
|
|
return pipeline_;
|
|
}
|
|
|
|
- (BOOL)isReady {
|
|
return ready_.load(std::memory_order_acquire);
|
|
}
|
|
@end
|
|
|
|
MetalRenderManager::MetalRenderManager(id<MTLDevice> device, CAMetalLayer *layer, int offscreenWidth, int offscreenHeight, bool useThread) : device_(device) {
|
|
queue_ = [device_ newCommandQueue];
|
|
queue_.label = @"PPSSPP";
|
|
unifiedMemory_ = device_.hasUnifiedMemory;
|
|
appleGPU_ = [device_ supportsFamily:MTLGPUFamilyApple1];
|
|
|
|
for (int i = 0; i < METAL_MAX_INFLIGHT_FRAMES; i++) {
|
|
frames_[i].push.Init(device_, 4 * 1024 * 1024, StringFromFormat("push%d", i).c_str());
|
|
}
|
|
frameSemaphore_ = dispatch_semaphore_create(METAL_MAX_INFLIGHT_FRAMES);
|
|
// Claim the first frame's slot.
|
|
dispatch_semaphore_wait(frameSemaphore_, DISPATCH_TIME_FOREVER);
|
|
flushSemaphore_ = dispatch_semaphore_create(0);
|
|
|
|
chunk_ = new MetalCommandChunk();
|
|
backbuffer_ = CreateSwapchain(layer, offscreenWidth, offscreenHeight);
|
|
if (useThread) {
|
|
renderThread_ = std::thread([this] { RenderThreadFunc(); });
|
|
}
|
|
}
|
|
|
|
MetalRenderManager::~MetalRenderManager() {
|
|
WaitIdle();
|
|
if (renderThread_.joinable()) {
|
|
MetalCommandChunk *exitChunk = new MetalCommandChunk();
|
|
exitChunk->exitThread = true;
|
|
{
|
|
std::lock_guard<std::mutex> guard(queueMutex_);
|
|
queue_chunks_.push_back(exitChunk);
|
|
}
|
|
queueCond_.notify_one();
|
|
renderThread_.join();
|
|
}
|
|
for (MetalSwapchain *swapchain : swapchains_) {
|
|
delete swapchain;
|
|
}
|
|
swapchains_.clear();
|
|
delete chunk_;
|
|
for (MetalCommandChunk *chunk : freeChunks_) {
|
|
delete chunk;
|
|
}
|
|
for (int i = 0; i < METAL_MAX_INFLIGHT_FRAMES; i++) {
|
|
frames_[i].push.Destroy();
|
|
frames_[i].deleteList.clear();
|
|
}
|
|
// Give back the slot we're holding, and wait for all the others, so the semaphore is
|
|
// back at its initial value. libdispatch crashes when releasing it otherwise.
|
|
dispatch_semaphore_signal(frameSemaphore_);
|
|
for (int i = 0; i < METAL_MAX_INFLIGHT_FRAMES; i++) {
|
|
dispatch_semaphore_wait(frameSemaphore_, DISPATCH_TIME_FOREVER);
|
|
}
|
|
for (int i = 0; i < METAL_MAX_INFLIGHT_FRAMES; i++) {
|
|
dispatch_semaphore_signal(frameSemaphore_);
|
|
}
|
|
}
|
|
|
|
void MetalRenderManager::SetInflightFrames(int count) {
|
|
// Changing the number of slots with frames in flight would be messy. We just
|
|
// stop using some of them, by waiting on the frame's completion in Present.
|
|
inflightFrames_ = std::clamp(count, 1, METAL_MAX_INFLIGHT_FRAMES);
|
|
}
|
|
|
|
MetalSwapchain *MetalRenderManager::CreateSwapchain(CAMetalLayer *layer, int width, int height) {
|
|
MetalSwapchain *swapchain = new MetalSwapchain();
|
|
if (layer) {
|
|
swapchain->layer = layer;
|
|
layer.device = device_;
|
|
layer.pixelFormat = MTLPixelFormatBGRA8Unorm;
|
|
// So we can read back screenshots, and copy from it.
|
|
layer.framebufferOnly = NO;
|
|
layer.maximumDrawableCount = 3;
|
|
if (width > 0 && height > 0) {
|
|
swapchain->pendingWidth = width;
|
|
swapchain->pendingHeight = height;
|
|
} else {
|
|
CGSize size = layer.drawableSize;
|
|
width = (int)size.width;
|
|
height = (int)size.height;
|
|
}
|
|
} else {
|
|
MTLTextureDescriptor *desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm width:width height:height mipmapped:NO];
|
|
desc.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead;
|
|
desc.storageMode = MTLStorageModePrivate;
|
|
swapchain->offscreen = [device_ newTextureWithDescriptor:desc];
|
|
swapchain->offscreen.label = @"offscreen_backbuffer";
|
|
}
|
|
swapchain->width = width;
|
|
swapchain->height = height;
|
|
CreateSwapchainDepth(swapchain);
|
|
swapchains_.push_back(swapchain);
|
|
return swapchain;
|
|
}
|
|
|
|
void MetalRenderManager::DestroySwapchain(MetalSwapchain *swapchain) {
|
|
_dbg_assert_(swapchain != backbuffer_);
|
|
if (passSwapchain_ == swapchain) {
|
|
UnbindRenderPass();
|
|
}
|
|
// After this, no chunk that refers to it is left to replay.
|
|
WaitIdle();
|
|
swapchains_.erase(std::remove(swapchains_.begin(), swapchains_.end(), swapchain), swapchains_.end());
|
|
rtAcquired_.erase(std::remove(rtAcquired_.begin(), rtAcquired_.end(), swapchain), rtAcquired_.end());
|
|
delete swapchain;
|
|
}
|
|
|
|
void MetalRenderManager::CreateSwapchainDepth(MetalSwapchain *swapchain) {
|
|
DeferRelease(swapchain->depth);
|
|
if (swapchain->width <= 0 || swapchain->height <= 0) {
|
|
swapchain->depth = nil;
|
|
return;
|
|
}
|
|
MTLTextureDescriptor *desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatDepth32Float_Stencil8 width:swapchain->width height:swapchain->height mipmapped:NO];
|
|
desc.usage = MTLTextureUsageRenderTarget;
|
|
if (appleGPU_) {
|
|
// The backbuffer's depth never needs to survive a render pass.
|
|
desc.storageMode = MTLStorageModeMemoryless;
|
|
} else {
|
|
desc.storageMode = MTLStorageModePrivate;
|
|
}
|
|
swapchain->depth = [device_ newTextureWithDescriptor:desc];
|
|
swapchain->depth.label = @"backbuffer_depth";
|
|
}
|
|
|
|
void MetalRenderManager::Resize(MetalSwapchain *swapchain, int width, int height) {
|
|
if (!swapchain->layer || width <= 0 || height <= 0) {
|
|
return;
|
|
}
|
|
if (passSwapchain_ == swapchain) {
|
|
UnbindRenderPass();
|
|
}
|
|
swapchain->pendingWidth = width;
|
|
swapchain->pendingHeight = height;
|
|
swapchain->width = width;
|
|
swapchain->height = height;
|
|
CreateSwapchainDepth(swapchain);
|
|
}
|
|
|
|
void MetalRenderManager::BeginFrame() {
|
|
_dbg_assert_(!inFrame_);
|
|
inFrame_ = true;
|
|
}
|
|
|
|
void MetalRenderManager::EndFrame() {
|
|
_dbg_assert_(inFrame_);
|
|
inFrame_ = false;
|
|
EndRenderPass();
|
|
}
|
|
|
|
void MetalRenderManager::Submit(MetalChunkEnd end, bool vsync, dispatch_semaphore_t completed) {
|
|
EndRenderPass();
|
|
MetalCommandChunk *chunk = chunk_;
|
|
chunk->end = end;
|
|
chunk->vsync = vsync;
|
|
chunk->completed = completed;
|
|
for (MetalSwapchain *swapchain : swapchains_) {
|
|
if (swapchain->pendingWidth > 0) {
|
|
chunk->drawableResizes.push_back({ swapchain, swapchain->pendingWidth, swapchain->pendingHeight });
|
|
swapchain->pendingWidth = 0;
|
|
swapchain->pendingHeight = 0;
|
|
}
|
|
}
|
|
|
|
if (renderThread_.joinable()) {
|
|
{
|
|
std::lock_guard<std::mutex> guard(freeMutex_);
|
|
if (!freeChunks_.empty()) {
|
|
chunk_ = freeChunks_.back();
|
|
freeChunks_.pop_back();
|
|
} else {
|
|
chunk_ = new MetalCommandChunk();
|
|
}
|
|
}
|
|
{
|
|
std::lock_guard<std::mutex> guard(queueMutex_);
|
|
queue_chunks_.push_back(chunk);
|
|
}
|
|
queueCond_.notify_one();
|
|
} else {
|
|
Replay(chunk);
|
|
chunk->Clear();
|
|
}
|
|
}
|
|
|
|
void MetalRenderManager::Present(bool vsync) {
|
|
const bool waitForGPU = inflightFrames_ == 1;
|
|
Submit(MetalChunkEnd::PRESENT, vsync, waitForGPU ? flushSemaphore_ : nullptr);
|
|
if (waitForGPU) {
|
|
dispatch_semaphore_wait(flushSemaphore_, DISPATCH_TIME_FOREVER);
|
|
}
|
|
|
|
lastStats_ = stats_;
|
|
stats_ = {};
|
|
|
|
// Advance to the next frame, and wait for the GPU to be done with its previous use.
|
|
frameNumber_++;
|
|
curFrame_ = (int)(frameNumber_ % METAL_MAX_INFLIGHT_FRAMES);
|
|
dispatch_semaphore_wait(frameSemaphore_, DISPATCH_TIME_FOREVER);
|
|
frames_[curFrame_].push.Reset();
|
|
frames_[curFrame_].deleteList.clear();
|
|
}
|
|
|
|
void MetalRenderManager::Flush(bool wait) {
|
|
Submit(MetalChunkEnd::FLUSH, true, wait ? flushSemaphore_ : nullptr);
|
|
if (wait) {
|
|
dispatch_semaphore_wait(flushSemaphore_, DISPATCH_TIME_FOREVER);
|
|
}
|
|
stats_.flushes++;
|
|
}
|
|
|
|
void MetalRenderManager::WaitIdle() {
|
|
Flush(true);
|
|
}
|
|
|
|
void MetalRenderManager::BindBackbufferPass(MetalSwapchain *swapchain, MTLLoadAction colorLoad, MTLClearColor clearColor, const char *tag) {
|
|
if (passSwapchain_ == swapchain && inPass_ && colorLoad == MTLLoadActionLoad) {
|
|
// Already rendering to it.
|
|
return;
|
|
}
|
|
swapchain->drawn = true;
|
|
EndRenderPass();
|
|
MetalRenderPassDesc desc{};
|
|
desc.colorLoad = colorLoad;
|
|
desc.clearColor = clearColor;
|
|
desc.depthLoad = MTLLoadActionClear;
|
|
desc.stencilLoad = MTLLoadActionClear;
|
|
desc.depthMemoryless = appleGPU_;
|
|
desc.tag = tag;
|
|
desc.depth = swapchain->depth;
|
|
pass_ = desc;
|
|
passSwapchain_ = swapchain;
|
|
curFormat_.color = MTLPixelFormatBGRA8Unorm;
|
|
curFormat_.depth = swapchain->depth ? MTLPixelFormatDepth32Float_Stencil8 : MTLPixelFormatInvalid;
|
|
curFormat_.stencil = curFormat_.depth;
|
|
curFormat_.sampleCount = 1;
|
|
passWidth_ = swapchain->width;
|
|
passHeight_ = swapchain->height;
|
|
}
|
|
|
|
void MetalRenderManager::BindRenderPass(const MetalRenderPassDesc &desc) {
|
|
if (!passSwapchain_ && HasRenderPass() && desc.color == pass_.color && desc.depth == pass_.depth) {
|
|
// Binding the target we're already on continues the pass, like in the Vulkan backend: clears are applied,
|
|
// and DontCare is ignored since there's content to keep.
|
|
bool clearColor = desc.colorLoad == MTLLoadActionClear;
|
|
bool clearDepth = desc.depthLoad == MTLLoadActionClear;
|
|
bool clearStencil = desc.stencilLoad == MTLLoadActionClear;
|
|
if (clearColor || clearDepth || clearStencil) {
|
|
Clear(clearColor, clearDepth, clearStencil, desc.clearColor, desc.clearDepth, desc.clearStencil);
|
|
}
|
|
return;
|
|
}
|
|
EndRenderPass();
|
|
pass_ = desc;
|
|
passSwapchain_ = nullptr;
|
|
id<MTLTexture> sizeTex = desc.color ? desc.color : desc.depth;
|
|
passWidth_ = sizeTex ? (int)sizeTex.width : 0;
|
|
passHeight_ = sizeTex ? (int)sizeTex.height : 0;
|
|
curFormat_.color = desc.color ? desc.color.pixelFormat : MTLPixelFormatInvalid;
|
|
curFormat_.depth = desc.depth ? desc.depth.pixelFormat : MTLPixelFormatInvalid;
|
|
curFormat_.stencil = (curFormat_.depth == MTLPixelFormatDepth32Float_Stencil8) ? curFormat_.depth : MTLPixelFormatInvalid;
|
|
id<MTLTexture> msaaTex = desc.msaaColor ? desc.msaaColor : desc.msaaDepth;
|
|
curFormat_.sampleCount = msaaTex ? (int)msaaTex.sampleCount : 1;
|
|
}
|
|
|
|
void MetalRenderManager::UnbindRenderPass() {
|
|
EndRenderPass();
|
|
pass_ = MetalRenderPassDesc{};
|
|
passSwapchain_ = nullptr;
|
|
curFormat_ = MetalRenderTargetFormat{};
|
|
}
|
|
|
|
void MetalRenderManager::Clear(bool color, bool depth, bool stencil, MTLClearColor clearColor, double clearDepth, uint32_t clearStencil) {
|
|
if (!HasRenderPass()) {
|
|
return;
|
|
}
|
|
if (inPass_) {
|
|
// Metal has no clear command within a pass, so restart it with clear load actions.
|
|
RecordEndPass();
|
|
}
|
|
if (color) {
|
|
pass_.colorLoad = MTLLoadActionClear;
|
|
pass_.clearColor = clearColor;
|
|
}
|
|
if (depth) {
|
|
pass_.depthLoad = MTLLoadActionClear;
|
|
pass_.clearDepth = clearDepth;
|
|
}
|
|
if (stencil) {
|
|
pass_.stencilLoad = MTLLoadActionClear;
|
|
pass_.clearStencil = clearStencil;
|
|
}
|
|
}
|
|
|
|
void MetalRenderManager::StartRenderPass() {
|
|
if (!HasRenderPass()) {
|
|
return;
|
|
}
|
|
MetalRecordedPass rp;
|
|
rp.swapchain = passSwapchain_;
|
|
rp.color = passSwapchain_ ? nullptr : (__bridge void *)pass_.color;
|
|
rp.depth = (__bridge void *)pass_.depth;
|
|
rp.msaaColor = (__bridge void *)pass_.msaaColor;
|
|
rp.msaaDepth = (__bridge void *)pass_.msaaDepth;
|
|
rp.colorLoad = pass_.colorLoad;
|
|
rp.depthLoad = pass_.depthLoad;
|
|
rp.stencilLoad = pass_.stencilLoad;
|
|
rp.clearColor = pass_.clearColor;
|
|
rp.clearDepth = pass_.clearDepth;
|
|
rp.clearStencil = pass_.clearStencil;
|
|
rp.depthMemoryless = pass_.depthMemoryless;
|
|
rp.depthStore = MTLStoreActionStore;
|
|
rp.usesDepth = false;
|
|
if (pass_.tag) {
|
|
rp.tag = pass_.tag;
|
|
}
|
|
chunk_->passes.push_back(std::move(rp));
|
|
recordedPassIndex_ = chunk_->passes.size() - 1;
|
|
Record(MetalCmd::BEGIN_PASS).pass = (uint32_t)recordedPassIndex_;
|
|
inPass_ = true;
|
|
passUsesDepth_ = false;
|
|
|
|
stats_.renderPasses++;
|
|
encoderGeneration_++;
|
|
|
|
// If the pass is interrupted and resumed, keep the contents.
|
|
pass_.colorLoad = MTLLoadActionLoad;
|
|
pass_.depthLoad = MTLLoadActionLoad;
|
|
pass_.stencilLoad = MTLLoadActionLoad;
|
|
|
|
ResetEncoderState();
|
|
}
|
|
|
|
void MetalRenderManager::EndRenderPass() {
|
|
if (!inPass_ && HasRenderPass()) {
|
|
// A pass that was set up to clear, but never drawn to, still has to clear.
|
|
if (pass_.colorLoad == MTLLoadActionClear || pass_.depthLoad == MTLLoadActionClear || pass_.stencilLoad == MTLLoadActionClear) {
|
|
if (!(pass_.depthMemoryless && pass_.colorLoad != MTLLoadActionClear)) {
|
|
StartRenderPass();
|
|
} else {
|
|
// Only the memoryless depth would be cleared, which nobody can see.
|
|
pass_.depthLoad = MTLLoadActionLoad;
|
|
pass_.stencilLoad = MTLLoadActionLoad;
|
|
}
|
|
}
|
|
}
|
|
if (inPass_) {
|
|
RecordEndPass();
|
|
}
|
|
}
|
|
|
|
void MetalRenderManager::RecordEndPass() {
|
|
chunk_->passes[recordedPassIndex_].usesDepth = passUsesDepth_;
|
|
Record(MetalCmd::END_PASS);
|
|
inPass_ = false;
|
|
}
|
|
|
|
void MetalRenderManager::ResetEncoderState() {
|
|
state_ = EncoderState{};
|
|
}
|
|
|
|
MetalRecordedCopy &MetalRenderManager::RecordCopy(bool upload) {
|
|
if (!upload) {
|
|
EndRenderPass();
|
|
stats_.blitPasses++;
|
|
}
|
|
chunk_->copies.emplace_back();
|
|
MetalCommand c{};
|
|
c.cmd = MetalCmd::COPY;
|
|
c.copy = (uint32_t)chunk_->copies.size() - 1;
|
|
if (upload) {
|
|
chunk_->uploads.push_back(c);
|
|
} else {
|
|
chunk_->commands.push_back(c);
|
|
}
|
|
return chunk_->copies.back();
|
|
}
|
|
|
|
void MetalRenderManager::CopyTexture(__unsafe_unretained id<MTLTexture> src, int srcLevel, MTLOrigin srcOrigin, MTLSize size, __unsafe_unretained id<MTLTexture> dst, int dstLevel, MTLOrigin dstOrigin) {
|
|
MetalRecordedCopy © = RecordCopy(false);
|
|
copy.type = MetalCopyType::TEXTURE_TO_TEXTURE;
|
|
copy.src = (__bridge void *)src;
|
|
copy.dst = (__bridge void *)dst;
|
|
copy.srcLevel = srcLevel;
|
|
copy.dstLevel = dstLevel;
|
|
copy.srcOrigin = srcOrigin;
|
|
copy.dstOrigin = dstOrigin;
|
|
copy.size = size;
|
|
}
|
|
|
|
void MetalRenderManager::CopyTextureToBuffer(__unsafe_unretained id<MTLTexture> src, int level, MTLOrigin origin, MTLSize size, __unsafe_unretained id<MTLBuffer> dst, uint32_t offset, uint32_t bytesPerRow, uint32_t bytesPerImage, MTLBlitOption options, MetalSwapchain *srcSwapchain) {
|
|
MetalRecordedCopy © = RecordCopy(false);
|
|
copy.type = MetalCopyType::TEXTURE_TO_BUFFER;
|
|
copy.srcSwapchain = srcSwapchain;
|
|
copy.src = (__bridge void *)src;
|
|
copy.dst = (__bridge void *)dst;
|
|
copy.srcLevel = level;
|
|
copy.srcOrigin = origin;
|
|
copy.size = size;
|
|
copy.dstOffset = offset;
|
|
copy.bytesPerRow = bytesPerRow;
|
|
copy.bytesPerImage = bytesPerImage;
|
|
copy.options = options;
|
|
}
|
|
|
|
void MetalRenderManager::CopyBufferToTexture(__unsafe_unretained id<MTLBuffer> src, uint32_t offset, uint32_t bytesPerRow, uint32_t bytesPerImage, MTLSize size, __unsafe_unretained id<MTLTexture> dst, int slice, int level, MTLOrigin origin, MTLBlitOption options, bool upload) {
|
|
MetalRecordedCopy © = RecordCopy(upload);
|
|
copy.type = MetalCopyType::BUFFER_TO_TEXTURE;
|
|
copy.src = (__bridge void *)src;
|
|
copy.dst = (__bridge void *)dst;
|
|
copy.srcOffset = offset;
|
|
copy.bytesPerRow = bytesPerRow;
|
|
copy.bytesPerImage = bytesPerImage;
|
|
copy.size = size;
|
|
copy.dstSlice = slice;
|
|
copy.dstLevel = level;
|
|
copy.dstOrigin = origin;
|
|
copy.options = options;
|
|
}
|
|
|
|
void MetalRenderManager::GenerateMips(__unsafe_unretained id<MTLTexture> texture, bool upload) {
|
|
MetalRecordedCopy © = RecordCopy(upload);
|
|
copy.type = MetalCopyType::GENERATE_MIPS;
|
|
copy.dst = (__bridge void *)texture;
|
|
}
|
|
|
|
MetalRecordedDispatch &MetalRenderManager::RecordUploadDispatch() {
|
|
chunk_->dispatches.emplace_back();
|
|
MetalCommand c{};
|
|
c.cmd = MetalCmd::DISPATCH;
|
|
c.dispatch = (uint32_t)chunk_->dispatches.size() - 1;
|
|
chunk_->uploads.push_back(c);
|
|
MetalRecordedDispatch &dispatch = chunk_->dispatches.back();
|
|
dispatch = {};
|
|
return dispatch;
|
|
}
|
|
|
|
void MetalRenderManager::SetPipeline(__unsafe_unretained MetalPipelineState *pipeline) {
|
|
if (state_.pipeline != pipeline) {
|
|
Record(MetalCmd::PIPELINE).bind.obj = (__bridge void *)pipeline;
|
|
state_.pipeline = pipeline;
|
|
}
|
|
}
|
|
|
|
void MetalRenderManager::SetDepthStencilState(__unsafe_unretained id<MTLDepthStencilState> dss, bool usesDepthStencil) {
|
|
state_.depthStencilUsed = usesDepthStencil;
|
|
if (state_.depthStencil != dss) {
|
|
Record(MetalCmd::DEPTH_STENCIL).bind.obj = (__bridge void *)dss;
|
|
state_.depthStencil = dss;
|
|
}
|
|
}
|
|
|
|
void MetalRenderManager::SetStencilRef(uint32_t ref) {
|
|
if (!state_.stencilRefValid || state_.stencilRef != ref) {
|
|
Record(MetalCmd::STENCIL_REF).value = ref;
|
|
state_.stencilRef = ref;
|
|
state_.stencilRefValid = true;
|
|
}
|
|
}
|
|
|
|
void MetalRenderManager::SetBlendColor(float r, float g, float b, float a) {
|
|
if (!state_.blendColorValid || state_.blendColor[0] != r || state_.blendColor[1] != g || state_.blendColor[2] != b || state_.blendColor[3] != a) {
|
|
MetalCommand &c = Record(MetalCmd::BLEND_COLOR);
|
|
c.color[0] = r;
|
|
c.color[1] = g;
|
|
c.color[2] = b;
|
|
c.color[3] = a;
|
|
state_.blendColor[0] = r;
|
|
state_.blendColor[1] = g;
|
|
state_.blendColor[2] = b;
|
|
state_.blendColor[3] = a;
|
|
state_.blendColorValid = true;
|
|
}
|
|
}
|
|
|
|
void MetalRenderManager::SetCullMode(MTLCullMode mode) {
|
|
if (!state_.cullValid || state_.cull != mode) {
|
|
Record(MetalCmd::CULL).value = (uint32_t)mode;
|
|
state_.cull = mode;
|
|
state_.cullValid = true;
|
|
}
|
|
}
|
|
|
|
void MetalRenderManager::SetFrontFacing(MTLWinding winding) {
|
|
if (!state_.windingValid || state_.winding != winding) {
|
|
Record(MetalCmd::WINDING).value = (uint32_t)winding;
|
|
state_.winding = winding;
|
|
state_.windingValid = true;
|
|
}
|
|
}
|
|
|
|
void MetalRenderManager::SetDepthClip(MTLDepthClipMode mode) {
|
|
if (!state_.depthClipValid || state_.depthClip != mode) {
|
|
Record(MetalCmd::DEPTH_CLIP).value = (uint32_t)mode;
|
|
state_.depthClip = mode;
|
|
state_.depthClipValid = true;
|
|
}
|
|
}
|
|
|
|
void MetalRenderManager::SetViewport(const MTLViewport &vp) {
|
|
if (!state_.viewportValid || memcmp(&vp, &state_.viewport, sizeof(vp)) != 0) {
|
|
Record(MetalCmd::VIEWPORT).viewport = vp;
|
|
state_.viewport = vp;
|
|
state_.viewportValid = true;
|
|
}
|
|
}
|
|
|
|
bool MetalRenderManager::SetScissor(int x, int y, int w, int h) {
|
|
int x2 = std::min(x + w, passWidth_);
|
|
int y2 = std::min(y + h, passHeight_);
|
|
x = std::max(x, 0);
|
|
y = std::max(y, 0);
|
|
if (x2 <= x || y2 <= y) {
|
|
return false;
|
|
}
|
|
MTLScissorRect rc{ (NSUInteger)x, (NSUInteger)y, (NSUInteger)(x2 - x), (NSUInteger)(y2 - y) };
|
|
if (!state_.scissorValid || memcmp(&rc, &state_.scissor, sizeof(rc)) != 0) {
|
|
Record(MetalCmd::SCISSOR).scissor = rc;
|
|
state_.scissor = rc;
|
|
state_.scissorValid = true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void MetalRenderManager::SetVertexBuffer(__unsafe_unretained id<MTLBuffer> buffer, uint32_t offset, int index) {
|
|
if (state_.vertexBuffers[index] != buffer) {
|
|
MetalCommand &c = Record(MetalCmd::VERTEX_BUFFER);
|
|
c.index = index;
|
|
c.bind.obj = (__bridge void *)buffer;
|
|
c.bind.offset = offset;
|
|
state_.vertexBuffers[index] = buffer;
|
|
state_.vertexBufferOffsets[index] = offset;
|
|
} else if (state_.vertexBufferOffsets[index] != offset) {
|
|
MetalCommand &c = Record(MetalCmd::VERTEX_BUFFER_OFFSET);
|
|
c.index = index;
|
|
c.bind.offset = offset;
|
|
state_.vertexBufferOffsets[index] = offset;
|
|
}
|
|
}
|
|
|
|
void MetalRenderManager::SetFragmentBuffer(__unsafe_unretained id<MTLBuffer> buffer, uint32_t offset, int index) {
|
|
if (state_.fragmentBuffers[index] != buffer) {
|
|
MetalCommand &c = Record(MetalCmd::FRAGMENT_BUFFER);
|
|
c.index = index;
|
|
c.bind.obj = (__bridge void *)buffer;
|
|
c.bind.offset = offset;
|
|
state_.fragmentBuffers[index] = buffer;
|
|
state_.fragmentBufferOffsets[index] = offset;
|
|
} else if (state_.fragmentBufferOffsets[index] != offset) {
|
|
MetalCommand &c = Record(MetalCmd::FRAGMENT_BUFFER_OFFSET);
|
|
c.index = index;
|
|
c.bind.offset = offset;
|
|
state_.fragmentBufferOffsets[index] = offset;
|
|
}
|
|
}
|
|
|
|
void MetalRenderManager::SetFragmentTexture(__unsafe_unretained id<MTLTexture> texture, int index) {
|
|
if (state_.fragmentTextures[index] != texture) {
|
|
MetalCommand &c = Record(MetalCmd::FRAGMENT_TEXTURE);
|
|
c.index = index;
|
|
c.bind.obj = (__bridge void *)texture;
|
|
state_.fragmentTextures[index] = texture;
|
|
}
|
|
}
|
|
|
|
void MetalRenderManager::SetFragmentSampler(__unsafe_unretained id<MTLSamplerState> sampler, int index) {
|
|
if (state_.fragmentSamplers[index] != sampler) {
|
|
MetalCommand &c = Record(MetalCmd::FRAGMENT_SAMPLER);
|
|
c.index = index;
|
|
c.bind.obj = (__bridge void *)sampler;
|
|
state_.fragmentSamplers[index] = sampler;
|
|
}
|
|
}
|
|
|
|
void MetalRenderManager::Draw(MTLPrimitiveType prim, uint32_t start, uint32_t count) {
|
|
// Metal doesn't allow empty draws.
|
|
if (count == 0) {
|
|
return;
|
|
}
|
|
passUsesDepth_ |= state_.depthStencilUsed;
|
|
MetalCommand &c = Record(MetalCmd::DRAW);
|
|
c.prim = (uint8_t)prim;
|
|
c.draw.start = start;
|
|
c.draw.count = count;
|
|
stats_.draws++;
|
|
}
|
|
|
|
void MetalRenderManager::DrawIndexed(MTLPrimitiveType prim, uint32_t count, __unsafe_unretained id<MTLBuffer> indexBuffer, uint32_t offset) {
|
|
if (count == 0) {
|
|
return;
|
|
}
|
|
passUsesDepth_ |= state_.depthStencilUsed;
|
|
MetalCommand &c = Record(MetalCmd::DRAW_INDEXED);
|
|
c.prim = (uint8_t)prim;
|
|
c.drawIndexed.buffer = (__bridge void *)indexBuffer;
|
|
c.drawIndexed.offset = offset;
|
|
c.drawIndexed.count = count;
|
|
stats_.draws++;
|
|
}
|
|
|
|
void MetalRenderManager::RenderThreadFunc() {
|
|
SetCurrentThreadName("MetalRender");
|
|
while (true) {
|
|
MetalCommandChunk *chunk;
|
|
{
|
|
std::unique_lock<std::mutex> lock(queueMutex_);
|
|
queueCond_.wait(lock, [this] { return !queue_chunks_.empty(); });
|
|
chunk = queue_chunks_.front();
|
|
queue_chunks_.pop_front();
|
|
}
|
|
if (chunk->exitThread) {
|
|
delete chunk;
|
|
break;
|
|
}
|
|
@autoreleasepool {
|
|
Replay(chunk);
|
|
}
|
|
chunk->Clear();
|
|
std::lock_guard<std::mutex> guard(freeMutex_);
|
|
freeChunks_.push_back(chunk);
|
|
}
|
|
}
|
|
|
|
id<MTLTexture> MetalRenderManager::ReplaySwapchainTexture(MetalSwapchain *swapchain) {
|
|
if (!swapchain->layer) {
|
|
return swapchain->offscreen;
|
|
}
|
|
if (!swapchain->drawable && swapchain->noDrawableFrame != replayFrame_) {
|
|
@autoreleasepool {
|
|
swapchain->drawable = [swapchain->layer nextDrawable];
|
|
}
|
|
if (swapchain->drawable) {
|
|
rtAcquired_.push_back(swapchain);
|
|
} else {
|
|
swapchain->noDrawableFrame = replayFrame_;
|
|
}
|
|
}
|
|
return swapchain->drawable ? swapchain->drawable.texture : nil;
|
|
}
|
|
|
|
void MetalRenderManager::EndReplayEncoders() {
|
|
if (rtBlit_) {
|
|
[rtBlit_ endEncoding];
|
|
rtBlit_ = nil;
|
|
}
|
|
if (rtUploadBlit_) {
|
|
[rtUploadBlit_ endEncoding];
|
|
rtUploadBlit_ = nil;
|
|
}
|
|
if (rtUploadCompute_) {
|
|
[rtUploadCompute_ endEncoding];
|
|
rtUploadCompute_ = nil;
|
|
}
|
|
}
|
|
|
|
// Metal orders the encoders' accesses to the (tracked) textures, so there are no barriers to place.
|
|
void MetalRenderManager::ReplayDispatch(const MetalRecordedDispatch &dispatch, id<MTLCommandBuffer> cmd) {
|
|
if (rtUploadBlit_) {
|
|
[rtUploadBlit_ endEncoding];
|
|
rtUploadBlit_ = nil;
|
|
}
|
|
if (!rtUploadCompute_) {
|
|
rtUploadCompute_ = [cmd computeCommandEncoder];
|
|
rtUploadCompute_.label = @"upload compute";
|
|
}
|
|
id<MTLComputeCommandEncoder> enc = rtUploadCompute_;
|
|
[enc setComputePipelineState:(__bridge id<MTLComputePipelineState>)dispatch.pipeline];
|
|
for (int i = 0; i < METAL_MAX_TEXTURES; i++) {
|
|
if (dispatch.textures[i]) {
|
|
[enc setTexture:(__bridge id<MTLTexture>)dispatch.textures[i] atIndex:i];
|
|
}
|
|
}
|
|
for (int i = 0; i < METAL_MAX_BUFFERS; i++) {
|
|
if (dispatch.buffers[i]) {
|
|
[enc setBuffer:(__bridge id<MTLBuffer>)dispatch.buffers[i] offset:dispatch.bufferOffsets[i] atIndex:i];
|
|
}
|
|
}
|
|
if (dispatch.pushConstantSize) {
|
|
[enc setBytes:dispatch.pushConstants length:dispatch.pushConstantSize atIndex:METAL_PUSH_CONSTANT_INDEX];
|
|
}
|
|
[enc dispatchThreadgroups:dispatch.threadgroups threadsPerThreadgroup:dispatch.threadsPerThreadgroup];
|
|
}
|
|
|
|
void MetalRenderManager::Replay(MetalCommandChunk *chunk) {
|
|
for (const auto &resize : chunk->drawableResizes) {
|
|
resize.swapchain->layer.drawableSize = CGSizeMake(resize.width, resize.height);
|
|
}
|
|
|
|
// The upload buffer is enqueued first, so it runs first even though they're committed together.
|
|
id<MTLCommandBuffer> uploadCmd;
|
|
if (!chunk->uploads.empty()) {
|
|
uploadCmd = [queue_ commandBufferWithUnretainedReferences];
|
|
uploadCmd.label = @"upload";
|
|
[uploadCmd enqueue];
|
|
}
|
|
id<MTLCommandBuffer> cmd = [queue_ commandBufferWithUnretainedReferences];
|
|
cmd.label = @"main";
|
|
[cmd enqueue];
|
|
|
|
for (const MetalCommand &c : chunk->uploads) {
|
|
if (c.cmd == MetalCmd::DISPATCH) {
|
|
ReplayDispatch(chunk->dispatches[c.dispatch], uploadCmd);
|
|
} else {
|
|
if (rtUploadCompute_) {
|
|
[rtUploadCompute_ endEncoding];
|
|
rtUploadCompute_ = nil;
|
|
}
|
|
ReplayCopy(*chunk, chunk->copies[c.copy], uploadCmd, true);
|
|
}
|
|
}
|
|
BuildSteps(*chunk);
|
|
MergeSteps(*chunk);
|
|
for (size_t i = 0; i < rtStepCount_; i++) {
|
|
MetalStep &step = rtSteps_[i];
|
|
if (step.merged) {
|
|
continue;
|
|
}
|
|
if (step.isPass) {
|
|
ReplayPass(*chunk, step, cmd);
|
|
} else {
|
|
ReplayCopy(*chunk, chunk->copies[step.index], cmd, false);
|
|
}
|
|
}
|
|
EndReplayEncoders();
|
|
|
|
// The block keeps the drawables alive until the GPU is done with them.
|
|
NSMutableArray *drawables = nil;
|
|
if (chunk->end == MetalChunkEnd::PRESENT) {
|
|
for (MetalSwapchain *swapchain : rtAcquired_) {
|
|
#if PPSSPP_PLATFORM(MAC)
|
|
if (swapchain->layer.displaySyncEnabled != chunk->vsync) {
|
|
swapchain->layer.displaySyncEnabled = chunk->vsync;
|
|
}
|
|
#endif
|
|
[cmd presentDrawable:swapchain->drawable];
|
|
if (!drawables) {
|
|
drawables = [NSMutableArray arrayWithCapacity:rtAcquired_.size()];
|
|
}
|
|
[drawables addObject:swapchain->drawable];
|
|
swapchain->drawable = nil;
|
|
}
|
|
rtAcquired_.clear();
|
|
replayFrame_++;
|
|
}
|
|
|
|
dispatch_semaphore_t frameSemaphore = chunk->end == MetalChunkEnd::PRESENT ? frameSemaphore_ : nullptr;
|
|
dispatch_semaphore_t completed = chunk->completed;
|
|
if (frameSemaphore || completed) {
|
|
[cmd addCompletedHandler:^(id<MTLCommandBuffer> buffer) {
|
|
(void)drawables;
|
|
if (frameSemaphore)
|
|
dispatch_semaphore_signal(frameSemaphore);
|
|
if (completed)
|
|
dispatch_semaphore_signal(completed);
|
|
}];
|
|
}
|
|
[uploadCmd commit]; // A no-op if it's nil.
|
|
[cmd commit];
|
|
}
|
|
|
|
static inline void *BaseTexture(void *obj) {
|
|
id<MTLTexture> tex = (__bridge id<MTLTexture>)obj;
|
|
id<MTLTexture> parent = tex.parentTexture;
|
|
return parent ? (__bridge void *)parent : obj;
|
|
}
|
|
|
|
void MetalRenderManager::BuildSteps(const MetalCommandChunk &chunk) {
|
|
rtStepCount_ = 0;
|
|
auto newStep = [&]() -> MetalStep & {
|
|
if (rtStepCount_ == rtSteps_.size()) {
|
|
rtSteps_.emplace_back();
|
|
}
|
|
MetalStep &step = rtSteps_[rtStepCount_++];
|
|
step.ranges.clear();
|
|
step.reads.clear();
|
|
step.merged = false;
|
|
step.usesDepth = false;
|
|
return step;
|
|
};
|
|
MetalStep *pass = nullptr;
|
|
const std::vector<MetalCommand> &commands = chunk.commands;
|
|
for (uint32_t i = 0; i < (uint32_t)commands.size(); i++) {
|
|
const MetalCommand &c = commands[i];
|
|
switch (c.cmd) {
|
|
case MetalCmd::BEGIN_PASS:
|
|
{
|
|
const MetalRecordedPass &rp = chunk.passes[c.pass];
|
|
pass = &newStep();
|
|
pass->isPass = true;
|
|
pass->index = c.pass;
|
|
pass->ranges.push_back({ i + 1, i + 1 });
|
|
pass->target = rp.swapchain ? (void *)rp.swapchain : (rp.color ? rp.color : rp.depth);
|
|
pass->targetIsSwapchain = rp.swapchain != nullptr;
|
|
pass->targetDepth = rp.depth;
|
|
pass->targetMsaa = rp.msaaColor ? rp.msaaColor : rp.msaaDepth;
|
|
pass->usesDepth = rp.usesDepth;
|
|
break;
|
|
}
|
|
case MetalCmd::END_PASS:
|
|
if (pass) {
|
|
pass->ranges.back().second = i;
|
|
pass = nullptr;
|
|
}
|
|
break;
|
|
case MetalCmd::COPY:
|
|
{
|
|
const MetalRecordedCopy © = chunk.copies[c.copy];
|
|
MetalStep &step = newStep();
|
|
step.isPass = false;
|
|
step.index = c.copy;
|
|
// Compared with pass targets, which are never views.
|
|
step.target = copy.type == MetalCopyType::TEXTURE_TO_BUFFER ? copy.dst : BaseTexture(copy.dst);
|
|
step.targetMsaa = nullptr;
|
|
step.targetDepth = nullptr;
|
|
step.targetIsSwapchain = false;
|
|
if (copy.srcSwapchain) {
|
|
step.reads.push_back(copy.srcSwapchain);
|
|
} else if (copy.src && copy.type != MetalCopyType::BUFFER_TO_TEXTURE) {
|
|
step.reads.push_back(BaseTexture(copy.src));
|
|
}
|
|
break;
|
|
}
|
|
case MetalCmd::FRAGMENT_TEXTURE:
|
|
if (pass && c.bind.obj) {
|
|
pass->reads.push_back(BaseTexture(c.bind.obj));
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool Contains(const std::vector<void *> &v, void *p) {
|
|
return p && std::find(v.begin(), v.end(), p) != v.end();
|
|
}
|
|
|
|
// Moves later passes to the same framebuffer into earlier ones, when nothing in between depends on the result
|
|
// or writes to it. Same rules as VulkanQueueRunner::ApplyRenderPassMerge. God of War alternates between its
|
|
// framebuffer and block transfers to other ones, which otherwise breaks its passes up a dozen times a frame.
|
|
void MetalRenderManager::MergeSteps(MetalCommandChunk &chunk) {
|
|
std::vector<void *> touched;
|
|
for (size_t i = 0; i < rtStepCount_; i++) {
|
|
MetalStep &si = rtSteps_[i];
|
|
if (!si.isPass || si.merged || si.targetIsSwapchain) {
|
|
continue;
|
|
}
|
|
touched.clear();
|
|
for (size_t j = i + 1; j < rtStepCount_; j++) {
|
|
MetalStep &sj = rtSteps_[j];
|
|
if (sj.merged) {
|
|
continue;
|
|
}
|
|
const bool sameTarget = sj.isPass && sj.target == si.target && sj.targetDepth == si.targetDepth;
|
|
// Anything else reading this framebuffer needs it as it is at this point.
|
|
if (!sameTarget && (Contains(sj.reads, si.target) || Contains(sj.reads, si.targetDepth) || Contains(sj.reads, si.targetMsaa))) {
|
|
break;
|
|
}
|
|
if (sj.isPass) {
|
|
if (sameTarget) {
|
|
const MetalRecordedPass &rp = chunk.passes[sj.index];
|
|
const bool hasClear = rp.colorLoad == MTLLoadActionClear || rp.depthLoad == MTLLoadActionClear || rp.stencilLoad == MTLLoadActionClear;
|
|
bool readsTouched = false;
|
|
for (void *t : touched) {
|
|
if (Contains(sj.reads, t)) {
|
|
readsTouched = true;
|
|
break;
|
|
}
|
|
}
|
|
if (hasClear || readsTouched) {
|
|
break;
|
|
}
|
|
si.ranges.insert(si.ranges.end(), sj.ranges.begin(), sj.ranges.end());
|
|
si.usesDepth |= sj.usesDepth;
|
|
sj.merged = true;
|
|
} else {
|
|
touched.push_back(sj.target);
|
|
if (sj.targetDepth) {
|
|
touched.push_back(sj.targetDepth);
|
|
}
|
|
}
|
|
} else {
|
|
if (sj.target == si.target || (si.targetDepth && sj.target == si.targetDepth) || (si.targetMsaa && sj.target == si.targetMsaa)) {
|
|
// The copy overwrites the framebuffer.
|
|
break;
|
|
}
|
|
touched.push_back(sj.target);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void MetalRenderManager::ReplayPass(const MetalCommandChunk &chunk, MetalStep &step, id<MTLCommandBuffer> cmd) {
|
|
if (rtBlit_) {
|
|
[rtBlit_ endEncoding];
|
|
rtBlit_ = nil;
|
|
}
|
|
const MetalRecordedPass &pass = chunk.passes[step.index];
|
|
id<MTLTexture> color = pass.swapchain ? ReplaySwapchainTexture(pass.swapchain) : (__bridge id<MTLTexture>)pass.color;
|
|
if (pass.swapchain && !color) {
|
|
// No drawable (the window isn't visible?). Drop the pass.
|
|
return;
|
|
}
|
|
MTLRenderPassDescriptor *rp = [MTLRenderPassDescriptor renderPassDescriptor];
|
|
if (color) {
|
|
id<MTLTexture> msaaColor = (__bridge id<MTLTexture>)pass.msaaColor;
|
|
rp.colorAttachments[0].loadAction = pass.colorLoad;
|
|
rp.colorAttachments[0].clearColor = pass.clearColor;
|
|
if (msaaColor) {
|
|
rp.colorAttachments[0].texture = msaaColor;
|
|
rp.colorAttachments[0].resolveTexture = color;
|
|
rp.colorAttachments[0].storeAction = MTLStoreActionStoreAndMultisampleResolve;
|
|
} else {
|
|
rp.colorAttachments[0].texture = color;
|
|
rp.colorAttachments[0].storeAction = MTLStoreActionStore;
|
|
}
|
|
}
|
|
id<MTLTexture> depth = (__bridge id<MTLTexture>)pass.depth;
|
|
if (depth) {
|
|
MTLLoadAction depthLoad = pass.depthLoad;
|
|
MTLLoadAction stencilLoad = pass.stencilLoad;
|
|
MTLStoreAction store = MTLStoreActionStore;
|
|
if (pass.depthMemoryless) {
|
|
// Can't load or store memoryless attachments.
|
|
if (depthLoad == MTLLoadActionLoad)
|
|
depthLoad = MTLLoadActionClear;
|
|
if (stencilLoad == MTLLoadActionLoad)
|
|
stencilLoad = MTLLoadActionClear;
|
|
store = MTLStoreActionDontCare;
|
|
} else if (!step.usesDepth && depthLoad != MTLLoadActionClear && stencilLoad != MTLLoadActionClear) {
|
|
// Nothing in the pass touches depth or stencil, so leave the memory as it is. The Vulkan backend
|
|
// doesn't even attach depth then. Saves bandwidth, especially on tilers.
|
|
depthLoad = MTLLoadActionDontCare;
|
|
stencilLoad = MTLLoadActionDontCare;
|
|
store = MTLStoreActionDontCare;
|
|
}
|
|
id<MTLTexture> msaaDepth = (__bridge id<MTLTexture>)pass.msaaDepth;
|
|
id<MTLTexture> drawDepth = depth;
|
|
if (msaaDepth) {
|
|
// Resolve to sample 0, like the Vulkan backend.
|
|
drawDepth = msaaDepth;
|
|
if (store == MTLStoreActionStore) {
|
|
store = MTLStoreActionStoreAndMultisampleResolve;
|
|
rp.depthAttachment.resolveTexture = depth;
|
|
rp.depthAttachment.depthResolveFilter = MTLMultisampleDepthResolveFilterSample0;
|
|
}
|
|
}
|
|
rp.depthAttachment.texture = drawDepth;
|
|
rp.depthAttachment.loadAction = depthLoad;
|
|
rp.depthAttachment.clearDepth = pass.clearDepth;
|
|
rp.depthAttachment.storeAction = store;
|
|
if (depth.pixelFormat == MTLPixelFormatDepth32Float_Stencil8) {
|
|
rp.stencilAttachment.texture = drawDepth;
|
|
rp.stencilAttachment.loadAction = stencilLoad;
|
|
rp.stencilAttachment.clearStencil = pass.clearStencil;
|
|
rp.stencilAttachment.storeAction = store;
|
|
if (store == MTLStoreActionStoreAndMultisampleResolve) {
|
|
rp.stencilAttachment.resolveTexture = depth;
|
|
rp.stencilAttachment.stencilResolveFilter = MTLMultisampleStencilResolveFilterSample0;
|
|
}
|
|
}
|
|
}
|
|
id<MTLRenderCommandEncoder> enc = [cmd renderCommandEncoderWithDescriptor:rp];
|
|
if (!pass.tag.empty()) {
|
|
enc.label = [NSString stringWithUTF8String:pass.tag.c_str()];
|
|
}
|
|
|
|
const std::vector<MetalCommand> &commands = chunk.commands;
|
|
// Draws with a pipeline that failed to compile are dropped.
|
|
bool pipelineOK = false;
|
|
for (const auto &range : step.ranges) {
|
|
for (uint32_t i = range.first; i < range.second; i++) {
|
|
const MetalCommand &c = commands[i];
|
|
switch (c.cmd) {
|
|
case MetalCmd::PIPELINE:
|
|
{
|
|
id<MTLRenderPipelineState> pso = [(__bridge MetalPipelineState *)c.bind.obj wait];
|
|
pipelineOK = pso != nil;
|
|
if (pso) {
|
|
[enc setRenderPipelineState:pso];
|
|
}
|
|
break;
|
|
}
|
|
case MetalCmd::DEPTH_STENCIL: [enc setDepthStencilState:(__bridge id<MTLDepthStencilState>)c.bind.obj]; break;
|
|
case MetalCmd::STENCIL_REF: [enc setStencilReferenceValue:c.value]; break;
|
|
case MetalCmd::BLEND_COLOR: [enc setBlendColorRed:c.color[0] green:c.color[1] blue:c.color[2] alpha:c.color[3]]; break;
|
|
case MetalCmd::CULL: [enc setCullMode:(MTLCullMode)c.value]; break;
|
|
case MetalCmd::WINDING: [enc setFrontFacingWinding:(MTLWinding)c.value]; break;
|
|
case MetalCmd::DEPTH_CLIP: [enc setDepthClipMode:(MTLDepthClipMode)c.value]; break;
|
|
case MetalCmd::VIEWPORT: [enc setViewport:c.viewport]; break;
|
|
case MetalCmd::SCISSOR: [enc setScissorRect:c.scissor]; break;
|
|
case MetalCmd::VERTEX_BUFFER: [enc setVertexBuffer:(__bridge id<MTLBuffer>)c.bind.obj offset:c.bind.offset atIndex:c.index]; break;
|
|
case MetalCmd::VERTEX_BUFFER_OFFSET: [enc setVertexBufferOffset:c.bind.offset atIndex:c.index]; break;
|
|
case MetalCmd::FRAGMENT_BUFFER: [enc setFragmentBuffer:(__bridge id<MTLBuffer>)c.bind.obj offset:c.bind.offset atIndex:c.index]; break;
|
|
case MetalCmd::FRAGMENT_BUFFER_OFFSET: [enc setFragmentBufferOffset:c.bind.offset atIndex:c.index]; break;
|
|
case MetalCmd::FRAGMENT_TEXTURE: [enc setFragmentTexture:(__bridge id<MTLTexture>)c.bind.obj atIndex:c.index]; break;
|
|
case MetalCmd::FRAGMENT_SAMPLER: [enc setFragmentSamplerState:(__bridge id<MTLSamplerState>)c.bind.obj atIndex:c.index]; break;
|
|
case MetalCmd::DRAW: if (pipelineOK) [enc drawPrimitives:(MTLPrimitiveType)c.prim vertexStart:c.draw.start vertexCount:c.draw.count]; break;
|
|
case MetalCmd::DRAW_INDEXED:
|
|
if (pipelineOK) [enc drawIndexedPrimitives:(MTLPrimitiveType)c.prim indexCount:c.drawIndexed.count indexType:MTLIndexTypeUInt16
|
|
indexBuffer:(__bridge id<MTLBuffer>)c.drawIndexed.buffer indexBufferOffset:c.drawIndexed.offset];
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
[enc endEncoding];
|
|
}
|
|
|
|
void MetalRenderManager::ReplayCopy(const MetalCommandChunk &chunk, const MetalRecordedCopy ©, id<MTLCommandBuffer> cmd, bool upload) {
|
|
id<MTLBlitCommandEncoder> blit;
|
|
if (upload) {
|
|
if (!rtUploadBlit_) {
|
|
rtUploadBlit_ = [cmd blitCommandEncoder];
|
|
rtUploadBlit_.label = @"upload";
|
|
}
|
|
blit = rtUploadBlit_;
|
|
} else {
|
|
if (!rtBlit_) {
|
|
rtBlit_ = [cmd blitCommandEncoder];
|
|
}
|
|
blit = rtBlit_;
|
|
}
|
|
switch (copy.type) {
|
|
case MetalCopyType::TEXTURE_TO_TEXTURE:
|
|
[blit copyFromTexture:(__bridge id<MTLTexture>)copy.src sourceSlice:0 sourceLevel:copy.srcLevel sourceOrigin:copy.srcOrigin sourceSize:copy.size
|
|
toTexture:(__bridge id<MTLTexture>)copy.dst destinationSlice:0 destinationLevel:copy.dstLevel destinationOrigin:copy.dstOrigin];
|
|
break;
|
|
case MetalCopyType::TEXTURE_TO_BUFFER:
|
|
{
|
|
id<MTLTexture> src = copy.srcSwapchain ? ReplaySwapchainTexture(copy.srcSwapchain) : (__bridge id<MTLTexture>)copy.src;
|
|
if (!src) {
|
|
break;
|
|
}
|
|
[blit copyFromTexture:src sourceSlice:0 sourceLevel:copy.srcLevel sourceOrigin:copy.srcOrigin sourceSize:copy.size
|
|
toBuffer:(__bridge id<MTLBuffer>)copy.dst destinationOffset:copy.dstOffset destinationBytesPerRow:copy.bytesPerRow destinationBytesPerImage:copy.bytesPerImage options:copy.options];
|
|
break;
|
|
}
|
|
case MetalCopyType::BUFFER_TO_TEXTURE:
|
|
[blit copyFromBuffer:(__bridge id<MTLBuffer>)copy.src sourceOffset:copy.srcOffset sourceBytesPerRow:copy.bytesPerRow sourceBytesPerImage:copy.bytesPerImage sourceSize:copy.size
|
|
toTexture:(__bridge id<MTLTexture>)copy.dst destinationSlice:copy.dstSlice destinationLevel:copy.dstLevel destinationOrigin:copy.dstOrigin options:copy.options];
|
|
break;
|
|
case MetalCopyType::GENERATE_MIPS:
|
|
[blit generateMipmapsForTexture:(__bridge id<MTLTexture>)copy.dst];
|
|
break;
|
|
}
|
|
}
|
|
|
|
static char g_arrayViewKey;
|
|
|
|
id<MTLTexture> MetalRenderManager::ArrayView(id<MTLTexture> texture) {
|
|
if (!texture || texture.textureType == MTLTextureType2DArray || texture.textureType == MTLTextureType2DMultisampleArray) {
|
|
return texture;
|
|
}
|
|
id<MTLTexture> view = objc_getAssociatedObject(texture, &g_arrayViewKey);
|
|
if (!view) {
|
|
const MTLTextureType type = texture.textureType == MTLTextureType2DMultisample ? MTLTextureType2DMultisampleArray : MTLTextureType2DArray;
|
|
view = [texture newTextureViewWithPixelFormat:texture.pixelFormat textureType:type levels:NSMakeRange(0, texture.mipmapLevelCount) slices:NSMakeRange(0, 1)];
|
|
objc_setAssociatedObject(texture, &g_arrayViewKey, view, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
}
|
|
return view;
|
|
}
|