Skip to content

File offload_rendering_client.cpp

File List > offload_rendering_client > offload_rendering_client.cpp

Go to the documentation of this file

#include "offload_rendering_client.hpp"

#ifdef USING_OPENXR
#    include "illixr/data_format/poses/combined_pose.hpp"
#endif
#ifdef __ANDROID__
#    include "android/jni_helper.hpp"
#    ifdef ILLIXR_ENABLE_BOBA
#        include "android/keyframe_gate.hpp"
#    endif
#else
#    include 
#    include 
#    include 
#    define OFFLOAD_RENDERING_FFMPEG_DECODER_NAME "hevc"
#endif

using namespace ILLIXR;
using namespace ILLIXR::data_format;
#ifdef __ANDROID__
static bool is_hevc_keyframe(const uint8_t* data, size_t size) {
    // Need at least 6 bytes: 4-byte start code + 2-byte NAL header
    if (size < 6)
        return false;

    // Skip the start code (00 00 00 01)
    const uint8_t* nal = data + 4;

    // NAL unit type = bits [9:15] of the 2-byte header
    // = (first_byte & 0x7E) >> 1
    uint8_t nal_type = (nal[0] & 0x7E) >> 1;

    // IDR_W_RADL = 19, IDR_N_LP = 20
    return nal_type == 19 || nal_type == 20;
}

#else
using namespace ILLIXR::vulkan::ffmpeg_utils;

NppStreamContext makeNppStreamContext(cudaStream_t stream = nullptr) {
    NppStreamContext ctx{};
    int              device = 0;
    cudaGetDevice(&device);
    cudaDeviceProp prop{};
    cudaGetDeviceProperties(&prop, device);

    ctx.hStream                            = stream;
    ctx.nCudaDeviceId                      = device;
    ctx.nMultiProcessorCount               = prop.multiProcessorCount;
    ctx.nMaxThreadsPerMultiProcessor       = prop.maxThreadsPerMultiProcessor;
    ctx.nMaxThreadsPerBlock                = prop.maxThreadsPerBlock;
    ctx.nSharedMemPerBlock                 = prop.sharedMemPerBlock;
    ctx.nCudaDevAttrComputeCapabilityMajor = prop.major;
    ctx.nCudaDevAttrComputeCapabilityMinor = prop.minor;
    return ctx;
}

const Npp8u* nv12_chroma_plane(const AVFrame* frame) {
    if (frame->data[1] != nullptr) {
        return reinterpret_cast<const Npp8u*>(frame->data[1]);
    }
    if (frame->data[0] == nullptr || frame->linesize[0] <= 0 || frame->height <= 0) {
        return nullptr;
    }
    return reinterpret_cast<const Npp8u*>(frame->data[0]) + static_cast<size_t>(frame->linesize[0]) * frame->height;
}

void assert_npp_success(NppStatus status, const char* operation, int eye, const AVFrame* src, const AVFrame* dst = nullptr) {
    if (status == NPP_SUCCESS) {
        return;
    }

    std::string message = std::string{operation} + " failed with NPP status " + std::to_string(static_cast<int>(status)) +
        " eye=" + std::to_string(eye);
    if (src != nullptr) {
        message += " src_format=" + std::to_string(src->format) + " src=" + std::to_string(src->width) + "x" +
            std::to_string(src->height) + " src_linesize0=" + std::to_string(src->linesize[0]) +
            " src_linesize1=" + std::to_string(src->linesize[1]) +
            " src_data0=" + std::to_string(reinterpret_cast<uintptr_t>(src->data[0])) +
            " src_data1=" + std::to_string(reinterpret_cast<uintptr_t>(src->data[1]));
    }
    if (dst != nullptr) {
        message += " dst=" + std::to_string(dst->width) + "x" + std::to_string(dst->height) +
            " dst_linesize0=" + std::to_string(dst->linesize[0]) +
            " dst_data0=" + std::to_string(reinterpret_cast<uintptr_t>(dst->data[0]));
    }
    throw std::runtime_error{message};
}
#endif

const int LOG_INTERVAL = 300; // pose logging interval in frames

offload_rendering_client::offload_rendering_client(const std::string& name, phonebook* pb)
    : threadloop{name, pb}
    , switchboard_{pb->lookup_impl<switchboard>()}
    , log_{spdlogger(nullptr)}
#ifdef __ANDROID__
    , frame_writer_{switchboard_->get_writer<data_format::dual_frames>("unity_rendered_frame")}
#else
    , display_provider_{pb->lookup_impl<vulkan::display_provider>()}
#endif
    , frames_reader_{switchboard_->get_buffered_reader<compressed_frame>("compressed_frames")}
    , network_latency_reader_{switchboard_->get_reader<network_latency_result>("network_latency")}
#ifdef ILLIXR_ENABLE_BOBA
    , modal_texture_reader_{switchboard_->get_buffered_reader<boba_modal_texture>("boba_modal_texture")}
#endif
#ifndef USING_OPENXR
    , pose_writer_{switchboard_->get_network_writer<pose::fast_head_pose_type>("render_pose", {})}
#endif
#ifndef USING_OPENXR
    , pose_prediction_{pb->lookup_impl<pose_prediction>()}
#endif
    , clock_{pb->lookup_impl<relative_clock>()}
