Skip to content

File offload_rendering_client.hpp

File List > offload_rendering_client > offload_rendering_client.hpp

Go to the documentation of this file

#pragma once

#define DOUBLE_INCLUDE
// ILLIXR core headers
#ifdef USING_OPENXR
#    include "illixr/data_format/poses/combined_pose.hpp"
#endif

#include "illixr/data_format/frame.hpp"
#include "illixr/data_format/latency_data.hpp"
#include "illixr/data_format/pose_prediction.hpp"
#include "illixr/data_format/serialization/frame.hpp"
#include "illixr/data_format/serialization/head_pose.hpp"
#include "illixr/switchboard.hpp"
#include "illixr/threadloop.hpp"
#undef DOUBLE_INCLUDE
#ifdef USING_OPENXR
#    include 
#    include 
#    include 
#    include 
#    include 
#    ifdef __ANDROID__
#        include "android/stereo_surface_decoder.hpp"
#        include "illixr/quest3_params.hpp"

#        include 
#    endif
#else
// ILLIXR Vulkan headers
#    include "illixr/vk/display_provider.hpp"
#    include "illixr/vk/ffmpeg_utils.hpp"
#    include "illixr/vk/render_pass.hpp"
#    include "illixr/vk/vk_extension_request.hpp"
#    include "illixr/vk/vulkan_utils.hpp"

// FFmpeg headers (C interface)
extern "C" {
#    include "libavfilter_illixr/buffersink.h"
#    include "libavfilter_illixr/buffersrc.h"
#    include "libswscale_illixr/swscale.h"
}

// NVIDIA nppi headers
#    include "nppi.h"
#endif

