Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ namespace {
HybridResizer::HybridResizer(const ResizerOptions& options) : HybridObject(TAG), _pipeline(std::make_unique<vulkan::VulkanResizerPipeline>(options)) {}

std::shared_ptr<HybridGPUFrameSpec> HybridResizer::resize(const std::shared_ptr<camera::HybridFrameSpec>& frame) {
std::lock_guard<std::mutex> lock(_lifecycleMutex);

if (_pipeline == nullptr) [[unlikely]] {
throw std::runtime_error("This Resizer has already been disposed!");
}
Expand All @@ -59,6 +61,8 @@ std::shared_ptr<HybridGPUFrameSpec> HybridResizer::resize(const std::shared_ptr<
}

void HybridResizer::dispose() {
std::lock_guard<std::mutex> lock(_lifecycleMutex);

if (_pipeline == nullptr) {
return;
}
Expand All @@ -71,6 +75,10 @@ void HybridResizer::dispose() {
}

size_t HybridResizer::getExternalMemorySize() noexcept {
// Also guarded: Nitro calls this from toObject() on arbitrary threads, and it is never called
// from inside resize()/dispose(), so it cannot self-deadlock.
std::lock_guard<std::mutex> lock(_lifecycleMutex);

return _pipeline != nullptr ? _pipeline->getOutputBufferAllocationSize() : 0;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "vulkan/VulkanResizerPipeline.hpp"

#include <memory>
#include <mutex>

namespace margelo::nitro::camera::resizer {

Expand All @@ -28,6 +29,11 @@ class HybridResizer final : public HybridResizerSpec {
size_t getExternalMemorySize() noexcept override;

private:
// Serializes resize() against dispose(): dispose() can run on the JS thread while resize() is
// still running on the frame-processor thread, and resetting _pipeline mid-run() tears down the
// VkDevice underneath it. A dispose landing mid-frame blocks until that resize returns; every
// later call throws the catchable "already been disposed" error.
std::mutex _lifecycleMutex;
std::unique_ptr<vulkan::VulkanResizerPipeline> _pipeline;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import VisionCamera

/// High-level iOS resizer that turns camera frames into GPU-backed JS-visible output frames.
final class HybridResizer: HybridResizerSpec {
/// Serializes `resize()` against `dispose()`: `pipeline` is read on the frame-processor thread
/// while `dispose()` clears it on the JS thread, and a Swift class-reference load/store is not
/// atomic - `pipeline = nil` racing the `guard let` load is a data race on the possibly-last
/// reference.
private let lifecycleLock = NSLock()
private var pipeline: MetalResizerPipeline?

init(options: ResizerOptions) throws {
Expand All @@ -20,14 +25,20 @@ final class HybridResizer: HybridResizerSpec {
}

var memorySize: Int {
lifecycleLock.lock()
defer { lifecycleLock.unlock() }
return pipeline?.outputByteCount ?? 0
}

func dispose() {
lifecycleLock.lock()
defer { lifecycleLock.unlock() }
pipeline = nil
}

func resize(frame: any HybridFrameSpec) throws -> any HybridGPUFrameSpec {
lifecycleLock.lock()
defer { lifecycleLock.unlock() }
guard let pipeline else {
throw RuntimeError.error(withMessage: "This Resizer has already been disposed!")
}
Expand Down