/* Copyright (c) 2015-2026 The Khronos Group Inc.
 * Copyright (c) 2015-2026 Valve Corporation
 * Copyright (c) 2015-2026 LunarG, Inc.
 * Copyright (C) 2015-2024 Google Inc.
 * Modifications Copyright (C) 2020 Advanced Micro Devices, Inc. All rights reserved.
 *
 * 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 "state_tracker/semaphore_state.h"
#include "state_tracker/queue_state.h"
#include "state_tracker/state_tracker.h"
#include "state_tracker/wsi_state.h"
#include "utils/math_utils.h"
#include "containers/container_utils.h"

#ifdef VK_USE_PLATFORM_METAL_EXT
static bool GetMetalExport(const VkSemaphoreCreateInfo* info) {
    auto export_metal_object_info = vku::FindStructInPNextChain<VkExportMetalObjectCreateInfoEXT>(info->pNext);
    while (export_metal_object_info) {
        if (export_metal_object_info->exportObjectType == VK_EXPORT_METAL_OBJECT_TYPE_METAL_SHARED_EVENT_BIT_EXT) {
            return true;
        }
        export_metal_object_info = vku::FindStructInPNextChain<VkExportMetalObjectCreateInfoEXT>(export_metal_object_info->pNext);
    }
    return false;
}
#endif  // VK_USE_PLATFORM_METAL_EXT

static bool CanSignalBinarySemaphoreAfterOperation(vvl::Semaphore::OpType op_type) {
    return op_type == vvl::Semaphore::kNone || op_type == vvl::Semaphore::kWait;
}

static bool CanWaitBinarySemaphoreAfterOperation(vvl::Semaphore::OpType op_type) { return op_type == vvl::Semaphore::kSignal; }

static VkExternalSemaphoreHandleTypeFlags GetExportHandleTypes(const VkSemaphoreCreateInfo* pCreateInfo) {
    auto export_info = vku::FindStructInPNextChain<VkExportSemaphoreCreateInfo>(pCreateInfo->pNext);
    return export_info ? export_info->handleTypes : 0;
}

void vvl::Semaphore::TimePoint::Notify() const {
    assert(signal_submit.has_value() && signal_submit->queue);
    signal_submit->queue->Notify(signal_submit->seq);
}

vvl::Semaphore::Semaphore(DeviceState& device, VkSemaphore handle, const VkSemaphoreTypeCreateInfo* type_create_info,
                          const VkSemaphoreCreateInfo* pCreateInfo)
    : RefcountedStateObject(handle, kVulkanObjectTypeSemaphore),
      type(type_create_info ? type_create_info->semaphoreType : VK_SEMAPHORE_TYPE_BINARY),
      flags(pCreateInfo->flags),
      export_handle_types(GetExportHandleTypes(pCreateInfo)),
      initial_value(type == VK_SEMAPHORE_TYPE_TIMELINE ? type_create_info->initialValue : 0),
#ifdef VK_USE_PLATFORM_METAL_EXT
      metal_semaphore_export(GetMetalExport(pCreateInfo)),
#endif  // VK_USE_PLATFORM_METAL_EXT
      device_(device),
      current_payload_(type_create_info ? type_create_info->initialValue : 0),
      next_binary_payload_(current_payload_ + 1),
      completed_op_(type == VK_SEMAPHORE_TYPE_TIMELINE ? kSignal : kNone),
      completed_payload_(current_payload_) {
}

const VulkanTypedHandle* vvl::Semaphore::InUse() const {
    auto guard = ReadLock();
    // Semaphore does not have a parent (in the sense of VVL state object hierarchy).
    // The typed handle returned by the base class InUse is not useful for reporting
    // (it is the semaphore's own handle). Use it only to get boolean in use status.
    const bool in_use = RefcountedStateObject::InUse() != nullptr;
    if (!in_use) {
        return nullptr;
    }
    // Scan timeline to find the first queue that uses the semaphore
    for (const auto& [_, timepoint] : timeline_) {
        if (timepoint.signal_submit.has_value() && timepoint.signal_submit->queue) {
            return &timepoint.signal_submit->queue->Handle();
        } else {
            for (const SubmissionReference& wait_submit : timepoint.wait_submits) {
                if (wait_submit.queue) {
                    return &wait_submit.queue->Handle();
                }
            }
        }
    }
    // NOTE: In current implementation timepoints represent pending state. In-use tracking
    // can retire timepoint even if submission is still pending, so timeline_ state it's
    // always pending state but empty timeline does not mean there is no pending state.
    // We don't make stronger guarantees because it's enough for in-use tracking.
    // You can use NegativeSyncObject.TimelineSubmitSignalAndInUseTracking to check for
    // a scenario when there is pending submission and timeline is empty.
    //
    // This should be taken into account when semaphore is used by functionality other than
    // in-use tracking. In the following code we check completed_ state in case pending queue
    // cannot be derived from timeline_. It's a bit unconventional. Maybe we need better
    // separation between in-use tracking on other type of functionality. Or maybe it's about
    // better definitions.
    if (completed_queue_) {
        return &completed_queue_->Handle();
    }
    assert(false && "Can't find queue that uses the semaphore");
    static const VulkanTypedHandle empty{};
    return &empty;
}

enum vvl::Semaphore::Scope vvl::Semaphore::Scope() const {
    auto guard = ReadLock();
    return scope_;
}

void vvl::Semaphore::EnqueueSignal(const SubmissionReference& signal_submit, uint64_t& payload) {
    auto guard = WriteLock();
    if (type == VK_SEMAPHORE_TYPE_BINARY) {
        payload = next_binary_payload_++;
    } else {
        assert(type == VK_SEMAPHORE_TYPE_TIMELINE);
        // Host signal (vkSignalSemaphore) updates payload immediately.
        // This also handles vkLatencySleepNV external signal.
        if (signal_submit.queue == nullptr) {
            // Non-increasing signal value, do not track it. Validation phase already reported this.
            if (payload <= current_payload_) {
                return;
            }
            current_payload_ = payload;
        }

        // Track smallest pending signal
        if (signal_submit.queue != nullptr) {
            if (!smallest_pending_signal_value_.has_value() || payload < *smallest_pending_signal_value_) {
                smallest_pending_signal_value_ = payload;
            }
        }
    }

    timeline_[payload].signal_submit.emplace(signal_submit);
}

void vvl::Semaphore::EnqueueWait(const SubmissionReference& wait_submit, uint64_t& payload) {
    auto guard = WriteLock();
    if (type == VK_SEMAPHORE_TYPE_BINARY) {
        // Update the swapchain image acquire state if the semaphore was used by the acquire operation
        if (acquired_image_swapchain_ && !acquired_image_swapchain_->Destroyed()) {
            assert(acquired_image_index_ != vvl::kNoIndex32);
            acquired_image_swapchain_->images[acquired_image_index_].acquire_semaphore_status = AcquireSyncStatus::WasWaitedOn;
            acquired_image_swapchain_.reset();
            acquired_image_index_ = vvl::kNoIndex32;
        }
        // Get payload value
        if (timeline_.empty()) {
            if (scope_ != vvl::Semaphore::kInternal) {
                // for external semaphore mark wait as completed, no guarantee of signal visibility
                completed_op_ = kWait;
                completed_queue_ = wait_submit.queue;
                completed_payload_ = 0;
                current_payload_ = 0;
                return;
            } else {
                // generate binary payload value from the last completed signals
                assert(completed_op_ == kSignal);
                payload = completed_payload_;
            }
        } else {
            // generate binary payload value from the most recent pending binary signal
            assert(timeline_.rbegin()->second.HasSignaler());
            payload = timeline_.rbegin()->first;
        }
    }

    if (payload <= completed_payload_) {
        // Signal is already retired and its timepoint removed. Mark wait as completed.
        // NOTE: wait's submission can still be pending, but timepoint lifetime logic
        // is determined by the signal. completed_ is updated when signal is retired.
        // The matching waits should be resolved against completed_ in this case.
        assert(!vvl::Contains(timeline_, payload));
        completed_op_ = kWait;
        completed_queue_ = wait_submit.queue;
        return;
    }

    timeline_[payload].wait_submits.emplace_back(wait_submit);
}

void vvl::Semaphore::EnqueueAcquire(vvl::Func acquire_command) {
    assert(type == VK_SEMAPHORE_TYPE_BINARY);
    auto guard = WriteLock();
    uint64_t payload = next_binary_payload_++;
    assert(timeline_.find(payload) == timeline_.end());
    timeline_[payload].acquire_command.emplace(acquire_command);
}

std::optional<uint64_t> vvl::Semaphore::CheckMaxDiffThreshold(uint64_t value, const char*& payload_type) const {
    assert(type == VK_SEMAPHORE_TYPE_TIMELINE);
    auto guard = ReadLock();

    const uint64_t max_diff = device_.phys_dev_props_core12.maxTimelineSemaphoreValueDifference;

    // Check the current payload
    if (AbsDiff(value, current_payload_) > max_diff) {
        payload_type = "current";
        return current_payload_;
    }

    // It is enough to check only the first (smallest payload) and last (largest payload) entries.
    // If either exceeds the threshold, it must be a pending operation, since the current payload
    // has already been checked. One of these entries may equal the current payload (e.g., for
    // host signals), but in that case it will not exceed the threshold.
    if (!timeline_.empty()) {
        const auto& [first_payload, first_timepoint] = *timeline_.begin();
        if (AbsDiff(value, first_payload) > max_diff) {
            payload_type = first_timepoint.HasWaiters() ? "pending wait" : "pending signal";
            return first_payload;
        }
        const auto& [last_payload, last_timepoint] = *timeline_.rbegin();
        if (AbsDiff(value, last_payload) > max_diff) {
            payload_type = last_timepoint.HasWaiters() ? "pending wait" : "pending signal";
            return last_payload;
        }
    }
    return {};
}

bool vvl::Semaphore::HasPendingTimelineSignal(uint64_t signal_value) const {
    assert(type == VK_SEMAPHORE_TYPE_TIMELINE);
    auto guard = ReadLock();

    auto it = timeline_.find(signal_value);
    if (it == timeline_.end()) {
        return false;
    }
    const TimePoint& timepoint = it->second;
    if (!timepoint.signal_submit.has_value()) {
        return false;
    }
    const bool pending = timepoint.signal_submit->queue != nullptr;
    return pending;
}

std::optional<uint64_t> vvl::Semaphore::GetSmallestPendingTimelineSignal() const {
    assert(type == VK_SEMAPHORE_TYPE_TIMELINE);
    auto guard = ReadLock();
    return smallest_pending_signal_value_;
}

std::optional<vvl::SubmissionReference> vvl::Semaphore::GetPendingBinarySignalSubmission() const {
    assert(type == VK_SEMAPHORE_TYPE_BINARY);
    auto guard = ReadLock();
    if (timeline_.empty()) {
        return {};
    }
    const TimePoint& timepoint = timeline_.rbegin()->second;
    assert(timepoint.HasSignaler());  // semaphore was signaled or acquired

    if (!timepoint.signal_submit.has_value()) {
        assert(timepoint.acquire_command.has_value());
        return {};
    }
    assert(timepoint.signal_submit->queue != nullptr);  // binary semaphore can't be signaled from the host
    return timepoint.signal_submit;
}

std::optional<vvl::SubmissionReference> vvl::Semaphore::GetPendingBinaryWaitSubmission() const {
    assert(type == VK_SEMAPHORE_TYPE_BINARY);
    auto guard = ReadLock();
    if (timeline_.empty()) {
        return {};
    }
    const auto& timepoint = timeline_.rbegin()->second;
    assert(timepoint.wait_submits.empty() || timepoint.wait_submits.size() == 1);

    // No waits
    if (timepoint.wait_submits.empty()) {
        return {};
    }
    // Skip waits that are not associated with a queue
    if (timepoint.wait_submits[0].queue == nullptr) {
        return {};
    }
    return timepoint.wait_submits[0];
}

std::optional<vvl::SemaphoreInfo> vvl::Semaphore::GetPendingBinarySignalTimelineDependency() const {
    assert(type == VK_SEMAPHORE_TYPE_BINARY);
    auto guard = ReadLock();
    if (timeline_.empty()) {
        return {};
    }
    const TimePoint& timepoint = timeline_.rbegin()->second;
    assert(timepoint.HasSignaler());
    const auto& signal_submit = timepoint.signal_submit;

    // A signal not associated with a queue cannot be blocked by timeline wait
    // (host signal or image acquire signal)
    if (!signal_submit.has_value() || signal_submit->queue == nullptr) {
        return {};
    }

    return signal_submit->queue->FindTimelineWaitWithoutResolvingSignal(signal_submit->seq);
}

uint64_t vvl::Semaphore::CurrentPayload() const {
    auto guard = ReadLock();
    return current_payload_;
}

bool vvl::Semaphore::CanBinaryBeSignaled() const {
    assert(type == VK_SEMAPHORE_TYPE_BINARY);
    auto guard = ReadLock();
    if (timeline_.empty()) {
        return CanSignalBinarySemaphoreAfterOperation(completed_op_);
    }
    // Every timeline slot of binary semaphore should contain at least a signal.
    // Wait before signal is not allowed.
    assert(timeline_.rbegin()->second.HasSignaler());

    return timeline_.rbegin()->second.HasWaiters();
}

bool vvl::Semaphore::CanBinaryBeWaited() const {
    assert(type == VK_SEMAPHORE_TYPE_BINARY);
    auto guard = ReadLock();
    if (timeline_.empty()) {
        return CanWaitBinarySemaphoreAfterOperation(completed_op_);
    }

    const TimePoint& timepoint = timeline_.rbegin()->second;

    assert(scope_ == vvl::Semaphore::kInternal);  // Ensured by all calling sites

    // Every timeline slot of binary semaphore should contain at least a signal.
    // Wait before signal is not allowed.
    assert(timepoint.HasSignaler());

    // Can wait if there are no waiters
    return !timepoint.HasWaiters();
}

void vvl::Semaphore::GetLastBinarySignalSource(VkQueue& queue, vvl::Func& acquire_command) const {
    assert(type == VK_SEMAPHORE_TYPE_BINARY);
    queue = VK_NULL_HANDLE;
    acquire_command = vvl::Func::Empty;

    auto guard = ReadLock();
    if (timeline_.empty()) {
        if (completed_op_ == kSignal && completed_queue_) {
            queue = completed_queue_->VkHandle();
        }
    } else {
        const TimePoint& timepoint = timeline_.rbegin()->second;
        if (timepoint.signal_submit.has_value() && timepoint.signal_submit->queue) {
            queue = timepoint.signal_submit->queue->VkHandle();
        } else if (timepoint.acquire_command.has_value()) {
            acquire_command = *timepoint.acquire_command;
        }
    }
}

bool vvl::Semaphore::HasResolvingTimelineSignal(uint64_t wait_payload) const {
    assert(type == VK_SEMAPHORE_TYPE_TIMELINE);
    auto guard = ReadLock();

    // For external semaphore we can't track the signal.
    // In theory, it can be context-dependent whether to assume the external signal is
    // available or not in order to have false positive free validation. Assert that
    // this function is only called for regular semaphores and it is up to the caller
    // to handle external case.
    assert(scope_ == vvl::Semaphore::kInternal);

    // Check if completed payload value (which includes initial value) resolves the wait.
    if (wait_payload <= completed_payload_) {
        return true;
    }

    auto it = timeline_.find(wait_payload);
    assert(it != timeline_.end());  // for each registered wait there is a timepoint
    while (it != timeline_.end()) {
        if (it->second.signal_submit.has_value()) {
            assert(it->first >= wait_payload);  // timepoints are ordered in increasing order
            return true;
        }
        ++it;
    }
    return false;
}

bool vvl::Semaphore::CanRetireBinaryWait(TimePoint& timepoint) const {
    assert(type == VK_SEMAPHORE_TYPE_BINARY);
    // The only allowed configuration when binary semaphore wait does not have a signal
    // is external semaphore. Just retire the wait because there is no guarantee we can
    // track the signal.
    if (!timepoint.signal_submit.has_value()) {
        assert(scope_ != kInternal);
        return true;
    }

    // The resolving signal can only be on another queue (the earlier signals on the
    // current queue are already processed and corresponding timepoints are retired).
    // Initiate forward progress on signaling queue and ask the caller to wait.
    timepoint.Notify();
    return false;
}

bool vvl::Semaphore::CanRetireTimelineWait(const vvl::Queue* current_queue, uint64_t payload) const {
    assert(type == VK_SEMAPHORE_TYPE_TIMELINE);

    // In the correct program the resolving signal is the next signal on the timeline,
    // otherwise this violates the rule of strictly increasing signal values.
    auto it = timeline_.find(payload);
    assert(it != timeline_.end());
    for (; it != timeline_.end(); ++it) {
        const TimePoint& t = it->second;
        if (!t.signal_submit.has_value()) {
            continue;
        }
        // If the next signal is on the waiting (current) queue, it can't be a resolving signal (blocked by wait).
        // Submit time validation will also report an error about non-increasing signal values
        if (t.signal_submit->queue != nullptr && t.signal_submit->queue == current_queue) {
            continue;
        }
        // Found the resolving signal
        break;
    }

    // There is always a resolving signal when we reach a retirement phase (CPU successfully finished waiting on GPU).
    // For external semaphore we might not have visibility of this signal. Just retire the wait.
    if (it == timeline_.end()) {
        assert(scope_ != kInternal);
        return true;
    }

    // Found host signal that finishes this wait
    const TimePoint& t = it->second;
    if (t.signal_submit->queue == nullptr) {
        return true;
    }

    // Notify signaling queue and wait for its queue thread
    t.Notify();
    return false;
}

void vvl::Semaphore::RetireWait(vvl::Queue* current_queue, uint64_t payload, const Location& loc, bool queue_thread) {
    std::shared_future<void> waiter;
    bool retire_external_payload = false;
    uint64_t external_payload = 0;
    {
        auto guard = WriteLock();
        if (payload <= completed_payload_) {
            return;
        }
        if (scope_ != kInternal) {
            if (!vvl::Find(timeline_, payload)) {
                // GetSemaphoreCounterValue for external semaphore might not have a registered timepoint.
                // Add timepoint so we can retire timeline up to that point.
                assert(type == VK_SEMAPHORE_TYPE_TIMELINE);
                auto payload_it = timeline_.insert({payload, TimePoint{}}).first;

                // Search existing signal. If found, notify corresponding submission.
                // (external payload, which is already reached by the gpu, is larger than found signal,
                // this means that earlier signals were also processed, so we can retire them)
                for (auto it = std::make_reverse_iterator(payload_it); it != timeline_.rend(); ++it) {
                    const TimePoint& t = it->second;
                    if (t.signal_submit.has_value() && t.signal_submit->queue) {
                        retire_external_payload = true;
                        external_payload = payload;
                        // Update payload value to retire existing signal.
                        // External payload will be retired after that to update current payload value.
                        payload = it->first;
                        break;
                    }
                }
            }
            if (scope_ == kExternalTemporary) {
                scope_ = kInternal;
                imported_handle_type_.reset();
            }
        }
        TimePoint& timepoint = vvl::FindExisting(timeline_, payload);

        bool retire = false;
        if (timepoint.acquire_command) {
            retire = true;  // There is resolving acquire signal, timepoint can be retired
        } else if (type == VK_SEMAPHORE_TYPE_BINARY) {
            retire = CanRetireBinaryWait(timepoint);
        } else {
            retire = CanRetireTimelineWait(current_queue, payload);
        }
        if (retire) {
            RetireTimePoint(payload, kWait, current_queue);
            return;
        }

        // Wait for some other queue or a host operation to retire
        assert(timepoint.waiter.valid());
        // the current timepoint should get destroyed while we're waiting, so copy out the waiter.
        waiter = timepoint.waiter;
    }

    WaitTimePoint(std::move(waiter), payload, !queue_thread, loc);

    if (retire_external_payload) {
        auto guard = WriteLock();
        RetireTimePoint(external_payload, kWait, nullptr);
    }
}

void vvl::Semaphore::RetireSignal(uint64_t payload) {
    auto guard = WriteLock();
    if (payload <= completed_payload_) {
        return;
    }
    TimePoint& timepoint = vvl::FindExisting(timeline_, payload);
    assert(timepoint.signal_submit.has_value());

    OpType completed_op = kSignal;
    const Queue* completed_op_queue = timepoint.signal_submit->queue;

    // If there is a wait operation then mark it as the last completed instead.
    // The reason to do this here instead on the waiter side (after it is unblocked)
    // is because signal can have larger (timeline) value than corresponding wait value.
    // In this case it's the signal that defines the last completed value.
    if (!timepoint.wait_submits.empty()) {
        completed_op = kWait;
        // NOTE: for timeline semaphores there can be several waiters. Also for timeline
        // semaphores the queue of the completed operation is only used by Semaphore::InUse
        // for reporting purposes. We can choose any valid wait queue if there are multiple.
        completed_op_queue = timepoint.wait_submits[0].queue;
    }

    RetireTimePoint(payload, completed_op, completed_op_queue);
}

void vvl::Semaphore::RetireTimePoint(uint64_t payload, OpType completed_op, const Queue* completed_op_queue) {
    auto it = timeline_.begin();
    while (it != timeline_.end() && it->first <= payload) {
        assert(it->first > completed_payload_);
        it->second.completed.set_value();
        ++it;
    }
    timeline_.erase(timeline_.begin(), it);
    completed_op_ = completed_op;
    completed_payload_ = payload;
    completed_queue_ = completed_op_queue;

    // Update the current payload only if the given payload is larger.
    // vkSignalSemaphore updates the current payload immediately, so it can be
    // larger than the given payload from the most recently synchronized batch.
    if (payload > current_payload_) {
        current_payload_ = payload;
    }

    // Update smallest pending signal
    if (type == VK_SEMAPHORE_TYPE_TIMELINE) {
        if (timeline_.empty()) {
            smallest_pending_signal_value_.reset();
        } else if (smallest_pending_signal_value_.has_value() && timeline_.begin()->first > *smallest_pending_signal_value_) {
            smallest_pending_signal_value_.reset();
            for (const auto& [payload, timepoint] : timeline_) {
                if (!timepoint.signal_submit.has_value()) {
                    continue;
                }

                // host signals must always go before pending signals and at this point
                // we iterate over pending operations
                assert(timepoint.signal_submit->queue != nullptr);

                smallest_pending_signal_value_ = payload;
                break;
            }
        }
    }
}

void vvl::Semaphore::WaitTimePoint(std::shared_future<void>&& waiter, uint64_t payload, bool unblock_validation_object,
                                   const Location& loc) {
    if (unblock_validation_object) {
        device_.BeginBlockingOperation();
    }

    auto result = waiter.wait_until(GetCondWaitTimeout());

    if (unblock_validation_object) {
        device_.EndBlockingOperation();
    }

    if (result != std::future_status::ready) {
        device_.LogError(
            "INTERNAL-ERROR-VkSemaphore-state-timeout", Handle(), loc,
            "The Validation Layers hit a timeout waiting for timeline semaphore state to update. completed_.payload=%" PRIu64
            " wait_payload=%" PRIu64,
            completed_payload_, payload);
    }
}

void vvl::Semaphore::SetAcquiredImage(const std::shared_ptr<vvl::Swapchain>& swapchain, uint32_t image_index) {
    auto guard = WriteLock();
    acquired_image_swapchain_ = swapchain;
    acquired_image_index_ = image_index;
}

void vvl::Semaphore::SetSwapchainWaitInfo(const SwapchainWaitInfo& info) {
    auto guard = WriteLock();
    swapchain_wait_info_.emplace(info);
}

void vvl::Semaphore::ClearSwapchainWaitInfo() {
    auto guard = WriteLock();
    swapchain_wait_info_.reset();
}

std::optional<vvl::Semaphore::SwapchainWaitInfo> vvl::Semaphore::GetSwapchainWaitInfo() const {
    auto guard = ReadLock();
    // Return by value due to locking (not safe to access reference when unlocked)
    return swapchain_wait_info_;
}

void vvl::Semaphore::Import(VkExternalSemaphoreHandleTypeFlagBits handle_type, VkSemaphoreImportFlags flags) {
    auto guard = WriteLock();
    if (scope_ != kExternalPermanent) {
        if ((handle_type == VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT || flags & VK_SEMAPHORE_IMPORT_TEMPORARY_BIT) &&
            scope_ == kInternal) {
            scope_ = kExternalTemporary;
        } else {
            scope_ = kExternalPermanent;
        }
    }
    imported_handle_type_ = handle_type;
}

void vvl::Semaphore::Export(VkExternalSemaphoreHandleTypeFlagBits handle_type) {
    if (handle_type != VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT) {
        // Cannot track semaphore state once it is exported, except for Sync FD handle types which have copy transference
        auto guard = WriteLock();
        scope_ = kExternalPermanent;
    } else {
        assert(type == VK_SEMAPHORE_TYPE_BINARY);  // checked by validation phase
        // Exporting a semaphore payload to a handle with copy transference has the same side effects
        // on the source semaphore's payload as executing a semaphore wait operation
        if (std::optional<SubmissionReference> pending_signal_submit = GetPendingBinarySignalSubmission()) {
            uint64_t temp_payload;  // don't need output parameter
            EnqueueWait(*pending_signal_submit, temp_payload);
        } else {
            assert(completed_op_ == kSignal);  // checked by validation phase
            completed_op_ = kWait;
            completed_queue_ = nullptr;  // Export's wait is not associated with a queue
        }
    }
}

std::optional<VkExternalSemaphoreHandleTypeFlagBits> vvl::Semaphore::ImportedHandleType() const {
    auto guard = ReadLock();

    // Sanity check: semaphore imported -> scope is not internal
    assert(!imported_handle_type_.has_value() || scope_ != kInternal);

    return imported_handle_type_;
}