namespace ILLIXR {

class offload_rendering_client
    : public threadloop
#ifndef __ANDROID__
    , public vulkan::app
#endif
{
public:
    offload_rendering_client(const std::string& name, phonebook* pb);

#ifdef __ANDROID__
    ~offload_rendering_client() override;
#else
    void start() override;

    void setup(VkRenderPass render_pass, uint32_t subpass,
               std::shared_ptr<vulkan::buffer_pool<data_format::pose::fast_head_pose_type>> buffer_pool) override;

    void record_command_buffer(VkCommandBuffer commandBuffer, VkFramebuffer framebuffer, int buffer_ind, bool left) override {
        (void) commandBuffer;
        (void) framebuffer;
        (void) buffer_ind;
        (void) left;
    }

    void update_uniforms(const BUFFER_TYPE& render_pose) override {
        (void) render_pose;
    }

    bool is_external() override {
        return true;
    }

    void destroy() override;
#endif // __ANDROID__

protected:
    void _p_thread_setup() override;

    skip_option _p_should_skip() override {
        return threadloop::_p_should_skip();
    }

#ifndef __ANDROID__
    [[maybe_unused]] void copy_image_to_cpu_and_save_file(AVFrame* frame);
    [[maybe_unused]] void save_nv12_img_to_png(AVFrame* cuda_frame) const;
    void transition_layout(VkCommandBuffer cmd_buf, AVFrame* frame, VkImageLayout old_layout, VkImageLayout new_layout);
#endif
    void _p_one_iteration() override;

private:
#ifdef __ANDROID__
    struct frame_meta {
        BUFFER_TYPE pose;
        uint64_t    frame_number{0};
        uint64_t    frame_time{0};
        uint64_t    pose_id{0};
        float       near_z{0.f};
        float       far_z{0.f};
        double      encode_time{0.};
        bool        consumed{false};
#    ifdef ILLIXR_ENABLE_BOBA
        data_format::stereo_presentation_mode presentation_mode{data_format::stereo_presentation_mode::stereo_fullscreen};
        float                                 content_aspect_ratio{0.0F};
        data_format::boba_frame_overlay       boba_overlay{};
        data_format::boba_modal_overlay       boba_modal{};
        std::array<float, 2>                  fov_left{0.0F, 0.0F};
        std::array<float, 2>                  fov_right{0.0F, 0.0F};
        std::array<float, 2>                  fov_up{0.0F, 0.0F};
        std::array<float, 2>                  fov_down{0.0F, 0.0F};
#    endif
    };

#    ifdef ILLIXR_ENABLE_BOBA
    struct modal_texture_cache_entry {
        std::uint32_t                                    width{0};
        std::uint32_t                                    height{0};
        std::shared_ptr<const std::vector<std::uint8_t>> rgba;
    };
#    endif

    void receiver_loop();

#    ifdef ILLIXR_ENABLE_BOBA
    void drain_modal_texture_updates();
#    endif

    void log_android_decode_timing();

    data_format::dual_frames construct_dual_frames(time_point render_time);

    void release_stale_frames();

#else
    void push_pose();

    bool network_receive();

    [[maybe_unused]] void submit_command_buffer(VkCommandBuffer vk_command_buffer);

    void ffmpeg_init_device();

    void ffmpeg_init_cuda_device();

    void ffmpeg_init_frame_ctx();

    AVBufferRef* create_cuda_frame_ctx(AVPixelFormat fmt);

    void ffmpeg_init_cuda_frame_ctx();

    void ffmpeg_init_buffer_pool();

    void ffmpeg_init_decoder();
#endif // __ANDROID__

    std::shared_ptr<switchboard>    switchboard_;
    std::shared_ptr<spdlog::logger> log_;
#ifdef __ANDROID__
    switchboard::writer<data_format::dual_frames> frame_writer_;
#else
    std::shared_ptr<vulkan::display_provider> display_provider_;
#endif
    switchboard::buffered_reader<data_format::compressed_frame> frames_reader_;
    switchboard::reader<data_format::network_latency_result>    network_latency_reader_;
#ifdef ILLIXR_ENABLE_BOBA
    switchboard::buffered_reader<data_format::boba_modal_texture> modal_texture_reader_;
#endif

#ifndef USING_OPENXR
    // Pose transmission to server
    switchboard::network_writer<data_format::pose::fast_head_pose_type> pose_writer_;
    std::shared_ptr<data_format::pose_prediction>                       pose_prediction_;
#endif
    std::atomic<bool>               ready_ = false;
    std::shared_ptr<relative_clock> clock_;

#ifndef __ANDROID__
    std::shared_ptr<vulkan::buffer_pool<data_format::pose::fast_head_pose_type>> buffer_pool_;
#endif
    bool use_depth_ = false;

#ifdef __ANDROID__

    bool use_motion_vectors_ = false;

    // Receiver thread: feeds encoded data to decoders independently of the
    // consumer (_p_one_iteration) so decoder latency cannot block reception.
    std::thread       receiver_thread_;
    std::atomic<bool> receiver_running_{false};
    // Maximum number of encoded frames allowed in the color decoder input
    // queue before incoming frames are dropped to prevent growing latency.
    // At 90fps a frame arrives every ~11.1ms; the feeder thread's
    // dequeueInputBuffer has a 2ms timeout and may loop several times per
    // frame under normal scheduling jitter.  A threshold of 3 fires on any
    // brief stall; 8 gives ~89ms of headroom while still bounding latency
    // to under 200ms if the decoder genuinely falls behind.
    static constexpr size_t MAX_DECODER_QUEUE_DEPTH = 8;

    // Maps server frame_number -> complete frame metadata for that frame.
    // Written by receiver_thread_ as each compressed_frame arrives.
    // Read and pruned by _p_one_iteration once the decoded frame_number is known.
    // Entries for frame numbers older than the one just consumed are erased at
    // the same time, so the map stays bounded even if the decoder skips frames.
    std::map<uint64_t, frame_meta> frame_meta_map_;
    std::mutex                     frame_meta_map_mutex_;
#    ifdef ILLIXR_ENABLE_BOBA
    std::unordered_map<std::uint64_t, modal_texture_cache_entry> modal_texture_cache_;
#    endif
    // Bounds how many submitted dual_frames are kept alive awaiting release.
    // Rather than requiring an explicit GPU-completion signal from the
    // render-side consumer (oxr_interface), a submitted frame's
    // AHardwareBuffers are released once this many *newer* frames have been
    // submitted after it - i.e. once enough real time has passed that the
    // render pass reading it is essentially certain to be done.  At the
    // slowest supported refresh rate (72Hz, ~13.9ms/frame) this guarantees
    // at least ~139ms between a frame's submission and its release; at 90Hz
    // it's ~111ms.  The render-pass read itself takes on the order of a few
    // ms, so this leaves a wide margin under normal operation.  A sustained
    // render stall longer than this window could still release a buffer
    // while the GPU is reading it; if corruption is ever suspected, that's
    // the first thing to check (e.g. via frame-pacing logs around the same
    // time) before raising this value further.
    static constexpr size_t MAX_IN_FLIGHT_FRAMES = 10;

    // Frames already submitted on "unity_rendered_frame", retained here
    // purely so their AHardwareBuffers can be released later - not read for
    // any other purpose.  Pushed to in _p_one_iteration() right after each
    // submission; drained from the front by release_stale_frames().
    std::deque<data_format::dual_frames> in_flight_frames_;

    android_app* app_;

    std::unique_ptr<stereo_surface_decoder> color_decoder_;
    std::unique_ptr<stereo_surface_decoder> depth_decoder_;
    // Motion-vector decoder (432x432 HEVC 10-bit)
    std::unique_ptr<stereo_surface_decoder> motion_vec_decoder_;

    // Android timing metrics (accumulated between reports)
    // uint64_t android_queue_time_us_{0};         // Time spent queueing data to decoders
    // uint64_t android_texture_update_time_us_{0}; // Time spent updating GPU textures
    // uint64_t android_total_frame_time_us_{0};    // Total end-to-end frame processing time
    // uint64_t android_timing_frame_count_{0};     // Number of frames in current timing window

#else
    std::vector<std::array<vulkan::ffmpeg_utils::ffmpeg_vk_frame, 2>> avvk_color_frames_;
    std::vector<std::array<vulkan::ffmpeg_utils::ffmpeg_vk_frame, 2>> avvk_depth_frames_;
    std::vector<std::array<VkCommandBuffer, 2>>                       layout_transition_start_cmd_bufs_;
    std::vector<std::array<VkCommandBuffer, 2>>                       layout_transition_end_cmd_bufs_;
    AVBufferRef*                                                      device_ctx_          = nullptr;
    AVBufferRef*                                                      cuda_device_ctx_     = nullptr;
    AVBufferRef*                                                      frame_ctx_           = nullptr;
    AVBufferRef*                                                      cuda_nv12_frame_ctx_ = nullptr;
    AVBufferRef*                                                      cuda_bgra_frame_ctx_ = nullptr;

    AVCodecContext*          codec_color_ctx_               = nullptr;
    std::array<AVPacket*, 2> decode_src_color_packets_      = {nullptr, nullptr};
    std::array<AVFrame*, 2>  decode_out_color_frames_       = {nullptr, nullptr};
    std::array<AVFrame*, 2>  decode_converted_color_frames_ = {nullptr, nullptr};

    AVCodecContext*          codec_depth_ctx_               = nullptr;
    std::array<AVPacket*, 2> decode_src_depth_packets_      = {nullptr, nullptr};
    std::array<AVFrame*, 2>  decode_out_depth_frames_       = {nullptr, nullptr};
    std::array<AVFrame*, 2>  decode_converted_depth_frames_ = {nullptr, nullptr};

    data_format::pose::fast_head_pose_type decoded_frame_pose_;

    VkCommandPool    command_pool{};
    Npp8u*           yuv420_y_plane_ = nullptr;
    Npp8u*           yuv420_u_plane_ = nullptr;
    Npp8u*           yuv420_v_plane_ = nullptr;
    int              y_step_         = 0;
    int              u_step_         = 0;
    int              v_step_         = 0;
    NppStreamContext npp_ctx_        = {};
#endif // __ANDROID__

    uint64_t frame_count_ = 0;

#ifndef __ANDROID__
    VkFence fence_{};
#endif

    uint16_t                                       fps_counter_    = 0;
    std::chrono::high_resolution_clock::time_point fps_start_time_ = std::chrono::high_resolution_clock::now();
    std::map<std::string, uint32_t>                metrics_{};
#if defined(USING_OPENXR) && !defined(ILLIXR_ENABLE_BOBA)
    std::array<float, 2> cached_fov_left_  = {0.0f, 0.0f};
    std::array<float, 2> cached_fov_right_ = {0.0f, 0.0f};
    std::array<float, 2> cached_fov_up_    = {0.0f, 0.0f};
    std::array<float, 2> cached_fov_down_  = {0.0f, 0.0f};
    bool                 fov_cached_       = false;
#endif
    uint64_t last_submitted_frame_{0};
#ifdef __ANDROID__
#    ifdef ILLIXR_ENABLE_BOBA
    int headset_width_  = NATIVE_STREAM_EYE_WIDTH;
    int headset_height_ = NATIVE_STREAM_EYE_HEIGHT;
#    else
    int    headset_width_  = HEADSET_WIDTH;
    int    headset_height_ = HEADSET_HEIGHT;
    double overscan_;
#    endif
#endif
};

} // namespace ILLIXR