#ifdef __ANDROID__
    , app_{switchboard_->get_android_app()} {
#else
{
    display_provider_ffmpeg = display_provider_;
#endif

#ifdef __ANDROID__
#    ifndef ILLIXR_ENABLE_BOBA
    overscan_ = switchboard_->get_env_double("ILLIXR_OVERSCAN", 1.0);

    headset_width_  = static_cast<int>(headset_width_ * overscan_);
    headset_height_ = static_cast<int>(headset_height_ * overscan_);
#    endif

    // Motion vectors are decoded through the Android MediaCodec path and also
    // require the depth-image path to be active.
    use_motion_vectors_ = switchboard_->get_env_bool("ILLIXR_USE_MOTION_VECTORS");
    if (use_motion_vectors_) {
        use_depth_ = true;
    } else {
        use_depth_ = switchboard_->get_env_bool("ILLIXR_USE_DEPTH_IMAGES");
    }
    log_->debug(use_motion_vectors_ ? "Encoding motion vector images for the client"
                                    : "Not encoding motion vector images for the client");
#else
    use_depth_ = switchboard_->get_env_bool("ILLIXR_USE_DEPTH_IMAGES");
#endif
    log_->debug(use_depth_ ? "Encoding depth images for the client" : "Not encoding depth images for the client");
}

#ifdef __ANDROID__
offload_rendering_client::~offload_rendering_client() {
    spdlog::get("illixr")->info("[offload_rendering_client] Shutting down, processed {} frames", frame_count_);

    // Stop the receiver thread before stopping decoders so it cannot submit
    // new encoded data after the decoders have been torn down.
    receiver_running_ = false;
    if (receiver_thread_.joinable()) {
        receiver_thread_.join();
    }

    // Release whatever in_flight_frames_ hadn't yet aged past
    // MAX_IN_FLIGHT_FRAMES.  AHardwareBuffer_release() doesn't depend on the
    // decoders' MediaCodec/AImageReader state, so this is safe to do before
    // stop() below.
    while (!in_flight_frames_.empty()) {
        if (color_decoder_) {
            color_decoder_->release_frame(in_flight_frames_.front());
        }
        in_flight_frames_.pop_front();
    }

    // Log final timing statistics
    if (color_decoder_) {
        auto color_stats = color_decoder_->get_timing_stats();
        log_->info("[offload_rendering_client] Final color decode stats: "
                   "left_avg={:.2f}ms, right_avg={:.2f}ms, total_frames={}",
                   color_stats.left_eye.avg_decode_time_us() / 1000.0, color_stats.right_eye.avg_decode_time_us() / 1000.0,
                   color_stats.total_frames());
        color_decoder_->stop();
    }
    if (depth_decoder_) {
        auto depth_stats = depth_decoder_->get_timing_stats();
        log_->info("[offload_rendering_client] Final depth decode stats: "
                   "left_avg={:.2f}ms, right_avg={:.2f}ms, total_frames={}",
                   depth_stats.left_eye.avg_decode_time_us() / 1000.0, depth_stats.right_eye.avg_decode_time_us() / 1000.0,
                   depth_stats.total_frames());
        depth_decoder_->stop();
    }
    if (motion_vec_decoder_) {
        auto mv_stats = motion_vec_decoder_->get_timing_stats();
        log_->info("[offload_rendering_client] Final MV decode stats: "
                   "left_avg={:.2f}ms, right_avg={:.2f}ms, total_frames={}",
                   mv_stats.left_eye.avg_decode_time_us() / 1000.0, mv_stats.right_eye.avg_decode_time_us() / 1000.0,
                   mv_stats.total_frames());
        motion_vec_decoder_->stop();
    }
}

void offload_rendering_client::log_android_decode_timing() {
    // Calculate averages from accumulated metrics
    if (frame_count_ == 0) {
        return;
    }

    // Get decoder statistics
    stereo_decode_timing_stats color_stats;
    stereo_decode_timing_stats depth_stats;
    stereo_decode_timing_stats mv_stats;

    // Color decoder stats
    if (color_decoder_) {
        color_stats = color_decoder_->get_and_reset_timing_stats();
        if (color_stats.left_eye.max_decode_latency_us > 0 || color_stats.right_eye.max_decode_latency_us > 0) {
            log_->info("  Color decode latency (left):  avg={:.2f}ms, min={:.2f}ms, max={:.2f}ms",
                       color_stats.left_eye.avg_decode_time_us() / 1000.0,
                       color_stats.left_eye.min_decode_latency_us == UINT64_MAX
                           ? 0
                           : static_cast<double>(color_stats.left_eye.min_decode_latency_us) / 1000.0,
                       static_cast<double>(color_stats.left_eye.max_decode_latency_us) / 1000.0);
#    ifndef COMBINED_ENCODING
            log_->info("  Color decode latency (right): avg={:.2f}ms, min={:.2f}ms, max={:.2f}ms",
                       color_stats.right_eye.avg_decode_time_us() / 1000.0,
                       color_stats.right_eye.min_decode_latency_us == UINT64_MAX
                           ? 0
                           : color_stats.right_eye.min_decode_latency_us / 1000.0,
                       color_stats.right_eye.max_decode_latency_us / 1000.0);
#    endif
        }
    }

    // Depth decoder stats
    if (use_depth_ && depth_decoder_) {
        depth_stats = depth_decoder_->get_and_reset_timing_stats();
        if (depth_stats.left_eye.avg_decode_time_us() > 0.) {
            log_->info("  Depth decode latency (left):  avg={:.2f}ms, min={:.2f}ms, max={:.2f}ms",
                       depth_stats.left_eye.avg_decode_time_us() / 1000.0,
                       depth_stats.left_eye.min_decode_latency_us == UINT64_MAX
                           ? 0
                           : static_cast<double>(depth_stats.left_eye.min_decode_latency_us) / 1000.0,
                       static_cast<double>(depth_stats.left_eye.max_decode_latency_us) / 1000.0);
        }
#    ifndef COMBINED_ENCODING
        if (depth_stats.right_eye.avg_decode_time_us() > 0.) {
            log_->info("  Depth decode latency (right): avg={:.2f}ms, min={:.2f}ms, max={:.2f}ms",
                       depth_stats.right_eye.avg_decode_time_us() / 1000.0,
                       depth_stats.right_eye.min_decode_latency_us == UINT64_MAX
                           ? 0
                           : depth_stats.right_eye.min_decode_latency_us / 1000.0,
                       depth_stats.right_eye.max_decode_latency_us / 1000.0);
        }
#    endif
    }

    if (use_motion_vectors_ && motion_vec_decoder_) {
        mv_stats = motion_vec_decoder_->get_and_reset_timing_stats();
        log_->info("  MV decode latency (left):  avg={:.2f}ms, min={:.2f}ms, max={:.2f}ms",
                   mv_stats.left_eye.avg_decode_time_us() / 1000.0,
                   mv_stats.left_eye.min_decode_latency_us == UINT64_MAX
                       ? 0
                       : static_cast<double>(mv_stats.left_eye.min_decode_latency_us) / 1000.0,
                   static_cast<double>(mv_stats.left_eye.max_decode_latency_us) / 1000.0);
#    ifndef COMBINED_ENCODING
        log_->info("  MV decode latency (right): avg={:.2f}ms, min={:.2f}ms, max={:.2f}ms",
                   mv_stats.right_eye.avg_decode_time_us() / 1000.0,
                   mv_stats.right_eye.min_decode_latency_us == UINT64_MAX ? 0
                                                                          : mv_stats.right_eye.min_decode_latency_us / 1000.0,
                   mv_stats.right_eye.max_decode_latency_us / 1000.0);
#    endif
    }

    // log_->info("==========================================");

    // Reset accumulated metrics
    // android_texture_update_time_us_ = 0;
    // android_total_frame_time_us_ = 0;
    // android_timing_frame_count_ = 0;
}

#    ifdef ILLIXR_ENABLE_BOBA
void offload_rendering_client::drain_modal_texture_updates() {
    while (auto update = modal_texture_reader_.try_dequeue()) {
        const std::uint64_t expected_bytes = static_cast<std::uint64_t>(update->width) * update->height * 4ULL;
        if (update->texture_id == 0 || update->magic != boba_modal_texture::wire_magic || update->width == 0 ||
            update->height == 0 || expected_bytes != update->rgba.size()) {
            log_->warn("[receiver_loop] Ignoring invalid Boba modal texture update");
            continue;
        }

        auto pixels = std::make_shared<const std::vector<std::uint8_t>>(update->rgba);
        {
            std::lock_guard<std::mutex> lock(frame_meta_map_mutex_);
            modal_texture_cache_[update->texture_id] = {update->width, update->height, std::move(pixels)};
            while (modal_texture_cache_.size() > 8) {
                auto stale = modal_texture_cache_.begin();
                if (stale->first == update->texture_id && modal_texture_cache_.size() > 1) {
                    ++stale;
                }
                modal_texture_cache_.erase(stale);
            }
        }
        log_->info("[receiver_loop] Cached Boba modal texture id={} size={}x{}", update->texture_id, update->width,
                   update->height);
    }
}
#    endif

// receiver_loop
// Runs on receiver_thread_. Dequeues compressed frames from the network,
// drops frames when the decoder input queue is full to prevent growing latency,
// queues encoded data to the hardware decoders (all three stream types together
// so they stay in sync), and stores frame metadata for _p_one_iteration.
// receiver_loop
// Runs on receiver_thread_. Dequeues compressed frames from the network,
// drops frames when the decoder input queue is full to prevent growing latency,
// queues encoded data to the hardware decoders (all three stream types together
// so they stay in sync), and stores frame metadata for _p_one_iteration.
void offload_rendering_client::receiver_loop() {
    // spdlog::get("illixr")->info("[receiver_loop] Starting");
#    ifdef ILLIXR_ENABLE_BOBA
    keyframe_gate input_gate;
    auto          receive_metrics_start = std::chrono::steady_clock::now();
    uint64_t      received_frames = 0, skipped_frames = 0;
#    endif

    while (receiver_running_) {
#    ifdef ILLIXR_ENABLE_BOBA
        drain_modal_texture_updates();
#    endif
        auto current_frame = frames_reader_.dequeue();
        if (current_frame == nullptr) {
            spdlog::get("illixr")->debug("[receiver_loop] No frame available");
            continue;
        }
#    ifdef ILLIXR_ENABLE_BOBA
        drain_modal_texture_updates();
#    endif

        // Determine keyframe status for each stream.
        //
        // Color: use the authoritative flag set by the server from
        // nvenc_encoder::last_frame_was_keyframe().  This works correctly for
        // both HEVC (IDR NAL) and AV1 (KEY_FRAME OBU) without any bitstream
        // parsing on the client side.
        //
        // Depth / motion-vector streams are always HEVC regardless of USE_AV1,
        // so they continue to use the NAL-unit scan.
        const bool is_key_color = current_frame->is_keyframe;

        const bool is_key_depth = (use_depth_ && !current_frame->left_depth.empty())
            ? is_hevc_keyframe(current_frame->left_depth.data(), current_frame->left_depth.size())
            : is_key_color;

        const bool is_key_mv = (use_motion_vectors_ && !current_frame->left_motion_vec.empty())
            ? is_hevc_keyframe(current_frame->left_motion_vec.data(), current_frame->left_motion_vec.size())
            : is_key_color;

#    ifdef ILLIXR_ENABLE_BOBA
        // After skipping an encoded input, every dependent frame is unusable
        // until the streams restart at a keyframe, even if the queue has drained.
        const bool queue_full = color_decoder_ &&
            (color_decoder_->get_left_queue_depth() >= MAX_DECODER_QUEUE_DEPTH ||
             color_decoder_->get_right_queue_depth() >= MAX_DECODER_QUEUE_DEPTH);
        const bool accept_input = input_gate.accept(is_key_color && is_key_depth && is_key_mv, queue_full);
        ++received_frames;
        skipped_frames += accept_input ? 0 : 1;
        const auto   now      = std::chrono::steady_clock::now();
        const double interval = std::chrono::duration<double>(now - receive_metrics_start).count();
        if (interval >= 1.0) {
            spdlog::get("illixr")->info("Boba receiver: {:.1f} fps, {} skipped inputs, decoder queue left={} right={}",
                                        static_cast<double>(received_frames) / interval, skipped_frames,
                                        color_decoder_ ? color_decoder_->get_left_queue_depth() : 0,
                                        color_decoder_ ? color_decoder_->get_right_queue_depth() : 0);
            receive_metrics_start = now;
            received_frames = skipped_frames = 0;
        }
        if (!accept_input) {
            spdlog::get("illixr")->debug("[receiver_loop] Skipping frame {} until a keyframe after input loss",
                                         current_frame->frame_number);
            continue;
        }
#    else
        // Never drop a frame if any stream carries a keyframe - dropping a
        // keyframe causes decoder corruption until the next IDR arrives.
        const bool is_any_key = is_key_color || is_key_depth || is_key_mv;

        // Drop this frame if the color decoder input queue is already full
        // and no stream is a keyframe. This prevents latency from growing
        // unboundedly when the hardware decoder cannot keep up.
        if (!is_any_key && color_decoder_) {
            const size_t left_depth  = color_decoder_->get_left_queue_depth();
            const size_t right_depth = color_decoder_->get_right_queue_depth();
            if (left_depth >= MAX_DECODER_QUEUE_DEPTH || right_depth >= MAX_DECODER_QUEUE_DEPTH) {
                spdlog::get("illixr")->warn("[receiver_loop] Dropping P-frame {} - decoder queue full "
                                            "(left={}, right={})",
                                            current_frame->frame_number, left_depth, right_depth);
                continue;
            }
        }
#    endif

#    ifdef ILLIXR_ENABLE_BOBA
        // Publish the complete metadata snapshot before queueing the bitstream.
        // A fast decoder may make the output image visible immediately; storing
        // first guarantees that an acquired image can only be paired with its
        // own pose/FOV/overlay metadata.
        {
            std::lock_guard<std::mutex> lock(frame_meta_map_mutex_);
            frame_meta&                 meta = frame_meta_map_[current_frame->frame_number];
            meta.pose                        = current_frame->pose;
            meta.frame_number                = current_frame->frame_number;
            meta.frame_time                  = current_frame->sent_time;
            meta.pose_id                     = current_frame->pose_id;
            meta.near_z                      = current_frame->near_z;
            meta.far_z                       = current_frame->far_z;
            meta.encode_time                 = current_frame->encode_time;
            meta.presentation_mode           = current_frame->presentation_mode;
            meta.content_aspect_ratio        = current_frame->content_aspect_ratio;
            meta.boba_overlay                = current_frame->boba_overlay;
            meta.boba_modal                  = current_frame->boba_modal;
            meta.fov_left                    = current_frame->fov_left;
            meta.fov_right                   = current_frame->fov_right;
            meta.fov_up                      = current_frame->fov_up;
            meta.fov_down                    = current_frame->fov_down;
            meta.consumed                    = false;
        }

#    endif

        // Wire timestamps are nanoseconds; MediaCodec PTS is in microseconds.
        const auto presentation_time_us = static_cast<int64_t>(current_frame->sent_time / 1000);

        // Queue encoded data to the hardware decoders.
        // All stream types are submitted together so they stay in sync -
        // if we drop above, we drop all three atomically.
#    ifdef COMBINED_ENCODING
        // Combined mode: left_color holds the full side-by-side bitstream;
        // right_color is empty on the wire.  Feed only eye=0.
        if (color_decoder_ && !current_frame->left_color.empty()) {
            color_decoder_->queue_encoded_data(0, current_frame->left_color.data(), current_frame->left_color.size(),
                                               presentation_time_us, is_key_color, current_frame->frame_number);
        }

        // Depth and motion-vector streams remain per-eye even under
        // COMBINED_ENCODING (server sends them as separate streams).
        for (int eye = 0; eye < 2; eye++) {
            if (use_depth_ && depth_decoder_) {
                const auto& depth_pkt = (eye == 0) ? current_frame->left_depth : current_frame->right_depth;
                if (!depth_pkt.empty()) {
                    depth_decoder_->queue_encoded_data(eye, depth_pkt.data(), depth_pkt.size(), presentation_time_us,
                                                       is_key_depth, current_frame->frame_number);
                }
            }

            if (use_motion_vectors_ && motion_vec_decoder_ && current_frame->use_motion_vectors) {
                const auto& mv_pkt = (eye == 0) ? current_frame->left_motion_vec : current_frame->right_motion_vec;
                if (!mv_pkt.empty()) {
                    motion_vec_decoder_->queue_encoded_data(eye, mv_pkt.data(), mv_pkt.size(), presentation_time_us, is_key_mv,
                                                            current_frame->frame_number);
                }
            }
        }
#    else
        for (int eye = 0; eye < 2; eye++) {
            const auto& color_pkt = (eye == 0) ? current_frame->left_color : current_frame->right_color;
            if (color_decoder_ && !color_pkt.empty()) {
                color_decoder_->queue_encoded_data(eye, color_pkt.data(), color_pkt.size(), presentation_time_us, is_key_color,
                                                   current_frame->frame_number);
            }

            if (use_depth_ && depth_decoder_) {
                const auto& depth_pkt = (eye == 0) ? current_frame->left_depth : current_frame->right_depth;
                if (!depth_pkt.empty()) {
                    depth_decoder_->queue_encoded_data(eye, depth_pkt.data(), depth_pkt.size(), presentation_time_us,
                                                       is_key_depth, current_frame->frame_number);
                }
            }

            if (use_motion_vectors_ && motion_vec_decoder_ && current_frame->use_motion_vectors) {
                const auto& mv_pkt = (eye == 0) ? current_frame->left_motion_vec : current_frame->right_motion_vec;
                if (!mv_pkt.empty()) {
                    motion_vec_decoder_->queue_encoded_data(eye, mv_pkt.data(), mv_pkt.size(), presentation_time_us, is_key_mv,
                                                            current_frame->frame_number);
                }
            }
        }
#    endif // COMBINED_ENCODING
#    ifndef ILLIXR_ENABLE_BOBA
        // Store metadata so _p_one_iteration can populate dual_frames.
        // The mutex ensures _p_one_iteration always sees a consistent snapshot.
        {
            std::lock_guard<std::mutex> lock(frame_meta_map_mutex_);
            frame_meta&                 meta = frame_meta_map_[current_frame->frame_number];
            meta.pose                        = current_frame->pose;
            meta.frame_number                = current_frame->frame_number;
            meta.frame_time                  = current_frame->sent_time;
            meta.pose_id                     = current_frame->pose_id;
            meta.near_z                      = current_frame->near_z;
            meta.far_z                       = current_frame->far_z;
            meta.encode_time                 = current_frame->encode_time;

            // Cache first non-zero FOV received from server
            if (!fov_cached_ && current_frame->fov_left[0] != 0.0f) {
                cached_fov_left_  = current_frame->fov_left;
                cached_fov_right_ = current_frame->fov_right;
                cached_fov_up_    = current_frame->fov_up;
                cached_fov_down_  = current_frame->fov_down;
                fov_cached_       = true;
            }
        }
#    endif
    }
}
#else

void offload_rendering_client::start() {
    ffmpeg_init_device();
    ffmpeg_init_cuda_device();
    threadloop::start();
}

void offload_rendering_client::setup(VkRenderPass render_pass, uint32_t subpass,
                                     std::shared_ptr<vulkan::buffer_pool<pose::fast_head_pose_type>> buffer_pool) {
    (void) render_pass;
    (void) subpass;
    this->buffer_pool_ = buffer_pool;
    command_pool =
        vulkan::create_command_pool(display_provider_->vk_device_, display_provider_->queues_[vulkan::queue::GRAPHICS].family);

    // Initialize FFmpeg and frame resources
    ffmpeg_init_frame_ctx();
    ffmpeg_init_cuda_frame_ctx();
    ffmpeg_init_buffer_pool();
    ffmpeg_init_decoder();
    ready_ = true;

    // Initialize image layouts for color frames
    for (auto& frame : avvk_color_frames_) {
        for (auto& eye : frame) {
            auto cmd_buf = vulkan::begin_one_time_command(display_provider_->vk_device_, command_pool);
            transition_layout(cmd_buf, eye.frame, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
            vulkan::end_one_time_command(display_provider_->vk_device_, command_pool,
                                         display_provider_->queues_[vulkan::queue::GRAPHICS], cmd_buf);
        }
    }

    // Initialize image layouts for depth frames if enabled
    if (use_depth_) {
        for (auto& frame : avvk_depth_frames_) {
            for (auto& eye : frame) {
                auto cmd_buf = vulkan::begin_one_time_command(display_provider_->vk_device_, command_pool);
                transition_layout(cmd_buf, eye.frame, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
                vulkan::end_one_time_command(display_provider_->vk_device_, command_pool,
                                             display_provider_->queues_[vulkan::queue::GRAPHICS], cmd_buf);
            }
        }
    }

    // Create command buffers for layout transitions
    for (size_t i = 0; i < avvk_color_frames_.size(); i++) {
        for (auto eye = 0; eye < 2; eye++) {
            // Create start transition command buffers
            layout_transition_start_cmd_bufs_[i][eye] =
                vulkan::create_command_buffer(display_provider_->vk_device_, command_pool);
            VkCommandBufferBeginInfo begin_info{};
            begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
            begin_info.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT;
            vkBeginCommandBuffer(layout_transition_start_cmd_bufs_[i][eye], &begin_info);
            transition_layout(layout_transition_start_cmd_bufs_[i][eye], avvk_color_frames_[i][eye].frame,
                              VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
            if (use_depth_) {
                transition_layout(layout_transition_start_cmd_bufs_[i][eye], avvk_depth_frames_[i][eye].frame,
                                  VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
            }
            vkEndCommandBuffer(layout_transition_start_cmd_bufs_[i][eye]);

            // Create end transition command buffers
            layout_transition_end_cmd_bufs_[i][eye] =
                vulkan::create_command_buffer(display_provider_->vk_device_, command_pool);
            vkBeginCommandBuffer(layout_transition_end_cmd_bufs_[i][eye], &begin_info);
            transition_layout(layout_transition_end_cmd_bufs_[i][eye], avvk_color_frames_[i][eye].frame,
                              VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
            if (use_depth_) {
                transition_layout(layout_transition_end_cmd_bufs_[i][eye], avvk_depth_frames_[i][eye].frame,
                                  VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
            }
            vkEndCommandBuffer(layout_transition_end_cmd_bufs_[i][eye]);
        }
    }

    // Create fence for synchronization
    VkFenceCreateInfo fence_info = {
        .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
    };
    vkCreateFence(display_provider_->vk_device_, &fence_info, nullptr, &fence_);
}

void offload_rendering_client::destroy() {
    // Free color frame resources
    for (auto& frame : avvk_color_frames_) {
        for (auto& eye : frame) {
            av_frame_free(&eye.frame);
        }
    }

    // Free depth frame resources if enabled
    for (auto& frame : avvk_depth_frames_) {
        for (auto& eye : frame) {
            av_frame_free(&eye.frame);
        }
    }

    // Release FFmpeg contexts
    av_buffer_unref(&frame_ctx_);
    av_buffer_unref(&device_ctx_);
}

[[maybe_unused]] void offload_rendering_client::copy_image_to_cpu_and_save_file(AVFrame* frame) {
    auto cpu_av_frame    = av_frame_alloc();
    cpu_av_frame->format = AV_PIX_FMT_RGBA;
    auto ret             = av_hwframe_transfer_data(cpu_av_frame, frame, 0);
    AV_ASSERT_SUCCESS(ret);

    // save cpu_av_frame as png
    auto png_codec           = avcodec_find_encoder(AV_CODEC_ID_PNG);
    auto png_codec_ctx       = avcodec_alloc_context3(png_codec);
    png_codec_ctx->pix_fmt   = AV_PIX_FMT_RGBA;
    png_codec_ctx->width     = cpu_av_frame->width;
    png_codec_ctx->height    = cpu_av_frame->height;
    png_codec_ctx->time_base = {1, 60};
    png_codec_ctx->framerate = {60, 1};

    ret = avcodec_open2(png_codec_ctx, png_codec, nullptr);
    AV_ASSERT_SUCCESS(ret);
    AVPacket* png_packet = av_packet_alloc();
    ret                  = avcodec_send_frame(png_codec_ctx, cpu_av_frame);
    AV_ASSERT_SUCCESS(ret);
    ret = avcodec_receive_packet(png_codec_ctx, png_packet);
    AV_ASSERT_SUCCESS(ret);

    std::string filename = "frame_" + std::to_string(frame_count_) + ".png";
    FILE*       f        = fopen(filename.c_str(), "wb");
    fwrite(png_packet->data, 1, png_packet->size, f);
    fclose(f);

    av_packet_free(&png_packet);
    av_frame_free(&cpu_av_frame);
    avcodec_free_context(&png_codec_ctx);
}

[[maybe_unused]] void offload_rendering_client::save_nv12_img_to_png(AVFrame* cuda_frame) const {
    auto cpu_av_frame    = av_frame_alloc();
    cpu_av_frame->format = AV_PIX_FMT_NV12;
    auto ret             = av_hwframe_transfer_data(cpu_av_frame, cuda_frame, 0);
    AV_ASSERT_SUCCESS(ret);

    AVFrame* frameGRB = av_frame_alloc();
    frameGRB->width   = cpu_av_frame->width;
    frameGRB->height  = cpu_av_frame->height;
    frameGRB->format  = AV_PIX_FMT_RGBA;
    av_frame_get_buffer(frameGRB, 0);

    SwsContext* sws_context = sws_getContext(cpu_av_frame->width, cpu_av_frame->height, AV_PIX_FMT_NV12, frameGRB->width,
                                             frameGRB->height, AV_PIX_FMT_RGBA, SWS_BICUBIC, NULL, NULL, NULL);
    if (sws_context != NULL) {
        sws_scale(sws_context, cpu_av_frame->data, cpu_av_frame->linesize, 0, cpu_av_frame->height, frameGRB->data,
                  frameGRB->linesize);
    }

    // save cpu_av_frame as png
    auto png_codec           = avcodec_find_encoder(AV_CODEC_ID_PNG);
    auto png_codec_ctx       = avcodec_alloc_context3(png_codec);
    png_codec_ctx->pix_fmt   = AV_PIX_FMT_RGBA;
    png_codec_ctx->width     = cpu_av_frame->width;
    png_codec_ctx->height    = cpu_av_frame->height;
    png_codec_ctx->time_base = {1, 60};
    png_codec_ctx->framerate = {60, 1};

    ret = avcodec_open2(png_codec_ctx, png_codec, nullptr);
    AV_ASSERT_SUCCESS(ret);
    AVPacket* png_packet = av_packet_alloc();
    ret                  = avcodec_send_frame(png_codec_ctx, frameGRB);
    AV_ASSERT_SUCCESS(ret);
    ret = avcodec_receive_packet(png_codec_ctx, png_packet);
    AV_ASSERT_SUCCESS(ret);

    std::string filename = "frame_" + std::to_string(frame_count_) + ".png";
    FILE*       f        = fopen(filename.c_str(), "wb");
    fwrite(png_packet->data, 1, png_packet->size, f);
    fclose(f);

    av_packet_free(&png_packet);
    av_frame_free(&cpu_av_frame);
    avcodec_free_context(&png_codec_ctx);
}

// Vulkan layout transition
// supports: shader read <-> transfer dst
void offload_rendering_client::transition_layout(VkCommandBuffer cmd_buf, AVFrame* frame, VkImageLayout old_layout,
                                                 VkImageLayout new_layout) {
    auto vk_frame = reinterpret_cast<AVVkFrame*>(frame->data[0]);
    auto image    = vk_frame->img[0];

    VkImageMemoryBarrier barrier{};
    barrier.sType               = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
    barrier.oldLayout           = old_layout;
    barrier.newLayout           = new_layout;
    barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    barrier.image               = image;
    barrier.subresourceRange    = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};

    VkPipelineStageFlags src_stage;
    VkPipelineStageFlags dst_stage;

    if (old_layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL && new_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
        barrier.srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
        barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
        src_stage             = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
        dst_stage             = VK_PIPELINE_STAGE_TRANSFER_BIT;
    } else if (old_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && new_layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
        barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
        barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
        src_stage             = VK_PIPELINE_STAGE_TRANSFER_BIT;
        dst_stage             = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
    } else if (old_layout == VK_IMAGE_LAYOUT_UNDEFINED && new_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
        barrier.srcAccessMask = 0;
        barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
        src_stage             = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
        dst_stage             = VK_PIPELINE_STAGE_TRANSFER_BIT;
    } else if (old_layout == VK_IMAGE_LAYOUT_UNDEFINED && new_layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
        barrier.srcAccessMask = 0;
        barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
        src_stage             = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
        dst_stage             = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
    } else {
        throw std::invalid_argument("unsupported layout transition");
    }

    vkCmdPipelineBarrier(cmd_buf, src_stage, dst_stage, 0, 0, nullptr, 0, nullptr, 1, &barrier);
}

#endif // __ANDROID__

void offload_rendering_client::_p_thread_setup() {
#ifdef __ANDROID__
    spdlog::get("illixr")->info("[android_media_decoder] Thread setup starting");

    // Initialize color decoder - no GL/EGL arguments in the Vulkan path.
    color_decoder_ = std::make_unique<stereo_surface_decoder>(headset_width_, headset_height_, false);
    if (!color_decoder_->initialize()) {
        spdlog::get("illixr")->error("[android_media_decoder] Failed to initialize color decoder (Vulkan path)");
        color_decoder_.reset();
        return;
    }

    if (use_motion_vectors_) {
        // Motion vectors are 432x432 HEVC 10-bit.  The frame_decoder uses
        // "video/hevc" which handles both 8-bit and 10-bit bitstreams.
        // AIMAGE_FORMAT_PRIVATE allows the Snapdragon XR2 Gen 2 hardware
        // decoder to output directly to GPU memory (no CPU copy).
        // depth_decoder_ must have the same dimensions as the motion_vec_decoder_
        depth_decoder_ = std::make_unique<stereo_surface_decoder>(MOTION_VEC_WIDTH, MOTION_VEC_HEIGHT, true);
        if (!depth_decoder_->initialize()) {
            spdlog::get("illixr")->warn("[android_media_decoder] Failed to initialize depth decoder (Vulkan path), "
                                        "depth will be unavailable");
            depth_decoder_.reset();
        }

        motion_vec_decoder_ = std::make_unique<stereo_surface_decoder>(MOTION_VEC_WIDTH, MOTION_VEC_HEIGHT, false, true);
        if (!motion_vec_decoder_->initialize()) {
            spdlog::get("illixr")->warn("[android_media_decoder] Failed to initialize motion-vector decoder "
                                        "(Vulkan path); App Spacewarp will be unavailable");
            motion_vec_decoder_.reset();
        } else {
            spdlog::get("illixr")->info("[android_media_decoder] Motion-vector decoder initialized ({}x{})", MOTION_VEC_WIDTH,
                                        MOTION_VEC_HEIGHT);
        }
    } else if (use_depth_) {
        depth_decoder_ = std::make_unique<stereo_surface_decoder>(headset_width_, headset_height_, true);
        if (!depth_decoder_->initialize()) {
            spdlog::get("illixr")->warn("[android_media_decoder] Failed to initialize depth decoder (Vulkan path), "
                                        "depth will be unavailable");
            depth_decoder_.reset();
        }
    }

    ready_ = true;
    // Launch the receiver thread now that all decoders are initialized.
    // It feeds encoded data to the decoders independently of _p_one_iteration,
    // preventing decoder pipeline latency from blocking network reception.
    receiver_running_ = true;
    receiver_thread_  = std::thread([this]() {
        receiver_loop();
    });

    spdlog::get("illixr")->info("[android_media_decoder] Decoder initialized, receiver thread started");
#endif // __ANDROID__
}

void offload_rendering_client::_p_one_iteration() {
#ifdef __ANDROID__
    if (!ready_ || !color_decoder_ || !color_decoder_->is_ready()) {
#else
    if (!ready_) {
#endif // __ANDROID__
        return;
    }
    // Send latest pose to server
#ifndef USING_OPENXR
    push_pose();
#endif

#ifdef __ANDROID__
    // Consumer phase
    // The receiver_thread_ feeds encoded data to the decoders asynchronously.
    // Here we just acquire whatever the decoders have most recently finished
    // and publish it to the switchboard.

    // ============ TEXTURE UPDATE PHASE ============
    // Get current color frame with textures and transforms
    // This includes calling updateTexImage() which synchronizes with the decoder
    // auto texture_update_start = std::chrono::high_resolution_clock::now();

    // Get complete frame with both color and depth
    dual_frames frame = construct_dual_frames(clock_->now());

    // auto texture_update_end = std::chrono::high_resolution_clock::now();

    // android_texture_update_time_us_ += static_cast(
    //     std::chrono::duration_cast(
    //         texture_update_end - texture_update_start).count());

    if (frame.is_valid()) {
        last_submitted_frame_ = frame.frame_number;
        // Keep a copy for delayed release before frame is moved into the
        // switchboard event - see in_flight_frames_.
        in_flight_frames_.push_back(frame);
        frame_writer_.put(frame_writer_.allocate(std::move(frame)));
        release_stale_frames();
        frame_count_++;
    } else {
        std::this_thread::sleep_for(std::chrono::microseconds(500));
    }
    // auto frame_end = std::chrono::high_resolution_clock::now();
    // android_total_frame_time_us_ += static_cast(
    //     std::chrono::duration_cast(
    //         frame_end - frame_start).count());
    // android_timing_frame_count_++;

#else

    // Receive and process network data
    if (!network_receive()) {
        return;
    }

    // Track frame timing
    auto timestamp = std::chrono::high_resolution_clock::now();
    auto diff      = timestamp - decoded_frame_pose_.predict_target_time.time_since_epoch();

    // Decode frames. Include the operation and dimensions in fatal errors so
    // stream failures can be distinguished from stale display-buffer sizes.
    auto check_decode = [this](int ret, const char* operation, const char* stream, int eye, AVCodecContext* ctx) {
        if (ret < 0) {
            char error[AV_ERROR_MAX_STRING_SIZE];
            av_strerror(ret, error, sizeof(error));
            throw std::runtime_error{std::string{"HEVC "} + operation + " failed: " + error + " stream=" + stream +
                                     " eye=" + std::to_string(eye) + " decoded=" + std::to_string(ctx->width) + "x" +
                                     std::to_string(ctx->height) +
                                     " output=" + std::to_string(buffer_pool_->image_pool[0][0].image_info.extent.width) + "x" +
                                     std::to_string(buffer_pool_->image_pool[0][0].image_info.extent.height)};
        }
    };
    auto decode_start = std::chrono::high_resolution_clock::now();
    for (auto eye = 0; eye < 2; eye++) {
        // Decode color frames
        auto ret = avcodec_send_packet(codec_color_ctx_, decode_src_color_packets_[eye]);
        check_decode(ret, "send_packet", "color", eye, codec_color_ctx_);

        // Decode depth frames if enabled
        if (use_depth_) {
            ret = avcodec_send_packet(codec_depth_ctx_, decode_src_depth_packets_[eye]);
            check_decode(ret, "send_packet", "depth", eye, codec_depth_ctx_);
        }
    }

    // Receive decoded frames; check errors before accessing frame contents.
    for (auto eye = 0; eye < 2; eye++) {
        auto ret = avcodec_receive_frame(codec_color_ctx_, decode_out_color_frames_[eye]);
        check_decode(ret, "receive_frame", "color", eye, codec_color_ctx_);
        assert(decode_out_color_frames_[eye]->format == AV_PIX_FMT_CUDA);

        if (use_depth_) {
            ret = avcodec_receive_frame(codec_depth_ctx_, decode_out_depth_frames_[eye]);
            check_decode(ret, "receive_frame", "depth", eye, codec_depth_ctx_);
            assert(decode_out_depth_frames_[eye]->format == AV_PIX_FMT_CUDA);
        }
    }
    auto decode_end = std::chrono::high_resolution_clock::now();

    // Perform color space conversion
    for (auto eye = 0; eye < 2; eye++) {
        AVFrame* color_src = decode_out_color_frames_[eye];
        AVFrame* color_dst = decode_converted_color_frames_[eye];
        if (color_src->width <= 0 || color_src->height <= 0 || (color_src->width % 2) != 0 || (color_src->height % 2) != 0) {
            throw std::runtime_error{"Invalid NV12 decode dimensions for NPP conversion: " + std::to_string(color_src->width) +
                                     "x" + std::to_string(color_src->height)};
        }
        if (color_src->data[0] == nullptr || color_src->linesize[0] <= 0 || color_dst->data[0] == nullptr ||
            color_dst->linesize[0] <= 0) {
            throw std::runtime_error{"Invalid CUDA frame pointers or strides before NPP conversion"};
        }

        const int  dst_width        = color_dst->width;
        const int  dst_height       = color_dst->height;
        const bool stereo_color_src = color_src->width != dst_width && (color_src->width % 2) == 0;
        const int  src_eye_width    = stereo_color_src ? color_src->width / 2 : color_src->width;
        const bool can_copy_eye_roi = src_eye_width >= dst_width && (src_eye_width % 2) == 0;
        if (dst_width <= 0 || dst_height <= 0 || color_src->height != dst_height || !can_copy_eye_roi) {
            throw std::runtime_error{
                "Decoded frame dimensions do not match destination: src=" + std::to_string(color_src->width) + "x" +
                std::to_string(color_src->height) + " dst=" + std::to_string(dst_width) + "x" + std::to_string(dst_height)};
        }

        if (src_eye_width != dst_width) {
            static bool warned_color_crop = false;
            if (!warned_color_crop) {
                log_->warn("Decoded color eye width {} is larger than destination width {}; using left-side ROI for each eye",
                           src_eye_width, dst_width);
                warned_color_crop = true;
            }
        }

        const int    source_x_offset = stereo_color_src ? eye * src_eye_width : 0;
        NppiSize     roi             = {dst_width, dst_height};
        const Npp8u* pSrc[2];
        pSrc[0]             = reinterpret_cast<const Npp8u*>(color_src->data[0]) + source_x_offset;
        const Npp8u* chroma = nv12_chroma_plane(color_src);
        if (chroma == nullptr) {
            throw std::runtime_error{"Invalid NV12 chroma plane before NPP conversion"};
        }
        pSrc[1] = chroma + source_x_offset;
        Npp8u* pDst[3];
        pDst[0] = yuv420_y_plane_;
        pDst[1] = yuv420_u_plane_;
        pDst[2] = yuv420_v_plane_;
        int dst_linesizes[3];
        dst_linesizes[0] = y_step_;
        dst_linesizes[1] = u_step_;
        dst_linesizes[2] = v_step_;

        auto ret = nppiNV12ToYUV420_8u_P2P3R_Ctx(pSrc, color_src->linesize[0], pDst, dst_linesizes, roi, npp_ctx_);
        assert_npp_success(ret, "nppiNV12ToYUV420_8u_P2P3R_Ctx color", eye, color_src, color_dst);

        // Convert YUV420 to BGRA
        auto dst = reinterpret_cast<Npp8u*>(color_dst->data[0]);
        ret      = nppiYUV420ToBGR_8u_P3C4R_Ctx(pDst, dst_linesizes, dst, color_dst->linesize[0], roi, npp_ctx_);
        assert_npp_success(ret, "nppiYUV420ToBGR_8u_P3C4R_Ctx color", eye, color_src, color_dst);

        // Process depth frames if enabled
        if (use_depth_) {
            AVFrame*   depth_src           = decode_out_depth_frames_[eye];
            AVFrame*   depth_dst           = decode_converted_depth_frames_[eye];
            const int  depth_dst_width     = depth_dst->width;
            const int  depth_dst_height    = depth_dst->height;
            const bool stereo_depth_src    = depth_src->width != depth_dst_width && (depth_src->width % 2) == 0;
            const int  depth_src_eye_width = stereo_depth_src ? depth_src->width / 2 : depth_src->width;
            const bool can_copy_depth_roi  = depth_src_eye_width >= depth_dst_width && (depth_src_eye_width % 2) == 0;
            if (depth_dst_width <= 0 || depth_dst_height <= 0 || depth_src->height != depth_dst_height || !can_copy_depth_roi) {
                throw std::runtime_error{"Decoded depth dimensions do not match destination: src=" +
                                         std::to_string(depth_src->width) + "x" + std::to_string(depth_src->height) +
                                         " dst=" + std::to_string(depth_dst_width) + "x" + std::to_string(depth_dst_height)};
            }

            if (depth_src_eye_width != depth_dst_width) {
                static bool warned_depth_crop = false;
                if (!warned_depth_crop) {
                    log_->warn(
                        "Decoded depth eye width {} is larger than destination width {}; using left-side ROI for each eye",
                        depth_src_eye_width, depth_dst_width);
                    warned_depth_crop = true;
                }
            }

            const int    depth_source_x_offset = stereo_depth_src ? eye * depth_src_eye_width : 0;
            const Npp8u* pSrc_depth[2];
            pSrc_depth[0]             = reinterpret_cast<const Npp8u*>(depth_src->data[0]) + depth_source_x_offset;
            const Npp8u* depth_chroma = nv12_chroma_plane(depth_src);
            pSrc_depth[1]             = depth_chroma == nullptr ? nullptr : depth_chroma + depth_source_x_offset;
            if (pSrc_depth[0] == nullptr || pSrc_depth[1] == nullptr) {
                throw std::runtime_error{"Invalid NV12 depth planes before NPP conversion"};
            }
            NppiSize roi_depth = {depth_dst_width, depth_dst_height};
            ret = nppiNV12ToYUV420_8u_P2P3R_Ctx(pSrc_depth, depth_src->linesize[0], pDst, dst_linesizes, roi_depth, npp_ctx_);
            assert_npp_success(ret, "nppiNV12ToYUV420_8u_P2P3R_Ctx depth", eye, depth_src, depth_dst);
            auto dst_depth = reinterpret_cast<Npp8u*>(depth_dst->data[0]);
            ret = nppiYUV420ToBGR_8u_P3C4R_Ctx(pDst, dst_linesizes, dst_depth, depth_dst->linesize[0], roi_depth, npp_ctx_);
            assert_npp_success(ret, "nppiYUV420ToBGR_8u_P3C4R_Ctx depth", eye, depth_src, depth_dst);
        }
        cudaDeviceSynchronize();
    }
    auto conversion_end = std::chrono::high_resolution_clock::now();

    // Update display buffers
    auto ind            = buffer_pool_->src_acquire_image();
    auto transfer_start = std::chrono::high_resolution_clock::now();

    auto* frames = reinterpret_cast<AVHWFramesContext*>(frame_ctx_->data);
    auto* vk     = static_cast<AVVulkanFramesContext*>(frames->hwctx);

    // Transfer frames to display buffers
    for (auto eye = 0; eye < 2; eye++) {
        vk->lock_frame(frames, avvk_color_frames_[ind][eye].vk_frame);
        if (use_depth_) {
            vk->lock_frame(frames, avvk_depth_frames_[ind][eye].vk_frame);
        }

        vkResetFences(display_provider_->vk_device_, 1, &fence_);

        // Set up synchronization
        std::vector<VkSemaphore>          timelines   = {avvk_color_frames_[ind][eye].vk_frame->sem[0]};
        std::vector<VkPipelineStageFlags> wait_stages = {VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT};
        if (use_depth_) {
            timelines.push_back(avvk_depth_frames_[ind][eye].vk_frame->sem[0]);
            wait_stages.push_back(VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT);
        }

        std::vector<uint64_t> start_wait_values   = {avvk_color_frames_[ind][eye].vk_frame->sem_value[0]};
        std::vector<uint64_t> start_signal_values = {++avvk_color_frames_[ind][eye].vk_frame->sem_value[0]};
        if (use_depth_) {
            start_wait_values.push_back(avvk_depth_frames_[ind][eye].vk_frame->sem_value[0]);
            start_signal_values.push_back(++avvk_depth_frames_[ind][eye].vk_frame->sem_value[0]);
        }

        // Submit layout transition commands
        VkTimelineSemaphoreSubmitInfo transition_start_timeline = {
            .sType                     = VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO,
            .pNext                     = nullptr,
            .waitSemaphoreValueCount   = static_cast<uint16_t>(start_wait_values.size()),
            .pWaitSemaphoreValues      = start_wait_values.data(),
            .signalSemaphoreValueCount = static_cast<uint16_t>(start_signal_values.size()),
            .pSignalSemaphoreValues    = start_signal_values.data(),
        };

        VkSubmitInfo transition_start_submit = {
            .sType                = VK_STRUCTURE_TYPE_SUBMIT_INFO,
            .pNext                = &transition_start_timeline,
            .waitSemaphoreCount   = static_cast<uint16_t>(timelines.size()),
            .pWaitSemaphores      = timelines.data(),
            .pWaitDstStageMask    = wait_stages.data(),
            .commandBufferCount   = 1,
            .pCommandBuffers      = &layout_transition_start_cmd_bufs_[ind][eye],
            .signalSemaphoreCount = static_cast<uint16_t>(timelines.size()),
            .pSignalSemaphores    = timelines.data(),
        };
        vulkan::locked_queue_submit(display_provider_->queues_[vulkan::queue::GRAPHICS], 1, &transition_start_submit, nullptr);

        // Transfer frame data
        auto ret = av_hwframe_transfer_data(avvk_color_frames_[ind][eye].frame, decode_converted_color_frames_[eye], 0);
        AV_ASSERT_SUCCESS(ret);

        if (use_depth_) {
            ret = av_hwframe_transfer_data(avvk_depth_frames_[ind][eye].frame, decode_converted_depth_frames_[eye], 0);
            AV_ASSERT_SUCCESS(ret);
        }

        // Submit end transition commands
        std::vector<uint64_t> end_wait_values   = {avvk_color_frames_[ind][eye].vk_frame->sem_value[0]};
        std::vector<uint64_t> end_signal_values = {++avvk_color_frames_[ind][eye].vk_frame->sem_value[0]};
        if (use_depth_) {
            end_wait_values.push_back(avvk_depth_frames_[ind][eye].vk_frame->sem_value[0]);
            end_signal_values.push_back(++avvk_depth_frames_[ind][eye].vk_frame->sem_value[0]);
        }

        VkTimelineSemaphoreSubmitInfo transition_end_timeline = {
            .sType                     = VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO,
            .pNext                     = nullptr,
            .waitSemaphoreValueCount   = static_cast<uint16_t>(end_wait_values.size()),
            .pWaitSemaphoreValues      = end_wait_values.data(),
            .signalSemaphoreValueCount = static_cast<uint16_t>(end_signal_values.size()),
            .pSignalSemaphoreValues    = end_signal_values.data(),
        };

        VkSubmitInfo transition_end_submit = {
            .sType                = VK_STRUCTURE_TYPE_SUBMIT_INFO,
            .pNext                = &transition_end_timeline,
            .waitSemaphoreCount   = static_cast<uint16_t>(timelines.size()),
            .pWaitSemaphores      = timelines.data(),
            .pWaitDstStageMask    = wait_stages.data(),
            .commandBufferCount   = 1,
            .pCommandBuffers      = &layout_transition_end_cmd_bufs_[ind][eye],
            .signalSemaphoreCount = static_cast<uint16_t>(timelines.size()),
            .pSignalSemaphores    = timelines.data(),
        };
        vulkan::locked_queue_submit(display_provider_->queues_[vulkan::queue::GRAPHICS], 1, &transition_end_submit, fence_);
        vkWaitForFences(display_provider_->vk_device_, 1, &fence_, VK_TRUE, UINT64_MAX);

        // Update frame counters and release resources
        if (use_depth_) {
            decode_out_color_frames_[eye]->pts = static_cast<int64_t>(frame_count_++);
            decode_out_depth_frames_[eye]->pts = static_cast<int64_t>(frame_count_++);

            vulkan::wait_timeline_semaphores(
                display_provider_->vk_device_,
                {{avvk_color_frames_[ind][eye].vk_frame->sem[0], avvk_color_frames_[ind][eye].vk_frame->sem_value[0]},
                 {avvk_depth_frames_[ind][eye].vk_frame->sem[0], avvk_depth_frames_[ind][eye].vk_frame->sem_value[0]}});

            vk->unlock_frame(frames, avvk_color_frames_[ind][eye].vk_frame);
            vk->unlock_frame(frames, avvk_depth_frames_[ind][eye].vk_frame);
        } else {
            decode_out_color_frames_[eye]->pts = static_cast<int64_t>(frame_count_++);

            vulkan::wait_timeline_semaphores(
                display_provider_->vk_device_,
                {{avvk_color_frames_[ind][eye].vk_frame->sem[0], avvk_color_frames_[ind][eye].vk_frame->sem_value[0]}});

            vk->unlock_frame(frames, avvk_color_frames_[ind][eye].vk_frame);
        }
    }

    auto transfer_end = std::chrono::high_resolution_clock::now();
    buffer_pool_->src_release_image(ind, std::move(decoded_frame_pose_));

    // Include conversion and GPU waits: asynchronous decode work can finish
    // there rather than inside avcodec_receive_frame. Excludes network receive.
    const double processing_ms = std::chrono::duration<double, std::milli>(transfer_end - decode_start).count();
    if (processing_ms >= 10.0) {
        log_->warn("[OFFLOAD_SLOW_DECODE] processing_ms={:.2f} codec_ms={:.2f} conversion_ms={:.2f} "
                   "transfer_ms={:.2f} receive_queue={}",
                   processing_ms, std::chrono::duration<double, std::milli>(decode_end - decode_start).count(),
                   std::chrono::duration<double, std::milli>(conversion_end - decode_end).count(),
                   std::chrono::duration<double, std::milli>(transfer_end - transfer_start).count(), frames_reader_.size());
    }

    // Update performance metrics
    metrics_["decode"] += std::chrono::duration_cast<std::chrono::microseconds>(decode_end - decode_start).count();
    metrics_["conversion"] += std::chrono::duration_cast<std::chrono::microseconds>(conversion_end - decode_end).count();
    metrics_["transfer"] += std::chrono::duration_cast<std::chrono::microseconds>(transfer_end - transfer_start).count();
#endif

    // Log performance metrics every second
    if (std::chrono::duration_cast<std::chrono::seconds>(std::chrono::high_resolution_clock::now() - fps_start_time_).count() >=
        1) {
        log_->info("Decoder FPS: {}", fps_counter_);
        fps_start_time_ = std::chrono::high_resolution_clock::now();

        for (auto& metric : metrics_) {
            auto fps = std::max(fps_counter_, (uint16_t) 1);
            log_->info("{}: {}", metric.first, metric.second / (double) (fps));
            metric.second = 0;
        }
#ifdef __ANDROID__
        // Log detailed Android timing statistics
        log_android_decode_timing();
#endif
        fps_counter_ = 1;
    } else {
        fps_counter_++;
    }
}

#ifndef USING_OPENXR
void offload_rendering_client::push_pose() {
    auto current_pose = pose_prediction_->get_fast_pose();

    auto now = time_point{std::chrono::duration<long, std::nano>{std::chrono::high_resolution_clock::now().time_since_epoch()}};
    current_pose.predict_target_time     = now;
    current_pose.predict_computed_time   = now;
    static uint64_t pose_diag_push_count = 0;
    if (++pose_diag_push_count % 300 == 1) {
        log_->info("[POSE_DIAG][client_push_pose] count={} valid={} pos=({:.3f},{:.3f},{:.3f}) "
                   "ori=({:.3f},{:.3f},{:.3f},{:.3f})",
                   pose_diag_push_count, current_pose.pose.valid, current_pose.pose.position.x(),
                   current_pose.pose.position.y(), current_pose.pose.position.z(), current_pose.pose.orientation.x(),
                   current_pose.pose.orientation.y(), current_pose.pose.orientation.z(), current_pose.pose.orientation.w());
    }
    pose_writer_.put(std::make_shared<pose::fast_head_pose_type>(current_pose));
}
#endif

#ifdef __ANDROID__
// Boba requires exact image/metadata matching; general offload builds retain
// their latest-metadata fallback. The receiver and renderer share the metadata
// map under frame_meta_map_mutex_.
data_format::dual_frames offload_rendering_client::construct_dual_frames(time_point render_time) {
    // Vulkan path: acquire AHardwareBuffers from both decoders
    dual_frames frame = color_decoder_->get_current_frame(render_time);
#    ifdef ILLIXR_ENABLE_BOBA
    if (!frame.is_valid()) {
        return {};
    }

    if (frame.frame_number == 0 || frame.frame_number <= last_submitted_frame_) {
        color_decoder_->release_frame(frame);
        return {};
    }

    const uint64_t decoded_frame_number = frame.frame_number;
    frame_meta     meta;
    bool           exact_metadata_found = false;
    {
        std::lock_guard<std::mutex> lock(frame_meta_map_mutex_);
        auto                        it = frame_meta_map_.find(decoded_frame_number);
        if (it != frame_meta_map_.end() && !it->second.consumed) {
            meta                 = it->second;
            it->second.consumed  = true;
            exact_metadata_found = true;

            if (meta.boba_modal.visible) {
                const auto texture = modal_texture_cache_.find(meta.boba_modal.texture_id);
                if (texture != modal_texture_cache_.end() && texture->second.width == meta.boba_modal.width &&
                    texture->second.height == meta.boba_modal.height) {
                    frame.boba_modal_rgba = texture->second.rgba;
                }
            }

            // Everything older than the decoded image can no longer be used.
            auto stale = frame_meta_map_.begin();
            while (stale != frame_meta_map_.end() && stale->first < decoded_frame_number) {
                stale = frame_meta_map_.erase(stale);
            }
        }
    }

    if (!exact_metadata_found) {
        static std::uint64_t missing_metadata_count = 0;
        if (++missing_metadata_count % 120 == 1) {
            spdlog::get("illixr")->warn("[offload_rendering_client] Dropping decoded frame {} without exact metadata "
                                        "(count={})",
                                        decoded_frame_number, missing_metadata_count);
        }
        color_decoder_->release_frame(frame);
        return {};
    }

    frame.pose                 = meta.pose;
    frame.near_z               = meta.near_z;
    frame.far_z                = meta.far_z;
    frame.pose_id              = meta.pose_id;
    frame.encode_time          = meta.encode_time;
    frame.presentation_mode    = meta.presentation_mode;
    frame.content_aspect_ratio = meta.content_aspect_ratio;
    frame.boba_overlay         = std::move(meta.boba_overlay);
    frame.boba_modal           = meta.boba_modal;
    frame.fov_left             = meta.fov_left;
    frame.fov_right            = meta.fov_right;
    frame.fov_up               = meta.fov_up;
    frame.fov_down             = meta.fov_down;

#    else
    frame_meta meta;
    uint64_t   decoded_frame_number;
    if (frame.is_valid()) {
        // frame.frame_number was set atomically with the buffer acquisition
        // inside acquire_latest_buffer() - no separate call needed.
        if (frame.frame_number <= last_submitted_frame_) {
            color_decoder_->release_frame(frame);
            return {};
        }
        decoded_frame_number = frame.frame_number;
        {
            std::lock_guard<std::mutex> lock(frame_meta_map_mutex_);
            auto                        it = frame_meta_map_.find(decoded_frame_number);
            if (it != frame_meta_map_.end()) {
                meta = it->second;
                // Erase this entry and everything older - the map is ordered by
                // frame_number so begin()..next(it) covers all stale entries.
                auto it2 = frame_meta_map_.begin();
                while (it2 != frame_meta_map_.end()) {
                    if (it2->first < decoded_frame_number) {
                        it2 = frame_meta_map_.erase(it2);
                    } else {
                        ++it2;
                    }
                }
            } else if (!frame_meta_map_.empty()) {
                spdlog::get("illixr")->debug("Could not find meta for {}", decoded_frame_number);
                // Decoded frame not in map (dropped or not yet arrived).
                // Use the most recent available entry as the best approximation.
                meta = frame_meta_map_.rbegin()->second;
            } else if (it == frame_meta_map_.end()) {
                spdlog::get("illixr")->error("[openxr_interface] No meta for frame {}, dropping", decoded_frame_number);
                color_decoder_->release_frame(frame);
                return {};
            } else {
                spdlog::get("illixr")->debug("meta map empty");
            }
            // If map is empty, meta stays default-constructed - frame will likely
            // fail is_valid() and be discarded by the caller.
        }
        if (meta.consumed) {
            color_decoder_->release_frame(frame);
            return {}; // another thread already claimed this frame
        }
    }
    frame.pose        = meta.pose;
    frame.near_z      = meta.near_z;
    frame.far_z       = meta.far_z;
    frame.pose_id     = meta.pose_id;
    frame.encode_time = meta.encode_time;
    frame.fov_left    = cached_fov_left_;
    frame.fov_right   = cached_fov_right_;
    frame.fov_up      = cached_fov_up_;
    frame.fov_down    = cached_fov_down_;

#    endif

    // Depth and motion-vector buffers are only attached when they carry the
    // same server frame_number as the color frame above.  Each decoder is an
    // independent MediaCodec/AImageReader pipeline, so "latest available" from
    // each is not guaranteed to line up - explicit matching here is what
    // actually guarantees cadence, rather than relying on the three decoders
    // happening to drain in lockstep.  RGB is always submitted regardless;
    // depth/motion-vectors are simply omitted for this frame when unmatched.
    if (frame.is_valid() && use_depth_ && depth_decoder_ && depth_decoder_->is_ready()) {
        dual_frames depth_frame = depth_decoder_->get_current_frame(render_time);
        if (depth_frame.is_valid()) {
            if (depth_frame.frame_number == decoded_frame_number) {
                frame.left_depth.hw_buffer  = depth_frame.left_eye.hw_buffer;
                frame.right_depth.hw_buffer = depth_frame.right_eye.hw_buffer;
                frame.has_depth             = true;

                static uint64_t log_counter = 0;
                if (++log_counter % LOG_INTERVAL == 1) {
                    spdlog::get("illixr")->debug("[construct_dual_frames] depth: L={} R={}",
                                                 static_cast<void*>(frame.left_depth.hw_buffer),
                                                 static_cast<void*>(frame.right_depth.hw_buffer));
                }
            } else {
                // Depth decoder is ahead of or behind color for this render_time.
                // Drop depth for this frame rather than pairing mismatched data,
                // and release what was just acquired so it isn't leaked.
                depth_decoder_->release_frame(depth_frame);

                static uint64_t depth_mismatch_log_counter = 0;
                if (++depth_mismatch_log_counter % LOG_INTERVAL == 1) {
                    spdlog::get("illixr")->debug("[construct_dual_frames] depth frame_number {} != color {}, "
                                                 "submitting color-only this frame",
                                                 depth_frame.frame_number, decoded_frame_number);
                }
            }
        }

        // Motion-vector buffers
        if (use_motion_vectors_ && motion_vec_decoder_ && motion_vec_decoder_->is_ready()) {
            dual_frames mv_frame = motion_vec_decoder_->get_current_frame(render_time);
            if (mv_frame.is_valid()) {
                if (mv_frame.frame_number == decoded_frame_number) {
                    frame.left_motion_vec.hw_buffer  = mv_frame.left_eye.hw_buffer;
                    frame.right_motion_vec.hw_buffer = mv_frame.right_eye.hw_buffer;
                    frame.has_motion_vectors         = true;

                    static uint64_t mv_log_counter = 0;
                    if (++mv_log_counter % LOG_INTERVAL == 1) {
                        spdlog::get("illixr")->debug("[construct_dual_frames] Vulkan frame with motion vectors: "
                                                     "MV L={} R={}",
                                                     static_cast<void*>(frame.left_motion_vec.hw_buffer),
                                                     static_cast<void*>(frame.right_motion_vec.hw_buffer));
                    }
                } else {
                    motion_vec_decoder_->release_frame(mv_frame);

                    static uint64_t mv_mismatch_log_counter = 0;
                    if (++mv_mismatch_log_counter % LOG_INTERVAL == 1) {
                        spdlog::get("illixr")->debug("[construct_dual_frames] motion-vector frame_number {} != color {}, "
                                                     "submitting without motion vectors this frame",
                                                     mv_frame.frame_number, decoded_frame_number);
                    }
                }
            }
        }
    }
    return frame;
}

void offload_rendering_client::release_stale_frames() {
    // color_decoder_->release_frame() is decoder-agnostic (see
    // stereo_surface_decoder.hpp) - it releases whichever of color, depth,
    // and motion-vector hw_buffer fields are populated on the given
    // dual_frames, so one call per queued frame covers all three streams.
    while (in_flight_frames_.size() > MAX_IN_FLIGHT_FRAMES) {
        if (color_decoder_) {
            color_decoder_->release_frame(in_flight_frames_.front());
        }
        in_flight_frames_.pop_front();
    }
}

#else

bool offload_rendering_client::network_receive() {
    // Free previous packets if they exist
    if (decode_src_color_packets_[0] != nullptr) {
        av_packet_free_side_data(decode_src_color_packets_[0]);
        av_packet_free_side_data(decode_src_color_packets_[1]);
        av_packet_free(&decode_src_color_packets_[0]);
        av_packet_free(&decode_src_color_packets_[1]);
        if (use_depth_) {
            av_packet_free_side_data(decode_src_depth_packets_[0]);
            av_packet_free_side_data(decode_src_depth_packets_[1]);
            av_packet_free(&decode_src_depth_packets_[0]);
            av_packet_free(&decode_src_depth_packets_[1]);
        }
    }

    // Receive new frame data
    auto current_frame   = frames_reader_.dequeue();
    auto current_latency = network_latency_reader_.get_ro_nullable();
    if (current_frame == nullptr) {
        return false;
    }

    // Store color packet data
    decode_src_color_packets_[0] = current_frame->left_color;
    decode_src_color_packets_[1] = current_frame->right_color;

    // Store depth packet data
    if (use_depth_) {
        decode_src_depth_packets_[0] = current_frame->left_depth;
        decode_src_depth_packets_[1] = current_frame->right_depth;
    }

    // Track frame timing
    if (current_latency != nullptr) {
        log_->info("Network latency (one way): {} ms", current_latency->estimated_one_way_latency_ms);
    } else {
        log_->info("Network latency not available");
    }
    decoded_frame_pose_ = current_frame->pose;
    return true;
}

[[maybe_unused]] void offload_rendering_client::submit_command_buffer(VkCommandBuffer vk_command_buffer) {
    VkSubmitInfo submitInfo{
        VK_STRUCTURE_TYPE_SUBMIT_INFO, // sType
        nullptr,                       // pNext
        0,                             // waitSemaphoreCount
        nullptr,                       // pWaitSemaphores
        nullptr,                       // pWaitDstStageMask
        1,                             // commandBufferCount
        &vk_command_buffer,            // pCommandBuffers
        0,                             // signalSemaphoreCount
        nullptr                        // pSignalSemaphores
    };
    vulkan::locked_queue_submit(display_provider_->queues_[vulkan::queue::GRAPHICS], 1, &submitInfo, nullptr);
}

void offload_rendering_client::ffmpeg_init_device() {
    this->device_ctx_     = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VULKAN);
    auto hwdev_ctx        = reinterpret_cast<AVHWDeviceContext*>(device_ctx_->data);
    auto vulkan_hwdev_ctx = reinterpret_cast<AVVulkanDeviceContext*>(hwdev_ctx->hwctx);

    // Configure Vulkan device context
    vulkan_hwdev_ctx->inst            = display_provider_->vk_instance_;
    vulkan_hwdev_ctx->phys_dev        = display_provider_->vk_physical_device_;
    vulkan_hwdev_ctx->act_dev         = display_provider_->vk_device_;
    vulkan_hwdev_ctx->device_features = display_provider_->features_;

    // Set up queue families
    for (auto& queue : display_provider_->queues_) {
        switch (queue.first) {
        case vulkan::queue::GRAPHICS:
            vulkan_hwdev_ctx->queue_family_index    = static_cast<int>(queue.second.family);
            vulkan_hwdev_ctx->nb_graphics_queues    = 1;
            vulkan_hwdev_ctx->queue_family_tx_index = static_cast<int>(queue.second.family);
            vulkan_hwdev_ctx->nb_tx_queues          = 1;
            break;
        case vulkan::queue::COMPUTE:
            vulkan_hwdev_ctx->queue_family_comp_index = static_cast<int>(queue.second.family);
            vulkan_hwdev_ctx->nb_comp_queues          = 1;
        default:
            break;
        }
    }

    // Configure dedicated transfer queue if available
    if (display_provider_->queues_.find(vulkan::queue::DEDICATED_TRANSFER) != display_provider_->queues_.end()) {
        vulkan_hwdev_ctx->queue_family_tx_index =
            static_cast<int>(display_provider_->queues_[vulkan::queue::DEDICATED_TRANSFER].family);
        vulkan_hwdev_ctx->nb_tx_queues = 1;
    }

    // Vulkan Video not used in this implementation
    vulkan_hwdev_ctx->nb_encode_queues          = 0;
    vulkan_hwdev_ctx->nb_decode_queues          = 0;
    vulkan_hwdev_ctx->queue_family_encode_index = -1;
    vulkan_hwdev_ctx->queue_family_decode_index = -1;

    // Set up extensions and callbacks
    vulkan_hwdev_ctx->alloc                      = nullptr;
    vulkan_hwdev_ctx->get_proc_addr              = vkGetInstanceProcAddr;
    vulkan_hwdev_ctx->enabled_inst_extensions    = display_provider_->enabled_instance_extensions_.data();
    vulkan_hwdev_ctx->nb_enabled_inst_extensions = static_cast<int>(display_provider_->enabled_instance_extensions_.size());
    vulkan_hwdev_ctx->enabled_dev_extensions     = display_provider_->enabled_device_extensions_.data();
    vulkan_hwdev_ctx->nb_enabled_dev_extensions  = static_cast<int>(display_provider_->enabled_device_extensions_.size());
    vulkan_hwdev_ctx->lock_queue                 = &ffmpeg_lock_queue;
    vulkan_hwdev_ctx->unlock_queue               = &ffmpeg_unlock_queue;

    AV_ASSERT_SUCCESS(av_hwdevice_ctx_init(device_ctx_));
    log_->info("FFmpeg Vulkan hwdevice context initialized");
}

void offload_rendering_client::ffmpeg_init_cuda_device() {
    auto ret = av_hwdevice_ctx_create(&cuda_device_ctx_, AV_HWDEVICE_TYPE_CUDA, nullptr, nullptr, 0);
    AV_ASSERT_SUCCESS(ret);
    if (cuda_device_ctx_ == nullptr) {
        throw std::runtime_error{"Failed to create FFmpeg CUDA hwdevice context"};
    }
    log_->info("FFmpeg CUDA hwdevice context initialized");
}

void offload_rendering_client::ffmpeg_init_frame_ctx() {
    assert(this->buffer_pool_ != nullptr);
    this->frame_ctx_ = av_hwframe_ctx_alloc(device_ctx_);
    if (!frame_ctx_) {
        throw std::runtime_error{"Failed to create FFmpeg Vulkan hwframe context"};
    }

    auto hwframe_ctx    = reinterpret_cast<AVHWFramesContext*>(frame_ctx_->data);
    hwframe_ctx->format = AV_PIX_FMT_VULKAN;
    auto pix_format     = vulkan::ffmpeg_utils::get_pix_format_from_vk_format(buffer_pool_->image_pool[0][0].image_info.format);
    if (!pix_format) {
        throw std::runtime_error{"Unsupported Vulkan image format " +
                                 std::to_string(buffer_pool_->image_pool[0][0].image_info.format) +
                                 " when creating FFmpeg Vulkan hwframe context"};
    }
    hwframe_ctx->sw_format         = *pix_format;
    hwframe_ctx->width             = static_cast<int>(buffer_pool_->image_pool[0][0].image_info.extent.width);
    hwframe_ctx->height            = static_cast<int>(buffer_pool_->image_pool[0][0].image_info.extent.height);
    hwframe_ctx->initial_pool_size = 0;
    auto ret                       = av_hwframe_ctx_init(frame_ctx_);
    AV_ASSERT_SUCCESS(ret);
}

AVBufferRef* offload_rendering_client::create_cuda_frame_ctx(AVPixelFormat fmt) {
    auto cuda_frame_ref = av_hwframe_ctx_alloc(cuda_device_ctx_);
    if (!cuda_frame_ref) {
        throw std::runtime_error{"Failed to create FFmpeg CUDA hwframe context"};
    }
    auto cuda_hwframe_ctx               = reinterpret_cast<AVHWFramesContext*>(cuda_frame_ref->data);
    cuda_hwframe_ctx->format            = AV_PIX_FMT_CUDA;
    cuda_hwframe_ctx->sw_format         = fmt;
    cuda_hwframe_ctx->width             = static_cast<int>(buffer_pool_->image_pool[0][0].image_info.extent.width);
    cuda_hwframe_ctx->height            = static_cast<int>(buffer_pool_->image_pool[0][0].image_info.extent.height);
    cuda_hwframe_ctx->initial_pool_size = 0;
    auto ret                            = av_hwframe_ctx_init(cuda_frame_ref);
    AV_ASSERT_SUCCESS(ret);
    return cuda_frame_ref;
}

void offload_rendering_client::ffmpeg_init_cuda_frame_ctx() {
    assert(this->buffer_pool_ != nullptr);
    this->cuda_nv12_frame_ctx_ = create_cuda_frame_ctx(AV_PIX_FMT_NV12);
    this->cuda_bgra_frame_ctx_ = create_cuda_frame_ctx(AV_PIX_FMT_BGRA);
}

void offload_rendering_client::ffmpeg_init_buffer_pool() {
    assert(this->buffer_pool_ != nullptr);
    avvk_color_frames_.resize(buffer_pool_->image_pool.size());
    avvk_depth_frames_.resize(buffer_pool_->image_pool.size());
    layout_transition_start_cmd_bufs_.resize(buffer_pool_->image_pool.size());
    layout_transition_end_cmd_bufs_.resize(buffer_pool_->image_pool.size());

    // Initialize frame resources for each buffer in the pool
    for (size_t i = 0; i < buffer_pool_->image_pool.size(); i++) {
        for (size_t eye = 0; eye < 2; eye++) {
            // Create and configure color frames
            auto vk_frame = av_vk_frame_alloc();
            if (!vk_frame) {
                throw std::runtime_error{"Failed to allocate FFmpeg Vulkan frame"};
            }
            vk_frame->img[0]          = buffer_pool_->image_pool[i][eye].image;
            vk_frame->tiling          = buffer_pool_->image_pool[i][eye].image_info.tiling;
            vk_frame->mem[0]          = buffer_pool_->image_pool[i][eye].allocation_info.deviceMemory;
            vk_frame->size[0]         = buffer_pool_->image_pool[i][eye].allocation_info.size;
            vk_frame->offset[0]       = static_cast<ptrdiff_t>(buffer_pool_->image_pool[i][eye].allocation_info.offset);
            vk_frame->queue_family[0] = display_provider_->queues_[vulkan::queue::GRAPHICS].family;

            // Create timeline semaphore for synchronization
            VkExportSemaphoreCreateInfo export_semaphore_create_info{VK_STRUCTURE_TYPE_EXPORT_SEMAPHORE_CREATE_INFO, nullptr,
                                                                     VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT};
            vk_frame->sem[0] =
                vulkan::create_timeline_semaphore(display_provider_->vk_device_, 0, &export_semaphore_create_info);
            vk_frame->sem_value[0] = 0;

            avvk_color_frames_[i][eye].vk_frame = vk_frame;

            // Create and configure AVFrame
            auto av_frame = av_frame_alloc();
            if (!av_frame) {
                throw std::runtime_error{"Failed to allocate FFmpeg frame"};
            }
            av_frame->format                 = AV_PIX_FMT_VULKAN;
            av_frame->width                  = static_cast<int>(buffer_pool_->image_pool[i][eye].image_info.extent.width);
            av_frame->height                 = static_cast<int>(buffer_pool_->image_pool[i][eye].image_info.extent.height);
            av_frame->hw_frames_ctx          = av_buffer_ref(frame_ctx_);
            av_frame->data[0]                = reinterpret_cast<uint8_t*>(vk_frame);
            av_frame->buf[0]                 = av_buffer_create(av_frame->data[0], 0, [](void*, uint8_t*) { }, nullptr, 0);
            av_frame->pts                    = 0;
            avvk_color_frames_[i][eye].frame = av_frame;

            // Create and configure depth frames if enabled
            if (use_depth_) {
                auto vk_depth_frame = av_vk_frame_alloc();
                if (!vk_depth_frame) {
                    throw std::runtime_error{"Failed to allocate FFmpeg Vulkan frame"};
                }
                vk_depth_frame->img[0]  = buffer_pool_->depth_image_pool[i][eye].image;
                vk_depth_frame->tiling  = buffer_pool_->depth_image_pool[i][eye].image_info.tiling;
                vk_depth_frame->mem[0]  = buffer_pool_->depth_image_pool[i][eye].allocation_info.deviceMemory;
                vk_depth_frame->size[0] = buffer_pool_->depth_image_pool[i][eye].allocation_info.size;
                vk_depth_frame->offset[0] =
                    static_cast<ptrdiff_t>(buffer_pool_->depth_image_pool[i][eye].allocation_info.offset);
                vk_depth_frame->queue_family[0] = display_provider_->queues_[vulkan::queue::GRAPHICS].family;

                vk_depth_frame->sem[0] =
                    vulkan::create_timeline_semaphore(display_provider_->vk_device_, 0, &export_semaphore_create_info);
                vk_depth_frame->sem_value[0] = 0;

                avvk_depth_frames_[i][eye].vk_frame = vk_depth_frame;

                auto av_depth_frame = av_frame_alloc();
                if (!av_depth_frame) {
                    throw std::runtime_error{"Failed to allocate FFmpeg frame"};
                }
                av_depth_frame->format = AV_PIX_FMT_VULKAN;
                av_depth_frame->width  = static_cast<int>(buffer_pool_->depth_image_pool[i][eye].image_info.extent.width);
                av_depth_frame->height = static_cast<int>(buffer_pool_->depth_image_pool[i][eye].image_info.extent.height);
                av_depth_frame->hw_frames_ctx = av_buffer_ref(frame_ctx_);
                av_depth_frame->data[0]       = reinterpret_cast<uint8_t*>(vk_depth_frame);
                av_depth_frame->buf[0] = av_buffer_create(av_depth_frame->data[0], 0, [](void*, uint8_t*) { }, nullptr, 0);
                av_depth_frame->pts    = 0;
                avvk_depth_frames_[i][eye].frame = av_depth_frame;
            }
        }
    }

    // Initialize decode frames
    for (size_t eye = 0; eye < 2; eye++) {
        // Initialize color decode frames
        decode_out_color_frames_[eye]                = av_frame_alloc();
        decode_out_color_frames_[eye]->format        = AV_PIX_FMT_CUDA;
        decode_out_color_frames_[eye]->hw_frames_ctx = av_buffer_ref(cuda_nv12_frame_ctx_);
        decode_out_color_frames_[eye]->width         = static_cast<int>(buffer_pool_->image_pool[0][0].image_info.extent.width);
        decode_out_color_frames_[eye]->height = static_cast<int>(buffer_pool_->image_pool[0][0].image_info.extent.height);
        auto ret                              = av_hwframe_get_buffer(cuda_nv12_frame_ctx_, decode_out_color_frames_[eye], 0);
        decode_out_color_frames_[eye]->color_range     = AVCOL_RANGE_JPEG;
        decode_out_color_frames_[eye]->colorspace      = AVCOL_SPC_BT709;
        decode_out_color_frames_[eye]->color_trc       = AVCOL_TRC_BT709;
        decode_out_color_frames_[eye]->color_primaries = AVCOL_PRI_BT709;
        AV_ASSERT_SUCCESS(ret);

        // Initialize color conversion frames
        decode_converted_color_frames_[eye]                = av_frame_alloc();
        decode_converted_color_frames_[eye]->format        = AV_PIX_FMT_CUDA;
        decode_converted_color_frames_[eye]->hw_frames_ctx = av_buffer_ref(cuda_bgra_frame_ctx_);
        decode_converted_color_frames_[eye]->width  = static_cast<int>(buffer_pool_->image_pool[0][0].image_info.extent.width);
        decode_converted_color_frames_[eye]->height = static_cast<int>(buffer_pool_->image_pool[0][0].image_info.extent.height);
        ret = av_hwframe_get_buffer(cuda_bgra_frame_ctx_, decode_converted_color_frames_[eye], 0);
        AV_ASSERT_SUCCESS(ret);

        // Initialize depth frames if enabled
        if (use_depth_) {
            decode_out_depth_frames_[eye]                = av_frame_alloc();
            decode_out_depth_frames_[eye]->format        = AV_PIX_FMT_CUDA;
            decode_out_depth_frames_[eye]->hw_frames_ctx = av_buffer_ref(cuda_nv12_frame_ctx_);
            decode_out_depth_frames_[eye]->width =
                static_cast<int>(buffer_pool_->depth_image_pool[0][0].image_info.extent.width);
            decode_out_depth_frames_[eye]->height =
                static_cast<int>(buffer_pool_->depth_image_pool[0][0].image_info.extent.height);
            decode_out_depth_frames_[eye]->color_range     = AVCOL_RANGE_JPEG;
            decode_out_depth_frames_[eye]->colorspace      = AVCOL_SPC_BT709;
            decode_out_depth_frames_[eye]->color_trc       = AVCOL_TRC_BT709;
            decode_out_depth_frames_[eye]->color_primaries = AVCOL_PRI_BT709;
            ret = av_hwframe_get_buffer(cuda_nv12_frame_ctx_, decode_out_depth_frames_[eye], 0);
            AV_ASSERT_SUCCESS(ret);

            decode_converted_depth_frames_[eye]                = av_frame_alloc();
            decode_converted_depth_frames_[eye]->format        = AV_PIX_FMT_CUDA;
            decode_converted_depth_frames_[eye]->hw_frames_ctx = av_buffer_ref(cuda_bgra_frame_ctx_);
            decode_converted_depth_frames_[eye]->width =
                static_cast<int>(buffer_pool_->depth_image_pool[0][0].image_info.extent.width);
            decode_converted_depth_frames_[eye]->height =
                static_cast<int>(buffer_pool_->depth_image_pool[0][0].image_info.extent.height);
            ret = av_hwframe_get_buffer(cuda_bgra_frame_ctx_, decode_converted_depth_frames_[eye], 0);
            AV_ASSERT_SUCCESS(ret);
        }
    }

    // Allocate NPP buffers for color space conversion
    yuv420_u_plane_ = nppiMalloc_8u_C1(static_cast<int>(buffer_pool_->image_pool[0][0].image_info.extent.width) / 2,
                                       static_cast<int>(buffer_pool_->image_pool[0][0].image_info.extent.height) / 2, &u_step_);
    yuv420_v_plane_ = nppiMalloc_8u_C1(static_cast<int>(buffer_pool_->image_pool[0][0].image_info.extent.width) / 2,
                                       static_cast<int>(buffer_pool_->image_pool[0][0].image_info.extent.height) / 2, &v_step_);
    yuv420_y_plane_ = nppiMalloc_8u_C1(static_cast<int>(buffer_pool_->image_pool[0][0].image_info.extent.width),
                                       static_cast<int>(buffer_pool_->image_pool[0][0].image_info.extent.height), &y_step_);
    if (yuv420_y_plane_ == nullptr || yuv420_u_plane_ == nullptr || yuv420_v_plane_ == nullptr || y_step_ <= 0 ||
        u_step_ <= 0 || v_step_ <= 0) {
        throw std::runtime_error{"Failed to allocate NPP YUV420 conversion buffers"};
    }
    npp_ctx_ = makeNppStreamContext(nullptr);
}

void offload_rendering_client::ffmpeg_init_decoder() {
    auto decoder = avcodec_find_decoder_by_name(OFFLOAD_RENDERING_FFMPEG_DECODER_NAME);
    if (!decoder) {
        throw std::runtime_error{"Failed to find FFmpeg decoder"};
    }

    // Initialize color decoder
    this->codec_color_ctx_ = avcodec_alloc_context3(decoder);
    if (!codec_color_ctx_) {
        throw std::runtime_error{"Failed to allocate FFmpeg decoder context"};
    }

    // Configure decoder parameters
    codec_color_ctx_->thread_count  = 0; // auto
    codec_color_ctx_->thread_type   = FF_THREAD_SLICE;
    codec_color_ctx_->pix_fmt       = AV_PIX_FMT_CUDA;
    codec_color_ctx_->sw_pix_fmt    = AV_PIX_FMT_NV12;
    codec_color_ctx_->hw_device_ctx = av_buffer_ref(cuda_device_ctx_);
    // Let NVDEC size its decode surfaces from the stream headers. The Vulkan
    // output pool can differ from the encoded dimensions after a rebuild.
    codec_color_ctx_->width     = static_cast<int>(buffer_pool_->image_pool[0][0].image_info.extent.width);
    codec_color_ctx_->height    = static_cast<int>(buffer_pool_->image_pool[0][0].image_info.extent.height);
    codec_color_ctx_->framerate = {0, 1};
    codec_color_ctx_->flags |= AV_CODEC_FLAG_LOW_DELAY;
    codec_color_ctx_->color_range     = AVCOL_RANGE_JPEG;
    codec_color_ctx_->colorspace      = AVCOL_SPC_BT709;
    codec_color_ctx_->color_trc       = AVCOL_TRC_BT709;
    codec_color_ctx_->color_primaries = AVCOL_PRI_BT709;

    // Set zero latency mode
    av_opt_set_int(codec_color_ctx_->priv_data, "zerolatency", 1, 0);
    av_opt_set_int(codec_color_ctx_->priv_data, "delay", 0, 0);
    av_opt_set(codec_color_ctx_->priv_data, "hwaccel", "cuda", 0);

    auto ret = avcodec_open2(codec_color_ctx_, decoder, nullptr);
    AV_ASSERT_SUCCESS(ret);

    // Initialize depth decoder if enabled
    if (use_depth_) {
        this->codec_depth_ctx_ = avcodec_alloc_context3(decoder);
        if (!codec_depth_ctx_) {
            throw std::runtime_error{"Failed to allocate FFmpeg decoder context"};
        }

        // Configure depth decoder parameters (same as color)
        codec_depth_ctx_->thread_count  = 0;
        codec_depth_ctx_->thread_type   = FF_THREAD_SLICE;
        codec_depth_ctx_->pix_fmt       = AV_PIX_FMT_CUDA;
        codec_depth_ctx_->sw_pix_fmt    = AV_PIX_FMT_NV12;
        codec_depth_ctx_->hw_device_ctx = av_buffer_ref(cuda_device_ctx_);
        codec_depth_ctx_->width         = static_cast<int>(buffer_pool_->image_pool[0][0].image_info.extent.width);
        codec_depth_ctx_->height        = static_cast<int>(buffer_pool_->image_pool[0][0].image_info.extent.height);
        codec_depth_ctx_->framerate     = {0, 1};
        codec_depth_ctx_->flags |= AV_CODEC_FLAG_LOW_DELAY;
        codec_depth_ctx_->color_range     = AVCOL_RANGE_JPEG;
        codec_depth_ctx_->colorspace      = AVCOL_SPC_BT709;
        codec_depth_ctx_->color_trc       = AVCOL_TRC_BT709;
        codec_depth_ctx_->color_primaries = AVCOL_PRI_BT709;

        av_opt_set_int(codec_depth_ctx_->priv_data, "zerolatency", 1, 0);
        av_opt_set_int(codec_depth_ctx_->priv_data, "delay", 0, 0);
        av_opt_set(codec_depth_ctx_->priv_data, "hwaccel", "cuda", 0);

        ret = avcodec_open2(codec_depth_ctx_, decoder, nullptr);
        AV_ASSERT_SUCCESS(ret);
    }
    log_->info("HEVC decoder output pool: {}x{} per eye; decode surfaces follow stream dimensions",
               buffer_pool_->image_pool[0][0].image_info.extent.width, buffer_pool_->image_pool[0][0].image_info.extent.height);
}
#endif