#include "stdafx.h" #include "Emu/Cell/lv2/sys_memory.h" #include "Emu/Cell/timers.hpp" #include "Emu/Cell/PPUModule.h" #include "Emu/IdManager.h" #include "Emu/system_config.h" #include "Emu/VFS.h" #include "Emu/Audio/AudioBackend.h" #include "cellRec.h" #include "cellSysutil.h" #include "util/media_utils.h" #include "util/video_provider.h" extern "C" { #include } LOG_CHANNEL(cellRec); extern atomic_t g_recording_mode; template<> void fmt_class_string::format(std::string& out, u64 arg) { format_enum(out, arg, [](auto error) { switch (error) { STR_CASE(CELL_REC_ERROR_OUT_OF_MEMORY); STR_CASE(CELL_REC_ERROR_FATAL); STR_CASE(CELL_REC_ERROR_INVALID_VALUE); STR_CASE(CELL_REC_ERROR_FILE_OPEN); STR_CASE(CELL_REC_ERROR_FILE_WRITE); STR_CASE(CELL_REC_ERROR_INVALID_STATE); STR_CASE(CELL_REC_ERROR_FILE_NO_DATA); } return unknown; }); } // Helper to distinguish video type enum : s32 { VIDEO_TYPE_MPEG4 = 0x0000, VIDEO_TYPE_AVC_MP = 0x1000, VIDEO_TYPE_AVC_BL = 0x2000, VIDEO_TYPE_MJPEG = 0x3000, VIDEO_TYPE_M4HD = 0x4000, }; // Helper to distinguish video quality enum : s32 { VIDEO_QUALITY_0 = 0x000, // small VIDEO_QUALITY_1 = 0x100, // middle VIDEO_QUALITY_2 = 0x200, // large VIDEO_QUALITY_3 = 0x300, // youtube VIDEO_QUALITY_4 = 0x400, VIDEO_QUALITY_5 = 0x500, VIDEO_QUALITY_6 = 0x600, // HD720 VIDEO_QUALITY_7 = 0x700, }; enum class rec_state : u32 { closed = 0x2710, stopped = 0x2711, started = 0xBEEF, // I don't know the value of this }; struct rec_param { s32 ppu_thread_priority = CELL_REC_PARAM_PPU_THREAD_PRIORITY_DEFAULT; s32 spu_thread_priority = CELL_REC_PARAM_SPU_THREAD_PRIORITY_DEFAULT; s32 capture_priority = CELL_REC_PARAM_CAPTURE_PRIORITY_HIGHEST; s32 use_system_spu = CELL_REC_PARAM_USE_SYSTEM_SPU_DISABLE; s32 fit_to_youtube = 0; s32 xmb_bgm = CELL_REC_PARAM_XMB_BGM_DISABLE; s32 mpeg4_fast_encode = CELL_REC_PARAM_MPEG4_FAST_ENCODE_DISABLE; u32 ring_sec = 0; s32 video_input = CELL_REC_PARAM_VIDEO_INPUT_DISABLE; s32 audio_input = CELL_REC_PARAM_AUDIO_INPUT_DISABLE; s32 audio_input_mix_vol = CELL_REC_PARAM_AUDIO_INPUT_MIX_VOL_MIN; s32 reduce_memsize = CELL_REC_PARAM_REDUCE_MEMSIZE_DISABLE; s32 show_xmb = 0; std::string filename; std::string metadata_filename; CellRecSpursParam spurs_param{}; struct { std::string game_title; std::string movie_title; std::string description; std::string userdata; std::string to_string() const { return fmt::format("{ game_title='%s', movie_title='%s', description='%s', userdata='%s' }", game_title, movie_title, description, userdata); } } movie_metadata{}; struct { bool is_set = false; u32 type = 0; u64 start_time = 0; u64 end_time = 0; std::string title; std::vector tags; std::string to_string() const { std::string scene_metadata_tags; for (usz i = 0; i < tags.size(); i++) { if (i > 0) scene_metadata_tags += ", "; fmt::append(scene_metadata_tags, "'%s'", tags[i]); } return fmt::format("{ is_set=%d, type=%d, start_time=%d, end_time=%d, title='%s', tags=[ %s ] }", is_set, type, start_time, end_time, title, scene_metadata_tags); } } scene_metadata{}; std::string to_string() const { std::string priority; for (usz i = 0; i < 8; i++) { if (i > 0) priority += ", "; fmt::append(priority, "%d", spurs_param.priority[i]); } return fmt::format("ppu_thread_priority=%d, spu_thread_priority=%d, capture_priority=%d, use_system_spu=%d, fit_to_youtube=%d, " "xmb_bgm=%d, mpeg4_fast_encode=%d, ring_sec=%d, video_input=%d, audio_input=%d, audio_input_mix_vol=%d, reduce_memsize=%d, " "show_xmb=%d, filename='%s', metadata_filename='%s', spurs_param={ pSpurs=*0x%x, spu_usage_rate=%d, priority=[ %s ], " "movie_metadata=%s, scene_metadata=%s", ppu_thread_priority, spu_thread_priority, capture_priority, use_system_spu, fit_to_youtube, xmb_bgm, mpeg4_fast_encode, ring_sec, video_input, audio_input, audio_input_mix_vol, reduce_memsize, show_xmb, filename, metadata_filename, spurs_param.pSpurs, spurs_param.spu_usage_rate, priority, movie_metadata.to_string(), scene_metadata.to_string()); } bool use_external_audio() const { return audio_input != CELL_REC_PARAM_AUDIO_INPUT_DISABLE // != DISABLE means that cellRec will add samples on its own && audio_input_mix_vol > CELL_REC_PARAM_AUDIO_INPUT_MIX_VOL_MIN; // We need to mix cellRec audio with internal audio } bool use_internal_audio() const { return audio_input == CELL_REC_PARAM_AUDIO_INPUT_DISABLE // DISABLE means that cellRec won't add samples on its own || audio_input_mix_vol < CELL_REC_PARAM_AUDIO_INPUT_MIX_VOL_MAX; // We need to mix cellRec audio with internal audio } bool use_internal_video() const { return video_input == CELL_REC_PARAM_VIDEO_INPUT_DISABLE; // DISABLE means that cellRec won't add frames on its own } }; static constexpr u32 rec_framerate = 30; // Always 30 fps static constexpr u32 rec_channels = 2; // Always 2 channels class rec_video_sink final : public utils::video_sink { public: rec_video_sink() : utils::video_sink() { m_framerate = rec_framerate; } void set_sample_rate(u32 sample_rate) { m_sample_rate = sample_rate; } void stop(bool flush = true) override { cellRec.notice("Stopping video sink. flush=%d", flush); std::scoped_lock lock(m_video_mtx, m_audio_mtx); m_flush = flush; m_paused = false; m_frames_to_encode.clear(); m_samples_to_encode.clear(); has_error = false; } void pause(bool flush = true) override { cellRec.notice("Pausing video sink. flush=%d", flush); std::scoped_lock lock(m_video_mtx, m_audio_mtx); m_flush = flush; m_paused = true; } void resume() override { cellRec.notice("Resuming video sink"); std::scoped_lock lock(m_video_mtx, m_audio_mtx); m_flush = false; m_paused = false; } encoder_frame get_frame() { std::lock_guard lock_video(m_video_mtx); if (!m_frames_to_encode.empty()) { encoder_frame frame = std::move(m_frames_to_encode.front()); m_frames_to_encode.pop_front(); return frame; } return {}; } encoder_sample get_sample() { std::lock_guard lock(m_audio_mtx); if (!m_samples_to_encode.empty()) { encoder_sample block = std::move(m_samples_to_encode.front()); m_samples_to_encode.pop_front(); return block; } return {}; } }; struct rec_info { vm::ptr cb{}; vm::ptr cbUserData{}; atomic_t state = rec_state::closed; rec_param param{}; // These buffers are used by the game to inject one frame into the recording. // This apparently happens right before a frame is rendered to the screen. // It is only possible to inject a frame if the game has the external input mode enabled. vm::bptr video_input_buffer{}; // Used by the game to inject a frame right before it would render a frame to the screen. vm::bptr audio_input_buffer{}; // Used by the game to inject audio: 2-channel interleaved (left-right) * 256 samples * sizeof(f32) at 48000 kHz // Wrapper for our audio data struct audio_block { // 2-channel interleaved (left-right), 256 samples, float static constexpr usz block_size = rec_channels * CELL_REC_AUDIO_BLOCK_SAMPLES * sizeof(f32); std::array block{}; s64 pts{}; }; std::vector video_ringbuffer; std::vector audio_ringbuffer; usz video_ring_pos = 0; usz audio_ring_pos = 0; usz video_ring_frame_count = 0; usz audio_ring_block_count = 0; usz next_video_ring_pos() { const usz pos = video_ring_pos; video_ring_pos = (video_ring_pos + 1) % video_ringbuffer.size(); return pos; } usz next_audio_ring_pos() { const usz pos = audio_ring_pos; audio_ring_pos = (audio_ring_pos + 1) % audio_ringbuffer.size(); return pos; } std::shared_ptr sink; std::shared_ptr encoder; std::unique_ptr>> video_provider_thread; atomic_t paused = false; // Video parameters utils::video_encoder::frame_format output_format{}; utils::video_encoder::frame_format input_format{}; u32 video_bps = 512000; s32 video_codec_id = 12; // AV_CODEC_ID_MPEG4 s32 max_b_frames = 2; static constexpr u32 fps = rec_framerate; // Always 30 fps // Audio parameters u32 sample_rate = 48000; u32 audio_bps = 64000; s32 audio_codec_id = 86018; // AV_CODEC_ID_AAC static constexpr u32 channels = rec_channels; // Always 2 channels // Recording duration atomic_t recording_time_start = 0; atomic_t pause_time_start = 0; atomic_t pause_time_total = 0; atomic_t recording_time_total = 0; shared_mutex mutex; void set_video_params(s32 video_format); void set_audio_params(s32 audio_format); void start_video_provider(); void pause_video_provider(); void stop_video_provider(bool flush); }; void rec_info::set_video_params(s32 video_format) { const s32 video_type = video_format & 0xf000; const s32 video_quality = video_format & 0xf00; switch (video_format) { case CELL_REC_PARAM_VIDEO_FMT_MPEG4_SMALL_512K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_MPEG4_MIDDLE_512K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_MPEG4_LARGE_512K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_MP_SMALL_512K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_MP_MIDDLE_512K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_BL_SMALL_512K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_BL_MIDDLE_512K_30FPS: video_bps = 512000; break; case CELL_REC_PARAM_VIDEO_FMT_MPEG4_SMALL_768K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_MPEG4_MIDDLE_768K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_MPEG4_LARGE_768K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_MP_SMALL_768K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_MP_MIDDLE_768K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_BL_SMALL_768K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_BL_MIDDLE_768K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_M4HD_SMALL_768K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_M4HD_MIDDLE_768K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_YOUTUBE: video_bps = 768000; break; case CELL_REC_PARAM_VIDEO_FMT_MPEG4_LARGE_1024K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_MP_MIDDLE_1024K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_BL_MIDDLE_1024K_30FPS: video_bps = 1024000; break; case CELL_REC_PARAM_VIDEO_FMT_MPEG4_LARGE_1536K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_MP_MIDDLE_1536K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_BL_MIDDLE_1536K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_M4HD_LARGE_1536K_30FPS: video_bps = 1536000; break; case CELL_REC_PARAM_VIDEO_FMT_MPEG4_LARGE_2048K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_M4HD_LARGE_2048K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_M4HD_HD720_2048K_30FPS: video_bps = 2048000; break; case CELL_REC_PARAM_VIDEO_FMT_MJPEG_SMALL_5000K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_MJPEG_MIDDLE_5000K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_M4HD_HD720_5000K_30FPS: video_bps = 5000000; break; case CELL_REC_PARAM_VIDEO_FMT_MJPEG_LARGE_11000K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_MJPEG_HD720_11000K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_M4HD_HD720_11000K_30FPS: video_bps = 11000000; break; case CELL_REC_PARAM_VIDEO_FMT_MJPEG_HD720_20000K_30FPS: video_bps = 20000000; break; case CELL_REC_PARAM_VIDEO_FMT_MJPEG_HD720_25000K_30FPS: video_bps = 25000000; break; case 0x3791: case 0x37a1: case 0x3790: case 0x37a0: default: // TODO: confirm bitrate video_bps = 512000; break; } max_b_frames = 2; switch (video_type) { default: case VIDEO_TYPE_MPEG4: video_codec_id = 12; // AV_CODEC_ID_MPEG4 break; case VIDEO_TYPE_AVC_MP: video_codec_id = 27; // AV_CODEC_ID_H264 break; case VIDEO_TYPE_AVC_BL: video_codec_id = 27; // AV_CODEC_ID_H264 max_b_frames = 0; // Apparently baseline H264 does not support B-Frames break; case VIDEO_TYPE_MJPEG: video_codec_id = 7; // AV_CODEC_ID_MJPEG break; case VIDEO_TYPE_M4HD: // TODO: no idea if these are H264 or MPEG4 video_codec_id = 12; // AV_CODEC_ID_MPEG4 break; } const bool wide = g_cfg.video.aspect_ratio == video_aspect::_16_9; bool hd = true; switch(g_cfg.video.resolution) { case video_resolution::_1080p: case video_resolution::_1080i: case video_resolution::_720p: case video_resolution::_1600x1080p: case video_resolution::_1440x1080p: case video_resolution::_1280x1080p: case video_resolution::_960x1080p: hd = true; break; case video_resolution::_480p: case video_resolution::_480i: case video_resolution::_576p: case video_resolution::_576i: hd = false; break; } switch (video_quality) { case VIDEO_QUALITY_0: // small input_format.width = wide ? 368 : 320; input_format.height = wide ? 208 : 240; break; case VIDEO_QUALITY_1: // middle input_format.width = wide ? 480 : 368; input_format.height = 272; break; case VIDEO_QUALITY_2: // large input_format.width = wide ? 640 : 480; input_format.height = 368; break; case VIDEO_QUALITY_3: // youtube input_format.width = 320; input_format.height = 240; break; case VIDEO_QUALITY_4: case VIDEO_QUALITY_5: // TODO: break; case VIDEO_QUALITY_6: // HD720 input_format.width = hd ? 1280 : (wide ? 864 : 640); input_format.height = hd ? 720 : 480; break; case VIDEO_QUALITY_7: default: // TODO: input_format.width = 1280; input_format.height = 720; break; } output_format.av_pixel_format = AVPixelFormat::AV_PIX_FMT_YUV420P; output_format.width = input_format.width; output_format.height = input_format.height; output_format.pitch = input_format.width * 4; // unused switch (param.video_input) { case CELL_REC_PARAM_VIDEO_INPUT_ARGB_4_3: case CELL_REC_PARAM_VIDEO_INPUT_ARGB_16_9: input_format.av_pixel_format = AVPixelFormat::AV_PIX_FMT_ARGB; input_format.pitch = input_format.width * 4; break; case CELL_REC_PARAM_VIDEO_INPUT_RGBA_4_3: case CELL_REC_PARAM_VIDEO_INPUT_RGBA_16_9: input_format.av_pixel_format = AVPixelFormat::AV_PIX_FMT_RGBA; input_format.pitch = input_format.width * 4; break; case CELL_REC_PARAM_VIDEO_INPUT_YUV420PLANAR_16_9: input_format.av_pixel_format = AVPixelFormat::AV_PIX_FMT_YUV420P; input_format.pitch = input_format.width * 4; // TODO break; case CELL_REC_PARAM_VIDEO_INPUT_DISABLE: default: input_format.av_pixel_format = AVPixelFormat::AV_PIX_FMT_RGBA; input_format.width = 1280; input_format.height = 720; input_format.pitch = input_format.width * 4; // unused break; } cellRec.notice("set_video_params: video_format=0x%x, video_type=0x%x, video_quality=0x%x, video_bps=%d, video_codec_id=%d, wide=%d, hd=%d, input_format=%s, output_format=%s", video_format, video_type, video_quality, video_bps, video_codec_id, wide, hd, input_format.to_string(), output_format.to_string()); } void rec_info::set_audio_params(s32 audio_format) { // Get the codec switch (audio_format) { case CELL_REC_PARAM_AUDIO_FMT_AAC_96K: case CELL_REC_PARAM_AUDIO_FMT_AAC_128K: case CELL_REC_PARAM_AUDIO_FMT_AAC_64K: audio_codec_id = 86018; // AV_CODEC_ID_AAC break; case CELL_REC_PARAM_AUDIO_FMT_ULAW_384K: case CELL_REC_PARAM_AUDIO_FMT_ULAW_768K: // TODO: no idea what this is audio_codec_id = 65542; // AV_CODEC_ID_PCM_MULAW break; case CELL_REC_PARAM_AUDIO_FMT_PCM_384K: case CELL_REC_PARAM_AUDIO_FMT_PCM_768K: case CELL_REC_PARAM_AUDIO_FMT_PCM_1536K: audio_codec_id = 65556; // AV_CODEC_ID_PCM_F32BE //audio_codec_id = 65557; // AV_CODEC_ID_PCM_F32LE // TODO: maybe this one? break; default: audio_codec_id = 86018; // AV_CODEC_ID_AAC break; } // Get the sampling rate switch (audio_format) { case CELL_REC_PARAM_AUDIO_FMT_AAC_96K: case CELL_REC_PARAM_AUDIO_FMT_AAC_128K: case CELL_REC_PARAM_AUDIO_FMT_AAC_64K: case CELL_REC_PARAM_AUDIO_FMT_PCM_1536K: sample_rate = 48000; break; case CELL_REC_PARAM_AUDIO_FMT_ULAW_384K: case CELL_REC_PARAM_AUDIO_FMT_ULAW_768K: // TODO: no idea what this is sample_rate = 48000; break; case CELL_REC_PARAM_AUDIO_FMT_PCM_384K: sample_rate = 12000; break; case CELL_REC_PARAM_AUDIO_FMT_PCM_768K: sample_rate = 24000; break; default: sample_rate = 48000; break; } // Get the bitrate switch (audio_format) { case CELL_REC_PARAM_AUDIO_FMT_AAC_64K: audio_bps = 64000; break; case CELL_REC_PARAM_AUDIO_FMT_AAC_96K: audio_bps = 96000; break; case CELL_REC_PARAM_AUDIO_FMT_AAC_128K: audio_bps = 128000; break; case CELL_REC_PARAM_AUDIO_FMT_PCM_1536K: audio_bps = 1536000; break; case CELL_REC_PARAM_AUDIO_FMT_ULAW_384K: case CELL_REC_PARAM_AUDIO_FMT_PCM_384K: audio_bps = 384000; break; case CELL_REC_PARAM_AUDIO_FMT_ULAW_768K: case CELL_REC_PARAM_AUDIO_FMT_PCM_768K: audio_bps = 768000; break; default: audio_bps = 96000; break; } cellRec.notice("set_audio_params: audio_format=0x%x, audio_codec_id=%d, sample_rate=%d, audio_bps=%d", audio_format, audio_codec_id, sample_rate, audio_bps); } void rec_info::start_video_provider() { const bool was_paused = paused.exchange(false); if (video_provider_thread && was_paused) { // Resume const u64 pause_time_end = get_system_time(); ensure(pause_time_end > pause_time_start); pause_time_total += (pause_time_end - pause_time_start); if (param.use_internal_video() || param.use_internal_audio()) { utils::video_provider& video_provider = g_fxo->get(); video_provider.set_pause_time_us(pause_time_total); } cellRec.notice("Resuming video provider."); return; } cellRec.notice("Starting video provider."); recording_time_start = get_system_time(); pause_time_start = 0; pause_time_total = 0; if (param.use_internal_video() || param.use_internal_audio()) { utils::video_provider& video_provider = g_fxo->get(); video_provider.set_pause_time_us(0); } video_provider_thread = std::make_unique>>("cellRec video provider", [this]() { const bool use_internal_audio = param.use_internal_audio(); const bool use_external_audio = param.use_external_audio(); const bool use_external_video = !param.use_internal_video(); const bool use_ring_buffer = param.ring_sec > 0; const usz frame_size = input_format.pitch * input_format.height; audio_block buffer_external{}; // for cellRec input audio_block buffer_internal{}; // for cellAudio input s64 last_video_pts = -1; s64 last_audio_pts = -1; cellRec.notice("video_provider_thread: use_ring_buffer=%d, video_ringbuffer_size=%d, audio_ringbuffer_size=%d, ring_sec=%d, frame_size=%d, use_internal_video=%d, use_external_audio=%d, use_internal_audio=%d", use_ring_buffer, video_ringbuffer.size(), audio_ringbuffer.size(), param.ring_sec, frame_size, encoder->use_internal_video, use_external_audio, encoder->use_internal_audio); while (thread_ctrl::state() != thread_state::aborting && encoder) { if (encoder->has_error) { cellRec.error("Encoder error. Sending error status."); sysutil_register_cb([this](ppu_thread& ppu) -> s32 { if (cb) { cb(ppu, CELL_REC_STATUS_ERR, CELL_REC_ERROR_FILE_WRITE, cbUserData); } return CELL_OK; }); return; } if (paused) { thread_ctrl::wait_for(10000); continue; } // We only care for new video frames or audio samples that can be properly encoded, so we check the timestamps and pts. const usz timestamp_ms = (get_system_time() - recording_time_start - pause_time_total) / 1000; ///////////////// // VIDEO // ///////////////// // TODO: wait for flip before adding a frame if (use_external_video) { // The video frames originate from cellRec instead of our render pipeline. if (const s64 pts = encoder->get_pts(timestamp_ms); pts > last_video_pts) { if (video_input_buffer) { if (use_ring_buffer) { // The video frames originate from cellRec and are stored in a ringbuffer. utils::video_sink::encoder_frame& frame_data = video_ringbuffer[next_video_ring_pos()]; frame_data.pts = pts; frame_data.width = input_format.width; frame_data.height = input_format.height; frame_data.av_pixel_format = input_format.av_pixel_format; frame_data.data.resize(frame_size); std::memcpy(frame_data.data.data(), video_input_buffer.get_ptr(), frame_data.data.size()); video_ring_frame_count++; } else { // The video frames originate from cellRec and are pushed to the encoder immediately. std::vector frame(frame_size); std::memcpy(frame.data(), video_input_buffer.get_ptr(), frame.size()); encoder->add_frame(frame, input_format.pitch, input_format.width, input_format.height, input_format.av_pixel_format, timestamp_ms); } } last_video_pts = pts; } } else if (sink) { // The video frames originate from our render pipeline. utils::video_sink::encoder_frame frame = sink->get_frame(); if (const s64 pts = encoder->get_pts(frame.timestamp_ms); pts > last_video_pts && !frame.data.empty()) { if (use_ring_buffer) { // The video frames originate from our render pipeline and are stored in a ringbuffer. frame.pts = pts; video_ringbuffer[next_video_ring_pos()] = std::move(frame); video_ring_frame_count++; } else { // The video frames originate from our render pipeline and are directly encoded by the encoder. encoder->add_frame(frame.data, frame.pitch, frame.width, frame.height, frame.av_pixel_format, frame.timestamp_ms); } last_video_pts = pts; } } ///////////////// // AUDIO // ///////////////// const usz timestamp_us = get_system_time() - recording_time_start - pause_time_total; bool got_new_samples = false; if (use_external_audio) { if (const s64 pts = encoder->get_audio_pts(timestamp_us); pts > last_audio_pts) { if (audio_input_buffer) { // The audio samples originate from cellRec instead of our render pipeline. // TODO: This needs to be synchronized with the game somehow if possible. std::memcpy(buffer_external.block.data(), audio_input_buffer.get_ptr(), buffer_external.block.size()); buffer_external.pts = pts; got_new_samples = true; } last_audio_pts = pts; } } if (sink && use_internal_audio) { // The audio samples originate from cellAudio and are stored in a ringbuffer. utils::video_sink::encoder_sample sample = sink->get_sample(); if (!sample.data.empty() && sample.channels >= channels && sample.sample_count >= CELL_REC_AUDIO_BLOCK_SAMPLES) { s64 pts = encoder->get_audio_pts(sample.timestamp_us); // Each encoder_sample can have more than one block for (u32 i = 0; i < sample.sample_count; i += CELL_REC_AUDIO_BLOCK_SAMPLES) { if (pts > last_audio_pts) { const f32* src = reinterpret_cast(&sample.data[i * sample.channels * sizeof(f32)]); // Copy the new samples to the internal buffer if we need them for volume mixing below. // Otherwise copy them directly to the external buffer which is used for output later. audio_block& dst_buffer = got_new_samples ? buffer_internal : buffer_external; if (sample.channels > channels) { // Downmix channels AudioBackend::downmix(CELL_REC_AUDIO_BLOCK_SAMPLES * sample.channels, sample.channels, audio_channel_layout::stereo, src, reinterpret_cast(dst_buffer.block.data())); } else { std::memcpy(dst_buffer.block.data(), src, audio_block::block_size); } // Mix external and internal audio with param.audio_input_mix_vol if we already got samples from cellRec. if (got_new_samples) { const float volume = std::clamp(param.audio_input_mix_vol / 100.0f, 0.0f, 1.0f); const f32* src = reinterpret_cast(buffer_internal.block.data()); f32* dst = reinterpret_cast(buffer_external.block.data()); for (u32 sample = 0; sample < (CELL_REC_AUDIO_BLOCK_SAMPLES * channels); sample++) { *dst = std::clamp(*dst + (*src++ * volume), -1.0f, 1.0f); ++dst; } } last_audio_pts = std::max(pts, last_audio_pts); // The cellAudio pts may be older than the pts from cellRec buffer_external.pts = last_audio_pts; got_new_samples = true; } // We only take the first sample for simplicity for now break; // Increase pts for each sample block //pts++; } } } if (got_new_samples) { if (use_ring_buffer) { // Copy new sample to ringbuffer audio_ringbuffer[next_audio_ring_pos()] = buffer_external; audio_ring_block_count++; } else { // Push new sample to encoder encoder->add_audio_samples(buffer_external.block.data(), CELL_REC_AUDIO_BLOCK_SAMPLES, channels, timestamp_us); } } // Update recording time recording_time_total = encoder->get_timestamp_ms(encoder->last_video_pts()); thread_ctrl::wait_for(1); } }); } void rec_info::pause_video_provider() { cellRec.notice("Pausing video provider."); if (video_provider_thread) { paused = true; pause_time_start = get_system_time(); } } void rec_info::stop_video_provider(bool flush) { cellRec.notice("Stopping video provider."); if (video_provider_thread) { auto& thread = *video_provider_thread; thread = thread_state::aborting; thread(); video_provider_thread.reset(); } // Flush the ringbuffer if necessary. // This should only happen if the video sink is not the encoder itself. // In this case the encoder should have been idle until now. if (flush && param.ring_sec > 0 && (!video_ringbuffer.empty() || !audio_ringbuffer.empty())) { cellRec.notice("Flushing video ringbuffer."); // Fill encoder with data from ringbuffer // TODO: ideally the encoder should do this on the fly and overwrite old frames in the file. ensure(encoder); encoder->encode(); const usz frame_count = std::min(video_ringbuffer.size(), video_ring_frame_count); const usz video_start_offset = video_ring_frame_count < video_ringbuffer.size() ? 0 : video_ring_frame_count; const s64 video_start_pts = video_ringbuffer.empty() ? 0 : video_ringbuffer[video_start_offset % video_ringbuffer.size()].pts; const usz block_count = std::min(audio_ringbuffer.size(), audio_ring_block_count); const usz audio_start_offset = audio_ring_block_count < audio_ringbuffer.size() ? 0 : audio_ring_block_count; const s64 audio_start_pts = audio_ringbuffer.empty() ? 0 : audio_ringbuffer[audio_start_offset % audio_ringbuffer.size()].pts; // Try to add the frames and samples in proper order for (usz sync_timestamp_us = 0, frame = 0, block = 0; frame < frame_count || block < block_count; frame++) { // Add one frame if (frame < frame_count) { const usz pos = (video_start_offset + frame) % video_ringbuffer.size(); utils::video_sink::encoder_frame& frame_data = video_ringbuffer[pos]; const usz timestamp_ms = encoder->get_timestamp_ms(frame_data.pts - video_start_pts); encoder->add_frame(frame_data.data, frame_data.pitch, frame_data.width, frame_data.height, frame_data.av_pixel_format, timestamp_ms); // Increase sync timestamp sync_timestamp_us = timestamp_ms * 1000; } // Add all the samples that fit into the last frame for (usz i = block; i < block_count; i++) { const usz pos = (audio_start_offset + i) % audio_ringbuffer.size(); const audio_block& sample_block = audio_ringbuffer[pos]; const usz timestamp_us = encoder->get_audio_timestamp_us(sample_block.pts - audio_start_pts); // Stop adding new samples if the sync timestamp is exceeded, unless we already added all the frames. if (timestamp_us > sync_timestamp_us && frame < frame_count) { break; } encoder->add_audio_samples(sample_block.block.data(), CELL_REC_AUDIO_BLOCK_SAMPLES, channels, timestamp_us); block++; } } video_ringbuffer.clear(); audio_ringbuffer.clear(); } } bool create_path(std::string& out, std::string dir_name, std::string file_name) { out.clear(); if (dir_name.size() + file_name.size() > CELL_REC_MAX_PATH_LEN) { return false; } out = dir_name; if (!out.empty() && out.back() != '/') { out += '/'; } out += file_name; return true; } u32 cellRecQueryMemSize(vm::cptr pParam); // Forward declaration error_code cellRecOpen(vm::cptr pDirName, vm::cptr pFileName, vm::cptr pParam, u32 container, vm::ptr cb, vm::ptr cbUserData) { cellRec.todo("cellRecOpen(pDirName=%s, pFileName=%s, pParam=*0x%x, container=0x%x, cb=*0x%x, cbUserData=*0x%x)", pDirName, pFileName, pParam, container, cb, cbUserData); auto& rec = g_fxo->get(); if (rec.state != rec_state::closed) { return CELL_REC_ERROR_INVALID_STATE; } if (!pParam || !pDirName || !pFileName) { return CELL_REC_ERROR_INVALID_VALUE; } std::string options; for (s32 i = 0; i < pParam->numOfOpt; i++) { if (i > 0) options += ", "; fmt::append(options, "%d", pParam->pOpt[i].option); } cellRec.notice("cellRecOpen: pParam={ videoFmt=0x%x, audioFmt=0x%x, numOfOpt=0x%x, options=[ %s ] }", pParam->videoFmt, pParam->audioFmt, pParam->numOfOpt, options); const u32 mem_size = cellRecQueryMemSize(pParam); if (container == SYS_MEMORY_CONTAINER_ID_INVALID) { if (mem_size != 0) { return CELL_REC_ERROR_INVALID_VALUE; } } else { // NOTE: Most likely tries to allocate a complete container. But we use g_fxo instead. // TODO: how much data do we actually need ? rec.video_input_buffer = vm::cast(vm::alloc(mem_size, vm::main)); rec.audio_input_buffer = vm::cast(vm::alloc(mem_size, vm::main)); if (!rec.video_input_buffer || !rec.audio_input_buffer) { return CELL_REC_ERROR_OUT_OF_MEMORY; } } switch (pParam->videoFmt) { case CELL_REC_PARAM_VIDEO_FMT_MPEG4_SMALL_512K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_MPEG4_SMALL_768K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_MPEG4_MIDDLE_512K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_MPEG4_MIDDLE_768K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_MPEG4_LARGE_512K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_MPEG4_LARGE_768K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_MPEG4_LARGE_1024K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_MPEG4_LARGE_1536K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_MPEG4_LARGE_2048K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_MP_SMALL_512K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_MP_SMALL_768K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_MP_MIDDLE_512K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_MP_MIDDLE_768K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_MP_MIDDLE_1024K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_MP_MIDDLE_1536K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_BL_SMALL_512K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_BL_SMALL_768K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_BL_MIDDLE_512K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_BL_MIDDLE_768K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_BL_MIDDLE_1024K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_AVC_BL_MIDDLE_1536K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_MJPEG_SMALL_5000K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_MJPEG_MIDDLE_5000K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_MJPEG_LARGE_11000K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_MJPEG_HD720_11000K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_MJPEG_HD720_20000K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_MJPEG_HD720_25000K_30FPS: case 0x3791: case 0x37a1: case 0x3790: case 0x37a0: case CELL_REC_PARAM_VIDEO_FMT_M4HD_SMALL_768K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_M4HD_MIDDLE_768K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_M4HD_LARGE_1536K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_M4HD_LARGE_2048K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_M4HD_HD720_2048K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_M4HD_HD720_5000K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_M4HD_HD720_11000K_30FPS: case CELL_REC_PARAM_VIDEO_FMT_YOUTUBE: break; default: return CELL_REC_ERROR_INVALID_VALUE; } const s32 video_type = pParam->videoFmt & 0xf000; const s32 video_quality = pParam->videoFmt & 0xf00; switch (pParam->audioFmt) { case CELL_REC_PARAM_AUDIO_FMT_AAC_96K: case CELL_REC_PARAM_AUDIO_FMT_AAC_128K: case CELL_REC_PARAM_AUDIO_FMT_AAC_64K: { // Do not allow MJPEG or AVC_MP video format if (video_type == VIDEO_TYPE_AVC_MP || video_type == VIDEO_TYPE_MJPEG) { return CELL_REC_ERROR_INVALID_VALUE; } [[fallthrough]]; } case CELL_REC_PARAM_AUDIO_FMT_ULAW_384K: case CELL_REC_PARAM_AUDIO_FMT_ULAW_768K: { // Do not allow AVC_MP video format if (video_type == VIDEO_TYPE_AVC_MP) { return CELL_REC_ERROR_INVALID_VALUE; } break; } case CELL_REC_PARAM_AUDIO_FMT_PCM_384K: case CELL_REC_PARAM_AUDIO_FMT_PCM_768K: case CELL_REC_PARAM_AUDIO_FMT_PCM_1536K: { // Only allow MJPEG video format if (video_type != VIDEO_TYPE_MJPEG) { return CELL_REC_ERROR_INVALID_VALUE; } break; } default: return CELL_REC_ERROR_INVALID_VALUE; } rec.param = {}; u32 spurs_param = 0; bool abort_loop = false; for (s32 i = 0; i < pParam->numOfOpt; i++) { const CellRecOption& opt = pParam->pOpt[i]; switch (opt.option) { case CELL_REC_OPTION_PPU_THREAD_PRIORITY: { if (opt.value.ppu_thread_priority > 0xbff) { return CELL_REC_ERROR_INVALID_VALUE; } rec.param.ppu_thread_priority = opt.value.ppu_thread_priority; break; } case CELL_REC_OPTION_SPU_THREAD_PRIORITY: { if (opt.value.spu_thread_priority - 0x10U > 0xef) { return CELL_REC_ERROR_INVALID_VALUE; } rec.param.spu_thread_priority = opt.value.spu_thread_priority; break; } case CELL_REC_OPTION_CAPTURE_PRIORITY: { rec.param.capture_priority = opt.value.capture_priority; break; } case CELL_REC_OPTION_USE_SYSTEM_SPU: { if (video_quality == VIDEO_QUALITY_6 || video_quality == VIDEO_QUALITY_7) { // TODO: Seems differ if video_quality is VIDEO_QUALITY_6 or VIDEO_QUALITY_7 } rec.param.use_system_spu = opt.value.use_system_spu; break; } case CELL_REC_OPTION_FIT_TO_YOUTUBE: { rec.param.fit_to_youtube = opt.value.fit_to_youtube; break; } case CELL_REC_OPTION_XMB_BGM: { rec.param.xmb_bgm = opt.value.xmb_bgm; break; } case CELL_REC_OPTION_RING_SEC: { rec.param.ring_sec = opt.value.ring_sec; break; } case CELL_REC_OPTION_MPEG4_FAST_ENCODE: { rec.param.mpeg4_fast_encode = opt.value.mpeg4_fast_encode; break; } case CELL_REC_OPTION_VIDEO_INPUT: { const u32 v_input = (opt.value.video_input & 0xffU); if (v_input > CELL_REC_PARAM_VIDEO_INPUT_YUV420PLANAR_16_9) { return CELL_REC_ERROR_INVALID_VALUE; } rec.param.video_input = v_input; break; } case CELL_REC_OPTION_AUDIO_INPUT: { rec.param.audio_input = opt.value.audio_input; if (opt.value.audio_input == CELL_REC_PARAM_AUDIO_INPUT_DISABLE) { rec.param.audio_input_mix_vol = CELL_REC_PARAM_AUDIO_INPUT_MIX_VOL_MIN; } else { rec.param.audio_input_mix_vol = CELL_REC_PARAM_AUDIO_INPUT_MIX_VOL_MAX; } break; } case CELL_REC_OPTION_AUDIO_INPUT_MIX_VOL: { rec.param.audio_input_mix_vol = opt.value.audio_input_mix_vol; break; } case CELL_REC_OPTION_REDUCE_MEMSIZE: { rec.param.reduce_memsize = opt.value.reduce_memsize != CELL_REC_PARAM_REDUCE_MEMSIZE_DISABLE; break; } case CELL_REC_OPTION_SHOW_XMB: { rec.param.show_xmb = (opt.value.show_xmb != 0); break; } case CELL_REC_OPTION_METADATA_FILENAME: { if (opt.value.metadata_filename) { std::string path; if (!create_path(path, pDirName.get_ptr(), opt.value.metadata_filename.get_ptr())) { return CELL_REC_ERROR_INVALID_VALUE; } rec.param.metadata_filename = path; } else { abort_loop = true; // TODO: Apparently this doesn't return an error but goes to the callback section instead... } break; } case CELL_REC_OPTION_SPURS: { spurs_param = opt.value.pSpursParam.addr(); if (!opt.value.pSpursParam || !opt.value.pSpursParam->pSpurs) // TODO: check both or only pSpursParam ? { return CELL_REC_ERROR_INVALID_VALUE; } if (opt.value.pSpursParam->spu_usage_rate < 1 || opt.value.pSpursParam->spu_usage_rate > 100) { return CELL_REC_ERROR_INVALID_VALUE; } rec.param.spurs_param.spu_usage_rate = opt.value.pSpursParam->spu_usage_rate; [[fallthrough]]; } case 100: { if (spurs_param) { // TODO: } break; } default: { cellRec.warning("cellRecOpen: unknown option %d", opt.option); break; } } if (abort_loop) { break; } } // NOTE: The abort_loop variable I chose is actually a goto to the callback section at the end of the function. if (!abort_loop) { if (rec.param.video_input != CELL_REC_PARAM_VIDEO_INPUT_DISABLE) { if (video_type == VIDEO_TYPE_MJPEG || video_type == VIDEO_TYPE_M4HD) { if (rec.param.video_input != CELL_REC_PARAM_VIDEO_INPUT_YUV420PLANAR_16_9) { return CELL_REC_ERROR_INVALID_VALUE; } } else if (rec.param.video_input == CELL_REC_PARAM_VIDEO_INPUT_YUV420PLANAR_16_9) { return CELL_REC_ERROR_INVALID_VALUE; } } if (!create_path(rec.param.filename, pDirName.get_ptr(), pFileName.get_ptr())) { return CELL_REC_ERROR_INVALID_VALUE; } if (!rec.param.filename.starts_with("/dev_hdd0") && !rec.param.filename.starts_with("/dev_hdd1")) { return CELL_REC_ERROR_INVALID_VALUE; } //if (spurs_param) //{ // if (error_code error = cellSpurs_0xD9A9C592(); // { // return error; // } //} } if (!cb) { return CELL_REC_ERROR_INVALID_VALUE; } cellRec.notice("cellRecOpen: Using parameters: %s", rec.param.to_string()); rec.cb = cb; rec.cbUserData = cbUserData; rec.video_ringbuffer.clear(); rec.audio_ringbuffer.clear(); rec.video_ring_pos = 0; rec.audio_ring_pos = 0; rec.video_ring_frame_count = 0; rec.audio_ring_block_count = 0; rec.recording_time_start = 0; rec.recording_time_total = 0; rec.pause_time_start = 0; rec.pause_time_total = 0; rec.paused = false; rec.set_video_params(pParam->videoFmt); rec.set_audio_params(pParam->audioFmt); if (rec.param.ring_sec > 0) { const usz audio_ring_buffer_size = static_cast(std::ceil((rec.param.ring_sec * rec.sample_rate) / static_cast(CELL_REC_AUDIO_BLOCK_SAMPLES))); const usz video_ring_buffer_size = rec.param.ring_sec * rec.fps; cellRec.notice("Preparing ringbuffer for %d seconds. video_ring_buffer_size=%d, audio_ring_buffer_size=%d, pitch=%d, width=%d, height=%d", rec.param.ring_sec, video_ring_buffer_size, audio_ring_buffer_size, rec.input_format.pitch, rec.input_format.width, rec.input_format.height); rec.audio_ringbuffer.resize(audio_ring_buffer_size); rec.video_ringbuffer.resize(video_ring_buffer_size); } if (rec.param.use_internal_audio() || rec.param.use_internal_video()) { rec.sink = std::make_shared(); rec.sink->use_internal_audio = rec.param.use_internal_audio(); rec.sink->use_internal_video = rec.param.use_internal_video(); rec.sink->set_sample_rate(rec.sample_rate); } rec.encoder = std::make_shared(); rec.encoder->use_internal_audio = false; // We use the other sink rec.encoder->use_internal_video = false; // We use the other sink rec.encoder->set_path(vfs::get(rec.param.filename)); rec.encoder->set_framerate(rec.fps); rec.encoder->set_video_bitrate(rec.video_bps); rec.encoder->set_video_codec(rec.video_codec_id); rec.encoder->set_sample_rate(rec.sample_rate); rec.encoder->set_audio_channels(rec.channels); rec.encoder->set_audio_bitrate(rec.audio_bps); rec.encoder->set_audio_codec(rec.audio_codec_id); rec.encoder->set_output_format(rec.output_format); sysutil_register_cb([&rec](ppu_thread& ppu) -> s32 { rec.state = rec_state::stopped; rec.cb(ppu, CELL_REC_STATUS_OPEN, CELL_OK, rec.cbUserData); return CELL_OK; }); return CELL_OK; } error_code cellRecClose(s32 isDiscard) { cellRec.todo("cellRecClose(isDiscard=0x%x)", isDiscard); auto& rec = g_fxo->get(); if (rec.state == rec_state::closed) { return CELL_REC_ERROR_INVALID_STATE; } sysutil_register_cb([=, &rec](ppu_thread& ppu) -> s32 { bool is_valid_range = true; if (isDiscard) { // No need to flush the encoder rec.stop_video_provider(false); rec.encoder->stop(false); if (rec.sink) { rec.sink->stop(true); } if (fs::is_file(rec.param.filename)) { cellRec.warning("cellRecClose: removing discarded recording '%s'", rec.param.filename); if (!fs::remove_file(rec.param.filename)) { cellRec.error("cellRecClose: failed to remove recording '%s'", rec.param.filename); } } } else { // Flush to make sure we encode all remaining frames rec.stop_video_provider(true); rec.encoder->stop(true); rec.recording_time_total = rec.encoder->get_timestamp_ms(rec.encoder->last_video_pts()); if (rec.sink) { rec.sink->stop(true); } const s64 start_pts = rec.encoder->get_pts(rec.param.scene_metadata.start_time); const s64 end_pts = rec.encoder->get_pts(rec.param.scene_metadata.end_time); const s64 last_pts = rec.encoder->last_video_pts(); is_valid_range = start_pts >= 0 && end_pts <= last_pts; } vm::dealloc(rec.video_input_buffer.addr(), vm::main); vm::dealloc(rec.audio_input_buffer.addr(), vm::main); // Release the video sink if it was used if (rec.param.use_internal_video() || rec.param.use_internal_audio()) { const recording_mode old_mode = g_recording_mode.exchange(recording_mode::stopped); if (old_mode == recording_mode::rpcs3) { cellRec.error("cellRecClose: Unexpected recording mode %s found while stopping video capture.", old_mode); } utils::video_provider& video_provider = g_fxo->get(); if (!video_provider.set_video_sink(nullptr, recording_mode::cell)) { cellRec.error("cellRecClose failed to release video sink"); } } rec.param = {}; rec.encoder.reset(); rec.sink.reset(); rec.audio_ringbuffer.clear(); rec.video_ringbuffer.clear(); rec.state = rec_state::closed; if (is_valid_range) { rec.cb(ppu, CELL_REC_STATUS_CLOSE, CELL_OK, rec.cbUserData); } else { rec.cb(ppu, CELL_REC_STATUS_ERR, CELL_REC_ERROR_FILE_NO_DATA, rec.cbUserData); } return CELL_OK; }); return CELL_OK; } error_code cellRecStop() { cellRec.todo("cellRecStop()"); auto& rec = g_fxo->get(); if (rec.state != rec_state::started) { return CELL_REC_ERROR_INVALID_STATE; } sysutil_register_cb([&rec](ppu_thread& ppu) -> s32 { // cellRecStop actually just pauses the recording rec.pause_video_provider(); if (rec.sink) { rec.sink->pause(true); } ensure(!!rec.encoder); rec.encoder->pause(true); rec.recording_time_total = rec.encoder->get_timestamp_ms(rec.encoder->last_video_pts()); rec.state = rec_state::stopped; rec.cb(ppu, CELL_REC_STATUS_STOP, CELL_OK, rec.cbUserData); return CELL_OK; }); return CELL_OK; } error_code cellRecStart() { cellRec.todo("cellRecStart()"); auto& rec = g_fxo->get(); if (rec.state != rec_state::stopped) { return CELL_REC_ERROR_INVALID_STATE; } sysutil_register_cb([&rec](ppu_thread& ppu) -> s32 { // Start/resume the recording ensure(!!rec.encoder); if (rec.param.ring_sec == 0) { rec.encoder->encode(); } // Setup a video sink if it is needed if (rec.param.use_internal_video() || rec.param.use_internal_audio()) { utils::video_provider& video_provider = g_fxo->get(); if (rec.sink && !video_provider.set_video_sink(rec.sink, recording_mode::cell)) { cellRec.error("Failed to set video sink"); rec.cb(ppu, CELL_REC_STATUS_ERR, CELL_REC_ERROR_FATAL, rec.cbUserData); return CELL_OK; } // Force rsx recording g_recording_mode = recording_mode::cell; } else { // Force stop rsx recording g_recording_mode = recording_mode::stopped; } rec.start_video_provider(); if (rec.sink) { rec.sink->resume(); } if (rec.encoder->has_error) { rec.cb(ppu, CELL_REC_STATUS_ERR, CELL_REC_ERROR_FILE_OPEN, rec.cbUserData); } else { rec.state = rec_state::started; rec.cb(ppu, CELL_REC_STATUS_START, CELL_OK, rec.cbUserData); } return CELL_OK; }); return CELL_OK; } u32 cellRecQueryMemSize(vm::cptr pParam) { cellRec.notice("cellRecQueryMemSize(pParam=*0x%x)", pParam); if (!pParam) { return 0x900000; } u32 video_size = 0x600000; // 6 MB u32 audio_size = 0x100000; // 1 MB u32 external_input_size = 0; u32 reduce_memsize = 0; s32 video_type = pParam->videoFmt & 0xf000; s32 video_quality = pParam->videoFmt & 0xf00; switch (video_type) { case VIDEO_TYPE_MPEG4: { switch (video_quality) { case VIDEO_QUALITY_0: case VIDEO_QUALITY_3: video_size = 0x600000; // 6 MB break; case VIDEO_QUALITY_1: video_size = 0x700000; // 7 MB break; case VIDEO_QUALITY_2: video_size = 0x800000; // 8 MB break; default: video_size = 0x900000; // 9 MB break; } audio_size = 0x100000; // 1 MB break; } case VIDEO_TYPE_AVC_BL: case VIDEO_TYPE_AVC_MP: { switch (video_quality) { case VIDEO_QUALITY_0: case VIDEO_QUALITY_3: video_size = 0x800000; // 8 MB break; default: video_size = 0x900000; // 9 MB break; } audio_size = 0x100000; // 1 MB break; } case VIDEO_TYPE_M4HD: { switch (video_quality) { case VIDEO_QUALITY_0: case VIDEO_QUALITY_1: video_size = 0x600000; // 6 MB break; case VIDEO_QUALITY_2: video_size = 0x700000; // 7 MB break; default: video_size = 0x1000000; // 16 MB break; } audio_size = 0x100000; // 1 MB break; } default: { switch (video_quality) { case VIDEO_QUALITY_0: video_size = 0x300000; // 3 MB break; case VIDEO_QUALITY_1: case VIDEO_QUALITY_2: video_size = 0x400000; // 4 MB break; case VIDEO_QUALITY_6: video_size = 0x700000; // 7 MB break; default: video_size = 0xd00000; // 14 MB break; } audio_size = 0; // 0 MB break; } } for (s32 i = 0; i < pParam->numOfOpt; i++) { const CellRecOption& opt = pParam->pOpt[i]; switch (opt.option) { case CELL_REC_OPTION_REDUCE_MEMSIZE: { if (opt.value.reduce_memsize == CELL_REC_PARAM_REDUCE_MEMSIZE_DISABLE) { reduce_memsize = 0; // 0 MB } else { if (video_type == VIDEO_TYPE_M4HD && (video_quality == VIDEO_QUALITY_1 || video_quality == VIDEO_QUALITY_2)) { reduce_memsize = 0x200000; // 2 MB } else { reduce_memsize = 0x300000; // 3 MB } } break; } case CELL_REC_OPTION_VIDEO_INPUT: case CELL_REC_OPTION_AUDIO_INPUT: { if (opt.value.audio_input != CELL_REC_PARAM_AUDIO_INPUT_DISABLE) { if (video_type == VIDEO_TYPE_MJPEG || (video_type == VIDEO_TYPE_M4HD && video_quality != VIDEO_QUALITY_6)) { external_input_size = 0x100000; // 1MB } } break; } case CELL_REC_OPTION_AUDIO_INPUT_MIX_VOL: { // NOTE: Doesn't seem to check opt.value.audio_input if (video_type == VIDEO_TYPE_MJPEG || (video_type == VIDEO_TYPE_M4HD && video_quality != VIDEO_QUALITY_6)) { external_input_size = 0x100000; // 1MB } break; } default: break; } } u32 size = video_size + audio_size + external_input_size; if (size > reduce_memsize) { size -= reduce_memsize; } else { size = 0; } return size; } void cellRecGetInfo(s32 info, vm::ptr pValue) { cellRec.todo("cellRecGetInfo(info=0x%x, pValue=*0x%x)", info, pValue); if (!pValue) { return; } *pValue = 0; auto& rec = g_fxo->get(); switch (info) { case CELL_REC_INFO_VIDEO_INPUT_ADDR: { *pValue = rec.video_input_buffer.addr(); break; } case CELL_REC_INFO_VIDEO_INPUT_WIDTH: { *pValue = rec.input_format.width; break; } case CELL_REC_INFO_VIDEO_INPUT_PITCH: { *pValue = rec.input_format.pitch; break; } case CELL_REC_INFO_VIDEO_INPUT_HEIGHT: { *pValue = rec.input_format.height; break; } case CELL_REC_INFO_AUDIO_INPUT_ADDR: { *pValue = rec.audio_input_buffer.addr(); break; } case CELL_REC_INFO_MOVIE_TIME_MSEC: { if (rec.state == rec_state::stopped) { *pValue = rec.recording_time_total; } break; } case CELL_REC_INFO_SPURS_SYSTEMWORKLOAD_ID: { if (rec.param.spurs_param.pSpurs) { // TODO //cellSpurs_0xE279681F(); } *pValue = 0xffffffff; break; } default: { break; } } } error_code cellRecSetInfo(s32 setInfo, u64 value) { cellRec.todo("cellRecSetInfo(setInfo=0x%x, value=0x%x)", setInfo, value); auto& rec = g_fxo->get(); if (rec.state != rec_state::stopped) { return CELL_REC_ERROR_INVALID_STATE; } switch (setInfo) { case CELL_REC_SETINFO_MOVIE_START_TIME_MSEC: { // TODO: check if this is actually identical to scene metadata rec.param.scene_metadata.start_time = value; cellRec.notice("cellRecSetInfo: changed movie start time to %d", value); break; } case CELL_REC_SETINFO_MOVIE_END_TIME_MSEC: { // TODO: check if this is actually identical to scene metadata rec.param.scene_metadata.end_time = value; cellRec.notice("cellRecSetInfo: changed movie end time to %d", value); break; } case CELL_REC_SETINFO_MOVIE_META: { if (!value) { return CELL_REC_ERROR_INVALID_VALUE; } vm::ptr movie_metadata = vm::cast(value); if (!movie_metadata || !movie_metadata->gameTitle || !movie_metadata->movieTitle || strnlen(movie_metadata->gameTitle.get_ptr(), CELL_REC_MOVIE_META_GAME_TITLE_LEN) >= CELL_REC_MOVIE_META_GAME_TITLE_LEN || strnlen(movie_metadata->movieTitle.get_ptr(), CELL_REC_MOVIE_META_MOVIE_TITLE_LEN) >= CELL_REC_MOVIE_META_MOVIE_TITLE_LEN) { return CELL_REC_ERROR_INVALID_VALUE; } if ((movie_metadata->description && strnlen(movie_metadata->description.get_ptr(), CELL_REC_MOVIE_META_DESCRIPTION_LEN) >= CELL_REC_MOVIE_META_DESCRIPTION_LEN) || (movie_metadata->userdata && strnlen(movie_metadata->userdata.get_ptr(), CELL_REC_MOVIE_META_USERDATA_LEN) >= CELL_REC_MOVIE_META_USERDATA_LEN)) { return CELL_REC_ERROR_INVALID_VALUE; } rec.param.movie_metadata = {}; rec.param.movie_metadata.game_title = std::string{movie_metadata->gameTitle.get_ptr()}; rec.param.movie_metadata.movie_title = std::string{movie_metadata->movieTitle.get_ptr()}; if (movie_metadata->description) { rec.param.movie_metadata.description = std::string{movie_metadata->description.get_ptr()}; } if (movie_metadata->userdata) { rec.param.movie_metadata.userdata = std::string{movie_metadata->userdata.get_ptr()}; } cellRec.notice("cellRecSetInfo: changed movie metadata to %s", rec.param.movie_metadata.to_string()); break; } case CELL_REC_SETINFO_SCENE_META: { if (!value) { return CELL_REC_ERROR_INVALID_VALUE; } vm::ptr scene_metadata = vm::cast(value); if (!scene_metadata || !scene_metadata->title || scene_metadata->type > CELL_REC_SCENE_META_TYPE_CLIP_USER || scene_metadata->tagNum > CELL_REC_SCENE_META_TAG_NUM || strnlen(scene_metadata->title.get_ptr(), CELL_REC_SCENE_META_TITLE_LEN) >= CELL_REC_SCENE_META_TITLE_LEN) { return CELL_REC_ERROR_INVALID_VALUE; } for (u32 i = 0; i < scene_metadata->tagNum; i++) { if (!scene_metadata->tag[i] || strnlen(scene_metadata->tag[i].get_ptr(), CELL_REC_SCENE_META_TAG_LEN) >= CELL_REC_SCENE_META_TAG_LEN) { return CELL_REC_ERROR_INVALID_VALUE; } } rec.param.scene_metadata = {}; rec.param.scene_metadata.is_set = true; rec.param.scene_metadata.type = scene_metadata->type; rec.param.scene_metadata.start_time = scene_metadata->startTime; rec.param.scene_metadata.end_time = scene_metadata->endTime; rec.param.scene_metadata.title = std::string{scene_metadata->title.get_ptr()}; rec.param.scene_metadata.tags.resize(scene_metadata->tagNum); for (u32 i = 0; i < scene_metadata->tagNum; i++) { if (scene_metadata->tag[i]) { rec.param.scene_metadata.tags[i] = std::string{scene_metadata->tag[i].get_ptr()}; } } cellRec.notice("cellRecSetInfo: changed scene metadata to %s", rec.param.scene_metadata.to_string()); break; } default: { return CELL_REC_ERROR_INVALID_VALUE; } } return CELL_OK; } DECLARE(ppu_module_manager::cellRec)("cellRec", []() { REG_FUNC(cellRec, cellRecOpen); REG_FUNC(cellRec, cellRecClose); REG_FUNC(cellRec, cellRecGetInfo); REG_FUNC(cellRec, cellRecStop); REG_FUNC(cellRec, cellRecStart); REG_FUNC(cellRec, cellRecQueryMemSize); REG_FUNC(cellRec, cellRecSetInfo); });