diff --git a/audio/audio_driver.c b/audio/audio_driver.c index 7ab3782ade80..1e0b917a25b0 100644 --- a/audio/audio_driver.c +++ b/audio/audio_driver.c @@ -9092,9 +9092,15 @@ void audio_driver_menu_sample(bool by_clock) if ((AUDIO_FLAGS_GET(audio_st) & AUDIO_FLAG_SUSPENDED)) check_flush = false; + /* Record Game Only: the menu's filler silence (and so the time spent + * in the menu) stays out of the recording, like its frames do. */ + if (settings->bools.video_record_game_only) + recording_st = NULL; + while (sample_count > 1024) { - if ( recording_st->data && + if ( recording_st && + recording_st->data && recording_st->driver && recording_st->driver->push_audio) { @@ -9117,7 +9123,8 @@ void audio_driver_menu_sample(bool by_clock) sample_count -= 1024; } - if ( recording_st->data + if ( recording_st + && recording_st->data && recording_st->driver && recording_st->driver->push_audio) { diff --git a/config.def.h b/config.def.h index ac7dde5a5c38..a42a42049ada 100644 --- a/config.def.h +++ b/config.def.h @@ -1196,6 +1196,27 @@ /* Record post-shaded GPU output instead of raw game footage if available. */ #define DEFAULT_GPU_RECORD false +/* Drop recorded video frames when the encoder queue is full, instead + * of stalling the frontend until it drains. */ +#define DEFAULT_VIDEO_RECORD_ALLOW_FRAME_DROP true + +/* Record only the game: GPU recordings leave out the menu, OSD, + * widgets and overlays, and menu time is left out of recordings. */ +#define DEFAULT_VIDEO_RECORD_GAME_ONLY false + +/* Use a hardware H.264 encoder for recording/streaming when the FFmpeg + * build has one that works, falling back to libx264. */ +#ifdef HAVE_LAKKA_SWITCH +#define DEFAULT_VIDEO_RECORD_HW_ENCODER true +#else +#define DEFAULT_VIDEO_RECORD_HW_ENCODER false +#endif + +/* Depth of the recording video queue, in frames. */ +#define DEFAULT_VIDEO_RECORD_FIFO_FRAMES 32 +#define MIN_VIDEO_RECORD_FIFO_FRAMES 8 +#define MAX_VIDEO_RECORD_FIFO_FRAMES 128 + /* Watch shader files for changes and auto-apply as necessary. */ #define DEFAULT_VIDEO_SHADER_WATCH_FILES false diff --git a/configuration.h b/configuration.h index 8f69a2d2e5d9..647cc2d6ac27 100644 --- a/configuration.h +++ b/configuration.h @@ -426,6 +426,7 @@ typedef struct settings unsigned window_auto_dims_max; unsigned video_record_threads; + unsigned video_record_fifo_frames; unsigned libnx_overclock; unsigned ai_service_mode; @@ -669,6 +670,9 @@ typedef struct settings bool video_disable_composition; bool video_post_filter_record; bool video_gpu_record; + bool video_record_allow_frame_drop; + bool video_record_game_only; + bool video_record_hw_encoder; bool video_gpu_screenshot; bool video_allow_rotate; bool video_shared_context; diff --git a/gfx/drivers/gl1.c b/gfx/drivers/gl1.c index 4458db32fa59..3b8acc918a41 100644 --- a/gfx/drivers/gl1.c +++ b/gfx/drivers/gl1.c @@ -2867,13 +2867,12 @@ static void gl1_viewport_info(void *data, struct video_viewport *vp) VIDEO_POS_PUT_Y(vp->pos, top_dist); } -static bool gl1_read_viewport(void *data, uint8_t *buffer, bool is_idle) +/* bgrx: write bottom-up 32-bit BGRX (read_viewport_bgrx) instead of + * bottom-up BGR24. */ +static bool gl1_read_viewport_internal(gl1_t *gl1, uint8_t *buffer, + bool is_idle, bool bgrx) { unsigned num_pixels = 0; - gl1_t *gl1 = (gl1_t*)data; - - if (!gl1) - return false; num_pixels = VIDEO_SCALE_AREA(gl1->vp.dims); gl1->readback_buffer_screenshot = malloc(num_pixels * sizeof(uint32_t)); @@ -2895,13 +2894,19 @@ static bool gl1_read_viewport(void *data, uint8_t *buffer, bool is_idle) unsigned vd_h = VIDEO_SCALE_H(gl1->vp.full_dims); unsigned rb_w = (VIDEO_SCALE_W(gl1->vp.dims) > vd_w) ? vd_w : VIDEO_SCALE_W(gl1->vp.dims); unsigned rb_h = (VIDEO_SCALE_H(gl1->vp.dims) > vd_h) ? vd_h : VIDEO_SCALE_H(gl1->vp.dims); - video_frame_convert_rgba_to_bgr( - (const void*)gl1->readback_buffer_screenshot, - buffer, - rb_w * sizeof(uint32_t), - rb_w * 3, - rb_w, - rb_h); + if (bgrx) + video_frame_copy_to_bgrx(buffer, VIDEO_SCALE_W(gl1->vp.dims), + VIDEO_SCALE_H(gl1->vp.dims), + (const uint8_t*)gl1->readback_buffer_screenshot, + rb_w * 4, rb_w, rb_h, true); + else + video_frame_convert_rgba_to_bgr( + (const void*)gl1->readback_buffer_screenshot, + buffer, + rb_w * sizeof(uint32_t), + rb_w * 3, + rb_w, + rb_h); } free(gl1->readback_buffer_screenshot); @@ -2910,6 +2915,25 @@ static bool gl1_read_viewport(void *data, uint8_t *buffer, bool is_idle) return true; } +static bool gl1_read_viewport(void *data, uint8_t *buffer, bool is_idle) +{ + gl1_t *gl1 = (gl1_t*)data; + if (!gl1) + return false; + return gl1_read_viewport_internal(gl1, buffer, is_idle, false); +} + +static bool gl1_read_viewport_bgrx(void *data, uint8_t *buffer, + bool is_idle, bool *bottom_up) +{ + gl1_t *gl1 = (gl1_t*)data; + if (!gl1) + return false; + /* glReadPixels returns the bottom row first. */ + *bottom_up = true; + return gl1_read_viewport_internal(gl1, buffer, is_idle, true); +} + static void gl1_set_texture_frame(void *data, const void *frame, bool rgb32, unsigned dims, float alpha) @@ -3664,7 +3688,13 @@ video_driver_t video_gl1 = { #endif NULL, /* invalidate_hw_render_cache */ gl1_read_viewport_hdr, - &gl1_raster_font + &gl1_raster_font, +#ifdef HAVE_OPENXR + NULL, /* get_vr_frame_state */ + NULL, /* set_vr_content_info */ + NULL, /* get_video_views_status */ +#endif + gl1_read_viewport_bgrx }; gfx_display_ctx_driver_t gfx_display_ctx_gl1 = { diff --git a/gfx/drivers/gl2.c b/gfx/drivers/gl2.c index 1cf77dc39054..408ac6208d1f 100644 --- a/gfx/drivers/gl2.c +++ b/gfx/drivers/gl2.c @@ -2554,7 +2554,8 @@ static void gl2_renderchain_bind_prev_texture( } #ifdef HAVE_GL_ASYNC_READBACK -static bool gl2_read_pbo(gl2_t *gl, uint8_t *buffer) +/* bgrx: write 32-bit BGRX (read_viewport_bgrx) instead of BGR24. */ +static bool gl2_read_pbo(gl2_t *gl, uint8_t *buffer, bool bgrx) { const uint8_t *ptr = NULL; #ifdef HAVE_OPENGLES3 @@ -2583,17 +2584,31 @@ static bool gl2_read_pbo(gl2_t *gl, uint8_t *buffer) VIDEO_SCALE_W(gl->video_dims)); unsigned rb_h = MIN(VIDEO_SCALE_H(gl->vp.dims), VIDEO_SCALE_H(gl->video_dims)); - video_frame_convert_rgba_to_bgr( - (const void*)ptr, - buffer, - rb_w * sizeof(uint32_t), - rb_w * 3, - rb_w, - rb_h); + if (bgrx) + video_frame_copy_to_bgrx(buffer, VIDEO_SCALE_W(gl->vp.dims), + VIDEO_SCALE_H(gl->vp.dims), ptr, rb_w * 4, rb_w, rb_h, true); + else + video_frame_convert_rgba_to_bgr( + (const void*)ptr, + buffer, + rb_w * sizeof(uint32_t), + rb_w * 3, + rb_w, + rb_h); } #else ptr = (const uint8_t*)glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY); - if (ptr) + if (ptr && bgrx) + { + /* Already BGRA (GL_BGRA readback): rows are copied as-is. */ + unsigned rb_w = MIN(VIDEO_SCALE_W(gl->vp.dims), + VIDEO_SCALE_W(gl->video_dims)); + unsigned rb_h = MIN(VIDEO_SCALE_H(gl->vp.dims), + VIDEO_SCALE_H(gl->video_dims)); + video_frame_copy_to_bgrx(buffer, VIDEO_SCALE_W(gl->vp.dims), + VIDEO_SCALE_H(gl->vp.dims), ptr, rb_w * 4, rb_w, rb_h, false); + } + else if (ptr) { struct scaler_ctx *ctx = &gl->pbo_readback_scaler; scaler_ctx_scale_direct(ctx, buffer, ptr); @@ -2619,7 +2634,7 @@ static bool gl2_read_pbo(gl2_t *gl, uint8_t *buffer) static bool gl2_renderchain_read_viewport( gl2_t *gl, - uint8_t *buffer, bool is_idle) + uint8_t *buffer, bool is_idle, bool bgrx) { size_t num_pixels = 0; @@ -2653,7 +2668,7 @@ static bool gl2_renderchain_read_viewport( #ifdef HAVE_GL_ASYNC_READBACK if (gl->flags & GL2_FLAG_PBO_READBACK_ENABLE) { - if (!gl2_read_pbo(gl, buffer)) + if (!gl2_read_pbo(gl, buffer, bgrx)) goto error; } else @@ -2686,13 +2701,19 @@ static bool gl2_renderchain_read_viewport( VIDEO_SCALE_W(gl->video_dims)); unsigned rb_h = MIN(VIDEO_SCALE_H(gl->vp.dims), VIDEO_SCALE_H(gl->video_dims)); - video_frame_convert_rgba_to_bgr( - (const void*)gl->readback_buffer_screenshot, - buffer, - rb_w * sizeof(uint32_t), - rb_w * 3, - rb_w, - rb_h); + if (bgrx) + video_frame_copy_to_bgrx(buffer, VIDEO_SCALE_W(gl->vp.dims), + VIDEO_SCALE_H(gl->vp.dims), + (const uint8_t*)gl->readback_buffer_screenshot, + rb_w * 4, rb_w, rb_h, true); + else + video_frame_convert_rgba_to_bgr( + (const void*)gl->readback_buffer_screenshot, + buffer, + rb_w * sizeof(uint32_t), + rb_w * 3, + rb_w, + rb_h); } free(gl->readback_buffer_screenshot); @@ -4018,6 +4039,30 @@ static INLINE void gl2_draw_texture(gl2_t *gl) } #endif +static void gl2_pbo_async_readback(gl2_t *gl); + +/* The recording's per-frame async readback, or its teardown once + * recording has stopped. skip_in_menu: don't read back while the menu + * is drawn. */ +static void gl2_record_readback(gl2_t *gl, bool skip_in_menu) +{ + if (!(gl->flags & GL2_FLAG_PBO_READBACK_ENABLE)) + return; + + if (!(gl->flags & GL2_FLAG_GPU_RECORDING)) + { + glDeleteBuffers(4, gl->pbo_readback); + scaler_ctx_gen_reset(&gl->pbo_readback_scaler); + gl->flags &= ~GL2_FLAG_PBO_READBACK_ENABLE; + } +#ifdef HAVE_MENU + else if (skip_in_menu && (gl->flags & GL2_FLAG_MENU_TEXTURE_ENABLE)) + ; +#endif + else + gl2_pbo_async_readback(gl); +} + static void gl2_pbo_async_readback(gl2_t *gl) { #ifdef HAVE_OPENGLES @@ -5024,8 +5069,15 @@ static bool gl2_frame(void *data, const void *frame, * displayable image. Same choke-point pattern as glcore. */ if (gl->scrgb.active) gl2_bind_fb(gl2_ui_target_fbo(gl)); - else if (gl2_needs_pq_downconvert(gl)) - gl2_encode_pq_to_sdr(gl); + else + { + if (gl2_needs_pq_downconvert(gl)) + gl2_encode_pq_to_sdr(gl); + /* Record Game Only: read back before any UI is drawn. Under + * scRGB the image only lands at the final composite. */ + if (video_info->record_game_only) + gl2_record_readback(gl, false); + } #ifdef HAVE_OVERLAY if ((gl->flags & GL2_FLAG_OVERLAY_ENABLE) && overlay_behind_menu) @@ -5182,24 +5234,8 @@ static bool gl2_frame(void *data, const void *frame, 4, GL_RGBA, GL_UNSIGNED_BYTE, gl->readback_buffer_screenshot); - else if (gl->flags & GL2_FLAG_PBO_READBACK_ENABLE) - { - /* If recording has stopped, tear down PBO readback */ - if (!(gl->flags & GL2_FLAG_GPU_RECORDING)) - { - glDeleteBuffers(4, gl->pbo_readback); - scaler_ctx_gen_reset(&gl->pbo_readback_scaler); - gl->flags &= ~GL2_FLAG_PBO_READBACK_ENABLE; - } - else - { -#ifdef HAVE_MENU - /* Don't readback if we're in menu mode. */ - if (!(gl->flags & GL2_FLAG_MENU_TEXTURE_ENABLE)) -#endif - gl2_pbo_async_readback(gl); - } - } + else if (!video_info->record_game_only || gl->scrgb.active) + gl2_record_readback(gl, !video_info->record_game_only); /* The backbuffer is what it is until the swap, so the copy is * taken now. Not from the BFI light dupes, which recurse in here @@ -6616,7 +6652,20 @@ static bool gl2_read_viewport(void *data, uint8_t *buffer, bool is_idle) if (!gl) return false; - return gl2_renderchain_read_viewport(gl, buffer, is_idle); + return gl2_renderchain_read_viewport(gl, buffer, is_idle, false); +} + +static bool gl2_read_viewport_bgrx(void *data, uint8_t *buffer, + bool is_idle, bool *bottom_up) +{ + gl2_t *gl = (gl2_t*)data; + + if (!gl) + return false; + + /* glReadPixels returns the bottom row first. */ + *bottom_up = true; + return gl2_renderchain_read_viewport(gl, buffer, is_idle, true); } #if defined(HAVE_GL_ASYNC_READBACK) && !defined(HAVE_OPENGLES) && !defined(HAVE_PSGL) @@ -6639,7 +6688,7 @@ static bool gl2_record_read(void *data, uint8_t *buffer) gl->flags &= ~GL2_FLAG_PBO_READBACK_ENABLE; return false; } - return gl2_read_pbo(gl, buffer); + return gl2_read_pbo(gl, buffer, false); } #endif @@ -7575,8 +7624,9 @@ video_driver_t video_gl2 = { #ifdef HAVE_OPENXR gl2_get_vr_frame_state, gl2_set_vr_content_info, - gl2_get_video_views_status + gl2_get_video_views_status, #endif + gl2_read_viewport_bgrx }; gfx_display_ctx_driver_t gfx_display_ctx_gl = { diff --git a/gfx/drivers/gl3.c b/gfx/drivers/gl3.c index 33f757a80a67..a0b7b53418e5 100644 --- a/gfx/drivers/gl3.c +++ b/gfx/drivers/gl3.c @@ -1931,7 +1931,11 @@ static bool gl3_init_pbo_readback(gl3_t *gl) scaler->out_height = VIDEO_SCALE_H(gl->vp.dims); scaler->in_stride = VIDEO_SCALE_W(gl->vp.dims) * sizeof(uint32_t); scaler->out_stride = VIDEO_SCALE_W(gl->vp.dims) * 3; - scaler->in_fmt = SCALER_FMT_ABGR8888; +#ifdef HAVE_OPENGLES + scaler->in_fmt = SCALER_FMT_ABGR8888; /* RGBA readback */ +#else + scaler->in_fmt = SCALER_FMT_ARGB8888; /* BGRA readback */ +#endif scaler->out_fmt = SCALER_FMT_BGR24; scaler->scaler_type = SCALER_TYPE_POINT; @@ -1957,6 +1961,29 @@ static void gl3_deinit_pbo_readback(gl3_t *gl) scaler_ctx_gen_reset(&gl->pbo_readback_scaler); } +static void gl3_pbo_async_readback(gl3_t *gl); + +/* The recording's per-frame async readback, or its teardown once + * recording has stopped. skip_in_menu: don't read back while the menu + * is drawn. */ +static void gl3_record_readback(gl3_t *gl, bool skip_in_menu) +{ + if (!(gl->flags & GL3_FLAG_PBO_READBACK_ENABLE)) + return; + + if (!(gl->flags & GL3_FLAG_GPU_RECORDING)) + { + gl3_deinit_pbo_readback(gl); + gl->flags &= ~GL3_FLAG_PBO_READBACK_ENABLE; + } +#ifdef HAVE_MENU + else if (skip_in_menu && (gl->flags & GL3_FLAG_MENU_TEXTURE_ENABLE)) + ; +#endif + else + gl3_pbo_async_readback(gl); +} + static void gl3_pbo_async_readback(gl3_t *gl) { glBindBuffer(GL_PIXEL_PACK_BUFFER, @@ -1975,9 +2002,17 @@ static void gl3_pbo_async_readback(gl3_t *gl) gl->pbo_readback_index = 0; gl->pbo_readback_valid[gl->pbo_readback_index] = true; + /* Desktop GL reads BGRA, which both the recording's BGRX path and + * the BGR24 scaler want; GLES only guarantees RGBA. Keep in sync + * with the scaler in gl3_init_pbo_readback(). */ glReadPixels(VIDEO_POS_X(gl->vp.pos), VIDEO_POS_Y(gl->vp.pos), VIDEO_SCALE_W(gl->vp.dims), VIDEO_SCALE_H(gl->vp.dims), - GL_RGBA, GL_UNSIGNED_BYTE, NULL); +#ifdef HAVE_OPENGLES + GL_RGBA, GL_UNSIGNED_BYTE, +#else + GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, +#endif + NULL); if (gl->scrgb.active && gl->scrgb.fbo) glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); @@ -4589,14 +4624,13 @@ static bool gl3_read_viewport_hdr(void *data, uint16_t *buffer, return true; } -static bool gl3_read_viewport(void *data, uint8_t *buffer, bool is_idle) +/* bgrx: write bottom-up 32-bit BGRX (read_viewport_bgrx) instead of + * bottom-up BGR24. */ +static bool gl3_read_viewport_internal(gl3_t *gl, uint8_t *buffer, + bool is_idle, bool bgrx) { - gl3_t *gl = (gl3_t*)data; size_t num_pixels = 0; - if (!gl) - return false; - if (gl->flags & GL3_FLAG_USE_SHARED_CONTEXT) gl->ctx_driver->bind_hw_render(gl->ctx_data, false); @@ -4633,7 +4667,19 @@ static bool gl3_read_viewport(void *data, uint8_t *buffer, bool is_idle) glBindBuffer(GL_PIXEL_PACK_BUFFER, gl->pbo_readback[gl->pbo_readback_index]); ptr = glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, num_pixels * sizeof(uint32_t), GL_MAP_READ_BIT); - scaler_ctx_scale_direct(ctx, buffer, ptr); + if (ptr && bgrx) + video_frame_copy_to_bgrx(buffer, VIDEO_SCALE_W(gl->vp.dims), + VIDEO_SCALE_H(gl->vp.dims), + (const uint8_t*)ptr, VIDEO_SCALE_W(gl->vp.dims) * 4, + VIDEO_SCALE_W(gl->vp.dims), VIDEO_SCALE_H(gl->vp.dims), +#ifdef HAVE_OPENGLES + true +#else + false +#endif + ); + else if (ptr) + scaler_ctx_scale_direct(ctx, buffer, ptr); glUnmapBuffer(GL_PIXEL_PACK_BUFFER); glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); } @@ -4665,13 +4711,19 @@ static bool gl3_read_viewport(void *data, uint8_t *buffer, bool is_idle) VIDEO_SCALE_W(gl->video_dims)); unsigned rb_h = MIN(VIDEO_SCALE_H(gl->vp.dims), VIDEO_SCALE_H(gl->video_dims)); - video_frame_convert_rgba_to_bgr( - (const void*)gl->readback_buffer_screenshot, - buffer, - rb_w * sizeof(uint32_t), - rb_w * 3, - rb_w, - rb_h); + if (bgrx) + video_frame_copy_to_bgrx(buffer, VIDEO_SCALE_W(gl->vp.dims), + VIDEO_SCALE_H(gl->vp.dims), + (const uint8_t*)gl->readback_buffer_screenshot, + rb_w * 4, rb_w, rb_h, true); + else + video_frame_convert_rgba_to_bgr( + (const void*)gl->readback_buffer_screenshot, + buffer, + rb_w * sizeof(uint32_t), + rb_w * 3, + rb_w, + rb_h); } free(gl->readback_buffer_screenshot); @@ -4693,6 +4745,25 @@ static bool gl3_read_viewport(void *data, uint8_t *buffer, bool is_idle) return false; } +static bool gl3_read_viewport(void *data, uint8_t *buffer, bool is_idle) +{ + gl3_t *gl = (gl3_t*)data; + if (!gl) + return false; + return gl3_read_viewport_internal(gl, buffer, is_idle, false); +} + +static bool gl3_read_viewport_bgrx(void *data, uint8_t *buffer, + bool is_idle, bool *bottom_up) +{ + gl3_t *gl = (gl3_t*)data; + if (!gl) + return false; + /* glReadPixels returns the bottom row first. */ + *bottom_up = true; + return gl3_read_viewport_internal(gl, buffer, is_idle, true); +} + static void gl3_update_cpu_texture(gl3_t *gl, struct gl3_streamed_texture *streamed, const void *frame, unsigned dims, unsigned pitch) @@ -6255,13 +6326,18 @@ static bool gl3_frame(void *data, const void *frame, if (gl->scrgb.active) glBindFramebuffer(GL_FRAMEBUFFER, gl3_ui_target_fbo(gl, video_info->dims)); - else if (gl3_needs_pq_downconvert(gl)) + else { /* Convert the PQ frame to SDR now, straight into the backbuffer, * so the UI below draws over a displayable image in its own * space - no UI layer, no linear composite, nothing else about * the SDR path changes. */ - gl3_encode_pq_to_sdr(gl, width, height); + if (gl3_needs_pq_downconvert(gl)) + gl3_encode_pq_to_sdr(gl, width, height); + /* Record Game Only: read back before any UI is drawn. Under + * scRGB the image only lands at the final composite. */ + if (video_info->record_game_only) + gl3_record_readback(gl, false); } #ifdef HAVE_SLANG @@ -6491,23 +6567,8 @@ static bool gl3_frame(void *data, const void *frame, && !gl->video_info.source_hdr10) glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); } - else if (gl->flags & GL3_FLAG_PBO_READBACK_ENABLE) - { - /* If recording has stopped, tear down PBO readback */ - if (!(gl->flags & GL3_FLAG_GPU_RECORDING)) - { - gl3_deinit_pbo_readback(gl); - gl->flags &= ~GL3_FLAG_PBO_READBACK_ENABLE; - } - else - { -#ifdef HAVE_MENU - /* Don't readback if we're in menu mode. */ - if (!(gl->flags & GL3_FLAG_MENU_TEXTURE_ENABLE)) -#endif - gl3_pbo_async_readback(gl); - } - } + else if (!video_info->record_game_only || gl->scrgb.active) + gl3_record_readback(gl, !video_info->record_game_only); gl3_hw_ring_drawn(gl); gl->present_wait = 0; @@ -7552,7 +7613,13 @@ video_driver_t video_gl3 = { #endif NULL, /* invalidate_hw_render_cache */ gl3_read_viewport_hdr, - &gl3_raster_font + &gl3_raster_font, +#ifdef HAVE_OPENXR + NULL, /* get_vr_frame_state */ + NULL, /* set_vr_content_info */ + NULL, /* get_video_views_status */ +#endif + gl3_read_viewport_bgrx }; gfx_display_ctx_driver_t gfx_display_ctx_gl3 = { diff --git a/gfx/drivers/sdl2_gfx.c b/gfx/drivers/sdl2_gfx.c index 822c1ac7b0f8..fc125d4f83fb 100644 --- a/gfx/drivers/sdl2_gfx.c +++ b/gfx/drivers/sdl2_gfx.c @@ -587,26 +587,12 @@ static void check_window(sdl2_video_t *vid) } } -static bool sdl2_gfx_frame(void *data, const void *frame, - unsigned dims, uint64_t frame_count, - unsigned pitch, const char *msg, video_frame_info_t *video_info) +/* Draw the current core frame into the game viewport, honouring + * rotation. Also used by the viewport readback, which redraws rather + * than read the presented backbuffer (undefined after + * SDL_RenderPresent on several backends). */ +static void sdl2_blit_frame(sdl2_video_t *vid) { - char title[128]; - sdl2_video_t *vid = (sdl2_video_t*)data; -#ifdef HAVE_MENU - bool menu_is_alive = (video_info->menu_st_flags & MENU_ST_FLAG_ALIVE) ? true : false; -#endif - - if (vid->flags & SDL2_FLAG_SHOULD_RESIZE) - sdl_refresh_viewport(vid); - - if (frame) - { - SDL_RenderClear(vid->renderer); - sdl_refresh_input_size(vid, false, vid->video.rgb32, dims, pitch); - SDL_UpdateTexture(vid->frame.tex, NULL, frame, pitch); - } - { /* For 90/270 degree rotation the frame must not be clipped by the * aspect-corrected game viewport. SDL_RenderCopyEx() scales the @@ -644,6 +630,29 @@ static bool sdl2_gfx_frame(void *data, const void *frame, SDL_RenderCopyEx(vid->renderer, vid->frame.tex, NULL, NULL, vid->rotation, NULL, SDL_FLIP_NONE); } +} + +static bool sdl2_gfx_frame(void *data, const void *frame, + unsigned dims, uint64_t frame_count, + unsigned pitch, const char *msg, video_frame_info_t *video_info) +{ + char title[128]; + sdl2_video_t *vid = (sdl2_video_t*)data; +#ifdef HAVE_MENU + bool menu_is_alive = (video_info->menu_st_flags & MENU_ST_FLAG_ALIVE) ? true : false; +#endif + + if (vid->flags & SDL2_FLAG_SHOULD_RESIZE) + sdl_refresh_viewport(vid); + + if (frame) + { + SDL_RenderClear(vid->renderer); + sdl_refresh_input_size(vid, false, vid->video.rgb32, dims, pitch); + SDL_UpdateTexture(vid->frame.tex, NULL, frame, pitch); + } + + sdl2_blit_frame(vid); #ifdef HAVE_MENU { @@ -892,28 +901,79 @@ static void sdl2_gfx_viewport_info(void *data, struct video_viewport *vp) *vp = vid->vp; } -static bool sdl2_gfx_read_viewport(void *data, uint8_t *buffer, bool is_idle) +/* Redraw the last core frame into the game viewport and read it back. + * bgrx: 32-bit B,G,R,X, top row first; otherwise BGR24 bottom row + * first, as read_viewport() callers expect. Menu, widget and OSD + * passes are not replayed. Nothing is presented; the next frame + * redraws. */ +static bool sdl2_capture_viewport(sdl2_video_t *vid, uint8_t *buffer, + bool bgrx) { - SDL_Surface *surf = NULL, *bgr24 = NULL; - sdl2_video_t *vid = (sdl2_video_t*)data; + SDL_Rect saved_vp, game_vp; + int pitch; + bool ok; +#if SDL_VERSION_ATLEAST(2, 0, 5) + Uint32 fmt = bgrx ? SDL_PIXELFORMAT_BGRA32 : SDL_PIXELFORMAT_BGR24; +#else + Uint32 fmt = bgrx ? SDL_PIXELFORMAT_ARGB8888 : SDL_PIXELFORMAT_BGR24; +#endif - if (!is_idle) - video_driver_cached_frame(); + if (!vid || !vid->renderer || !vid->frame.tex + || !VIDEO_SCALE_W(vid->vp.dims) || !VIDEO_SCALE_H(vid->vp.dims)) + return false; - surf = SDL_GetWindowSurface(vid->window); - bgr24 = SDL_ConvertSurfaceFormat(surf, SDL_PIXELFORMAT_BGR24, 0); + pitch = (int)VIDEO_SCALE_W(vid->vp.dims) * (bgrx ? 4 : 3); + game_vp.x = VIDEO_POS_X(vid->vp.pos); + game_vp.y = VIDEO_POS_Y(vid->vp.pos); + game_vp.w = (int)VIDEO_SCALE_W(vid->vp.dims); + game_vp.h = (int)VIDEO_SCALE_H(vid->vp.dims); - if (!bgr24) + SDL_RenderGetViewport(vid->renderer, &saved_vp); + SDL_RenderSetViewport(vid->renderer, &game_vp); + SDL_RenderClear(vid->renderer); + sdl2_blit_frame(vid); + /* NULL rect: the current viewport, the size of the caller's buffer. */ + ok = SDL_RenderReadPixels(vid->renderer, NULL, fmt, buffer, pitch) == 0; + SDL_RenderSetViewport(vid->renderer, &saved_vp); + + if (!ok) { - RARCH_WARN("[SDL2] Failed to convert viewport data to BGR24: %s.", SDL_GetError()); + RARCH_WARN("[SDL2] Failed to read viewport: %s.\n", SDL_GetError()); return false; } - memcpy(buffer, bgr24->pixels, bgr24->h * bgr24->pitch); + if (!bgrx) + { + unsigned y, h = VIDEO_SCALE_H(vid->vp.dims); + uint8_t *tmp = (uint8_t*)malloc((size_t)pitch); + if (!tmp) + return false; + for (y = 0; y < h / 2; y++) + { + uint8_t *a = buffer + (size_t)y * pitch; + uint8_t *b = buffer + (size_t)(h - 1 - y) * pitch; + memcpy(tmp, a, pitch); + memcpy(a, b, pitch); + memcpy(b, tmp, pitch); + } + free(tmp); + } return true; } +static bool sdl2_gfx_read_viewport(void *data, uint8_t *buffer, bool is_idle) +{ + return sdl2_capture_viewport((sdl2_video_t*)data, buffer, false); +} + +static bool sdl2_gfx_read_viewport_bgrx(void *data, uint8_t *buffer, + bool is_idle, bool *bottom_up) +{ + *bottom_up = false; + return sdl2_capture_viewport((sdl2_video_t*)data, buffer, true); +} + static void sdl2_poke_set_filtering(void *data, unsigned index, bool smooth, bool ctx_scaling) { sdl2_video_t *vid = (sdl2_video_t*)data; @@ -2395,10 +2455,16 @@ video_driver_t video_sdl2 = { NULL, /* invalidate_hw_render_cache */ NULL, /* read_viewport_hdr */ #if SDL_VERSION_ATLEAST(2, 0, 18) - &sdl2_raster_font + &sdl2_raster_font, #else - NULL /* sdl2_raster_font needs SDL_RenderGeometry */ + NULL, /* sdl2_raster_font needs SDL_RenderGeometry */ +#endif +#ifdef HAVE_OPENXR + NULL, /* get_vr_frame_state */ + NULL, /* set_vr_content_info */ + NULL, /* get_video_views_status */ #endif + sdl2_gfx_read_viewport_bgrx }; #if SDL_VERSION_ATLEAST(2, 0, 18) diff --git a/gfx/drivers/sdl3_gfx.c b/gfx/drivers/sdl3_gfx.c index 99ee6c0fba3f..1c548daa5b05 100644 --- a/gfx/drivers/sdl3_gfx.c +++ b/gfx/drivers/sdl3_gfx.c @@ -361,11 +361,17 @@ static void sdl3_blit_frame(sdl3_video_t *vid) * flipped because read_viewport callers expect bottom-up data. * Must run before SDL_RenderPresent - afterwards the backbuffer * contents are undefined on several backends. */ -static bool sdl3_capture_viewport(sdl3_video_t *vid, uint8_t *buffer) +/* bgrx: write 32-bit B,G,R,X top row first (read_viewport_bgrx); + * otherwise BGR24 bottom row first, as read_viewport() callers expect. */ +static bool sdl3_capture_viewport(sdl3_video_t *vid, uint8_t *buffer, + bool bgrx) { SDL_Surface *surf; int w, h, y; bool ok = true; + size_t bpp = bgrx ? 4 : 3; + SDL_PixelFormat fmt = bgrx ? SDL_PIXELFORMAT_BGRA32 + : SDL_PIXELFORMAT_BGR24; /* NULL rect = the current render viewport, which is the * aspect-corrected game viewport at this point in the frame. */ @@ -382,18 +388,19 @@ static bool sdl3_capture_viewport(sdl3_video_t *vid, uint8_t *buffer) h = (surf->h < (int)VIDEO_SCALE_H(vid->vp.dims)) ? surf->h : (int)VIDEO_SCALE_H(vid->vp.dims); if (w < (int)VIDEO_SCALE_W(vid->vp.dims) || h < (int)VIDEO_SCALE_H(vid->vp.dims)) - memset(buffer, 0, VIDEO_SCALE_AREA(vid->vp.dims) * 3); + memset(buffer, 0, VIDEO_SCALE_AREA(vid->vp.dims) * bpp); for (y = 0; y < h; y++) { + int dst_row = bgrx ? y : (h - 1 - y); if (!SDL_ConvertPixels(w, 1, surf->format, (const uint8_t*)surf->pixels + (size_t)y * surf->pitch, surf->pitch, - SDL_PIXELFORMAT_BGR24, - buffer + (size_t)(h - 1 - y) * VIDEO_SCALE_W(vid->vp.dims) * 3, - (int)VIDEO_SCALE_W(vid->vp.dims) * 3)) + fmt, + buffer + (size_t)dst_row * VIDEO_SCALE_W(vid->vp.dims) * bpp, + (int)(VIDEO_SCALE_W(vid->vp.dims) * bpp))) { - RARCH_WARN("[SDL3] Failed to convert viewport data to BGR24: %s.\n", + RARCH_WARN("[SDL3] Failed to convert viewport data: %s.\n", SDL_GetError()); ok = false; break; @@ -624,7 +631,20 @@ static bool sdl3_gfx_read_viewport(void *data, uint8_t *buffer, bool is_idle) SDL_RenderClear(vid->renderer); sdl3_blit_frame(vid); - return sdl3_capture_viewport(vid, buffer); + return sdl3_capture_viewport(vid, buffer, false); +} + +static bool sdl3_gfx_read_viewport_bgrx(void *data, uint8_t *buffer, + bool is_idle, bool *bottom_up) +{ + sdl3_video_t *vid = (sdl3_video_t*)data; + + /* Same redraw-and-capture as sdl3_gfx_read_viewport(). */ + SDL_RenderClear(vid->renderer); + sdl3_blit_frame(vid); + + *bottom_up = false; + return sdl3_capture_viewport(vid, buffer, true); } /* Applies a new window size / fullscreen in place, without tearing @@ -2045,7 +2065,13 @@ video_driver_t video_sdl3 = { #endif NULL, /* invalidate_hw_render_cache */ NULL, /* read_viewport_hdr */ - &sdl3_raster_font + &sdl3_raster_font, +#ifdef HAVE_OPENXR + NULL, /* get_vr_frame_state */ + NULL, /* set_vr_content_info */ + NULL, /* get_video_views_status */ +#endif + sdl3_gfx_read_viewport_bgrx }; gfx_display_ctx_driver_t gfx_display_ctx_sdl3 = { diff --git a/gfx/drivers/vulkan.c b/gfx/drivers/vulkan.c index 4e1c83c88782..72e99c154afd 100644 --- a/gfx/drivers/vulkan.c +++ b/gfx/drivers/vulkan.c @@ -10473,6 +10473,8 @@ static bool vulkan_frame(void *data, const void *frame, #ifdef VULKAN_HDR_SWAPCHAIN bool use_offscreen_buffer = false; #endif + /* Recording's readback was already taken this frame, before the UI. */ + bool record_readback_done = false; #ifdef HAVE_OPENXR bool xr_stereo = (vk->flags & VK_FLAG_OPEN_XR) != 0; @@ -11341,6 +11343,50 @@ static bool vulkan_frame(void *data, const void *frame, (vulkan_filter_chain_t*)filter_chain, vk->cmd, &vk->video_vp, vk->mvp.data); + /* Record Game Only: copy the game image out before the UI is + * drawn, then resume the pass with LOAD. HDR output keeps the + * end-of-frame readback, which needs its tonemap pass. */ + if ( video_info->record_game_only + && (vk->flags & VK_FLAG_READBACK_STREAMED) + && (vk->flags & VK_FLAG_GPU_RECORDING) +#ifdef VULKAN_HDR_SWAPCHAIN + && !(vk->context->flags & VK_CTX_FLAG_HDR_ENABLE) +#endif + && (backbuffer->image != VK_NULL_HANDLE) + && (vk->context->flags & VK_CTX_FLAG_HAS_ACQUIRED_SWAPCHAIN)) + { + vkCmdEndRenderPass(vk->cmd); + + VULKAN_IMAGE_LAYOUT_TRANSITION( + vk->cmd, + backbuffer->image, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_ACCESS_TRANSFER_READ_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT); + + vulkan_readback(vk, backbuffer); + + VULKAN_IMAGE_LAYOUT_TRANSITION( + vk->cmd, + backbuffer->image, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_ACCESS_TRANSFER_READ_BIT, + VK_ACCESS_COLOR_ATTACHMENT_READ_BIT + | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT); + + /* Same framebuffer, LOAD instead of CLEAR. */ + rp_info.renderPass = vk->keep_render_pass; + vkCmdBeginRenderPass(vk->cmd, &rp_info, VK_SUBPASS_CONTENTS_INLINE); + + record_readback_done = true; + } + #ifdef VULKAN_HDR_SWAPCHAIN end_pass = true; end_main_pass = true; @@ -11626,7 +11672,8 @@ static bool vulkan_frame(void *data, const void *frame, } if ( (vk->flags & VK_FLAG_READBACK_PENDING) - || (vk->flags & VK_FLAG_READBACK_STREAMED)) + || ( (vk->flags & VK_FLAG_READBACK_STREAMED) + && !record_readback_done)) { VkImageLayout backbuffer_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; #ifdef VULKAN_HDR_SWAPCHAIN @@ -13422,11 +13469,13 @@ video_record_read_t vulkan_get_record_read(void) return vulkan_record_read; } -static bool vulkan_read_viewport(void *data, uint8_t *buffer, bool is_idle) +/* bgrx: write top-down 32-bit BGRX (read_viewport_bgrx) instead of + * bottom-up BGR24. */ +static bool vulkan_read_viewport_internal(vk_t *vk, uint8_t *buffer, + bool is_idle, bool bgrx) { VkFormat format; struct vk_texture *staging = NULL; - vk_t *vk = (vk_t*)data; if (!vk) return false; @@ -13478,7 +13527,8 @@ static bool vulkan_read_viewport(void *data, uint8_t *buffer, bool is_idle) if (staging->memory == VK_NULL_HANDLE) return false; - buffer += 3 * (VIDEO_SCALE_H(vk->vp.dims) - 1) * VIDEO_SCALE_W(vk->vp.dims); + if (!bgrx) + buffer += 3 * (VIDEO_SCALE_H(vk->vp.dims) - 1) * VIDEO_SCALE_W(vk->vp.dims); vkMapMemory(vk->context->device, staging->memory, staging->offset, staging->size, 0, (void**)&src); @@ -13494,9 +13544,27 @@ static bool vulkan_read_viewport(void *data, uint8_t *buffer, bool is_idle) vkInvalidateMappedMemoryRanges(vk->context->device, 1, &range); } - ctx->in_stride = (int)staging->stride; - ctx->out_stride = -(int)VIDEO_SCALE_W(vk->vp.dims) * 3; - scaler_ctx_scale_direct(ctx, buffer, src); + if (bgrx) + { + /* Only the region vulkan_readback() copied is valid. */ + unsigned vp_w = VIDEO_SCALE_W(vk->vp.dims); + unsigned vp_h = VIDEO_SCALE_H(vk->vp.dims); + unsigned cp_w = VIDEO_SCALE_W(vk->readback.copied_dims); + unsigned cp_h = VIDEO_SCALE_H(vk->readback.copied_dims); + if (!cp_w || cp_w > vp_w) + cp_w = vp_w; + if (!cp_h || cp_h > vp_h) + cp_h = vp_h; + video_frame_copy_to_bgrx(buffer, vp_w, vp_h, + src, staging->stride, cp_w, cp_h, + format != VK_FORMAT_B8G8R8A8_UNORM); + } + else + { + ctx->in_stride = (int)staging->stride; + ctx->out_stride = -(int)VIDEO_SCALE_W(vk->vp.dims) * 3; + scaler_ctx_scale_direct(ctx, buffer, src); + } vkUnmapMemory(vk->context->device, staging->memory); } @@ -13579,10 +13647,24 @@ static bool vulkan_read_viewport(void *data, uint8_t *buffer, bool is_idle) cp_width = vp_width; if (cp_height == 0 || cp_height > vp_height) cp_height = vp_height; - if (cp_width < vp_width || cp_height < vp_height) - memset(buffer, 0, (size_t)vp_width * vp_height * 3); - buffer += 3 * (vp_height - 1) * vp_width; + if (bgrx) + { + /* vp_width here is clamped to video_width; keep the + * recording's vp.width row pitch. */ + video_frame_copy_to_bgrx(buffer, VIDEO_SCALE_W(vk->vp.dims), + VIDEO_SCALE_H(vk->vp.dims), + src, staging->stride, cp_width, cp_height, + format != VK_FORMAT_B8G8R8A8_UNORM); + format = VK_FORMAT_UNDEFINED; /* skip the BGR24 conversion */ + } + else + { + if (cp_width < vp_width || cp_height < vp_height) + memset(buffer, 0, (size_t)vp_width * vp_height * 3); + + buffer += 3 * (vp_height - 1) * vp_width; + } switch (format) { @@ -13615,6 +13697,9 @@ static bool vulkan_read_viewport(void *data, uint8_t *buffer, bool is_idle) } break; + case VK_FORMAT_UNDEFINED: + break; + default: RARCH_ERR("[Vulkan] Unexpected swapchain format.\n"); break; @@ -13626,6 +13711,25 @@ static bool vulkan_read_viewport(void *data, uint8_t *buffer, bool is_idle) return true; } +static bool vulkan_read_viewport(void *data, uint8_t *buffer, bool is_idle) +{ + vk_t *vk = (vk_t*)data; + if (!vk) + return false; + return vulkan_read_viewport_internal(vk, buffer, is_idle, false); +} + +static bool vulkan_read_viewport_bgrx(void *data, uint8_t *buffer, + bool is_idle, bool *bottom_up) +{ + vk_t *vk = (vk_t*)data; + if (!vk) + return false; + /* The staging copy is top row first. */ + *bottom_up = false; + return vulkan_read_viewport_internal(vk, buffer, is_idle, true); +} + #ifdef VULKAN_HDR_SWAPCHAIN /* IEEE-754 half (binary16) -> float, for reading an RGBA16F scRGB backbuffer * on the CPU. Handles normals, subnormals, sign and inf/nan. */ @@ -14411,6 +14515,7 @@ video_driver_t video_vulkan = { vulkan_set_vr_content_info, vulkan_get_video_views_status, #endif + vulkan_read_viewport_bgrx }; gfx_display_ctx_driver_t gfx_display_ctx_vulkan = { diff --git a/gfx/video_driver.c b/gfx/video_driver.c index d938e1dc9fdb..37050dc6f776 100644 --- a/gfx/video_driver.c +++ b/gfx/video_driver.c @@ -164,6 +164,7 @@ static void video_driver_read_vp_params(struct video_vp_param_snap *ps); #include "../driver.h" #include "../file_path_special.h" #include "../list_special.h" +#include "../performance_counters.h" #include "../retroarch.h" #include "../verbosity.h" #include "../command.h" @@ -1835,6 +1836,27 @@ void video_driver_gpu_record_deinit(void) video_st->record_gpu_buffer = NULL; } +/* Main thread GPU readback while recording, for perfcnt_enable. */ +static struct retro_perf_counter record_perf_readback = {0}; + +/* BGR24 pixels to B,G,R,X. @dst may be @src: walked from the end, each + * pixel is read before anything is written over it. */ +VIDEO_NOINLINE static void recording_bgr24_to_bgrx(uint8_t *dst, + const uint8_t *src, size_t area) +{ + size_t i = area; + while (i--) + { + uint8_t b = src[3 * i + 0]; + uint8_t g = src[3 * i + 1]; + uint8_t r = src[3 * i + 2]; + dst[4 * i + 0] = b; + dst[4 * i + 1] = g; + dst[4 * i + 2] = r; + dst[4 * i + 3] = 0xff; + } +} + static void recording_dump_frame( const void *data, unsigned dims, size_t pitch, bool is_idle) { @@ -1851,6 +1873,7 @@ static void recording_dump_frame( if (video_st->record_gpu_buffer) { const uint8_t *gpu_frame = video_st->record_gpu_buffer; + bool bottom_up = true; #ifdef HAVE_THREADS int taken = -2; if (video_st->thread_wrapper_active) @@ -1864,10 +1887,21 @@ static void recording_dump_frame( record_st->driver->push_video(record_st->data, &ffemu_data); return; } + /* The wrapper reads back BGR24 on its own thread; a recording + * opened for 32-bit frames (threaded video switched on since) + * has it widened into the buffer sized for those. */ + if (taken == 1 && record_st->gpu_bgrx) + { + recording_bgr24_to_bgrx(video_st->record_gpu_buffer, gpu_frame, + VIDEO_SCALE_AREA(record_st->gpu_dims)); + gpu_frame = video_st->record_gpu_buffer; + } if (taken == -2) #endif { struct video_viewport vp; + bool ok; + bool perf = runloop_state_get_ptr()->perfcnt_enable; vp.pos = VIDEO_POS_PACK(0, 0); vp.dims = 0; @@ -1901,19 +1935,39 @@ static void recording_dump_frame( /* Big bottleneck. * Since we might need to do read-backs asynchronously, * it might take 3-4 times before this returns true. */ - if (!( vid->read_viewport - && vid->read_viewport( - video_st->data, video_st->record_gpu_buffer, is_idle))) + performance_counter_init(record_perf_readback, "record_readback"); + performance_counter_start_plus(perf, record_perf_readback); + if (record_st->gpu_bgrx && vid->read_viewport_bgrx) + ok = vid->read_viewport_bgrx(video_st->data, + video_st->record_gpu_buffer, is_idle, &bottom_up); + else + { + /* BGR24, as the driver now up reads it back: widened when + * the recording was opened for 32-bit frames. */ + ok = vid->read_viewport + && vid->read_viewport( + video_st->data, video_st->record_gpu_buffer, is_idle); + if (ok && record_st->gpu_bgrx) + recording_bgr24_to_bgrx(video_st->record_gpu_buffer, + video_st->record_gpu_buffer, + VIDEO_SCALE_AREA(record_st->gpu_dims)); + } + performance_counter_stop_plus(perf, record_perf_readback); + if (!ok) return; } ffemu_data.dims = record_st->gpu_dims; - ffemu_data.pitch = (int)(VIDEO_SCALE_W(ffemu_data.dims) * 3); - ffemu_data.data = gpu_frame - + (VIDEO_SCALE_H(ffemu_data.dims) - 1) * ffemu_data.pitch; - - ffemu_data.pitch = -ffemu_data.pitch; + ffemu_data.pitch = (int)(VIDEO_SCALE_W(ffemu_data.dims) + * (record_st->gpu_bgrx ? 4 : 3)); + ffemu_data.data = gpu_frame; + if (bottom_up) + { + ffemu_data.data = gpu_frame + + (VIDEO_SCALE_H(ffemu_data.dims) - 1) * ffemu_data.pitch; + ffemu_data.pitch = -ffemu_data.pitch; + } } else ffemu_data.is_dupe = !data; @@ -5866,6 +5920,8 @@ void video_driver_build_info(video_frame_info_t *video_info) video_info->msg_queue_delay = runloop_st->msg_queue_delay; video_info->runloop_is_paused = (runloop_st->flags & RUNLOOP_FLAG_PAUSED) ? true : false; video_info->gpu_recording = recording_state_get_ptr()->enable; + video_info->record_game_only = video_info->gpu_recording + && settings->bools.video_record_game_only; video_info->core_running = !(runloop_st->flags & RUNLOOP_FLAG_PAUSED); video_info->menu_content_rate = false; #ifdef HAVE_MENU diff --git a/gfx/video_driver.h b/gfx/video_driver.h index 81ce9c1d66dd..2a53f6c3fc93 100644 --- a/gfx/video_driver.h +++ b/gfx/video_driver.h @@ -699,6 +699,9 @@ typedef struct video_frame_info * draws them: it travels with the frame, so the thread that draws * never reads the recording state the main thread writes. */ bool gpu_recording; + /* Record Game Only: the driver reads the recording's frame back + * before drawing any UI over it. */ + bool record_game_only; bool threaded_present_repeat; /* The threaded presenter is holding each push to the display's * vblank: the setting is on and the wrapper is running. */ @@ -1284,6 +1287,11 @@ typedef struct video_driver unsigned (*get_video_views_status)(void *data); #endif + /* Optional: read_viewport as 32-bit B,G,R,X, viewport width * 4 + * per row, with *bottom_up set to the row order written. Lets GPU + * recording skip the BGR24 conversion. */ + bool (*read_viewport_bgrx)(void *data, uint8_t *buffer, bool is_idle, + bool *bottom_up); } video_driver_t; #ifdef HAVE_OPENXR diff --git a/libretro-common/include/gfx/video_frame.h b/libretro-common/include/gfx/video_frame.h index 98f46b704def..e08e0b9e3cb1 100644 --- a/libretro-common/include/gfx/video_frame.h +++ b/libretro-common/include/gfx/video_frame.h @@ -24,6 +24,8 @@ #define _LIBRETRO_SDK_VIDEO_FRAME_H #include +#include +#include #include #include @@ -207,6 +209,40 @@ static INLINE void video_frame_convert_rgba_to_bgr( } } +/* Copy w x h 32-bit pixels into a dst_w x dst_h buffer of B,G,R,X + * bytes with a row pitch of dst_w * 4, keeping the source row order. + * src_rgba: source bytes are R,G,B,A and are swapped; otherwise they + * are already B,G,R,A/X and rows are copied as-is. Anything outside + * the w x h source region is cleared to black. */ +static INLINE void video_frame_copy_to_bgrx( + uint8_t *dst, unsigned dst_w, unsigned dst_h, + const uint8_t *src, size_t src_pitch, + unsigned w, unsigned h, bool src_rgba) +{ + unsigned x, y; + + if (w > dst_w) + w = dst_w; + if (h > dst_h) + h = dst_h; + if (w < dst_w || h < dst_h) + memset(dst, 0, (size_t)dst_w * dst_h * 4); + + for (y = 0; y < h; y++, dst += (size_t)dst_w * 4, src += src_pitch) + { + if (!src_rgba) + memcpy(dst, src, (size_t)w * 4); + else + for (x = 0; x < w; x++) + { + dst[4 * x + 0] = src[4 * x + 2]; + dst[4 * x + 1] = src[4 * x + 1]; + dst[4 * x + 2] = src[4 * x + 0]; + dst[4 * x + 3] = 0xff; + } + } +} + static INLINE void video_pixel_frame_scale( struct scaler_ctx *scaler, void *output, const void *data, diff --git a/menu/cbs/menu_cbs_sublabel.c b/menu/cbs/menu_cbs_sublabel.c index 3fd282305705..5d7219d78f57 100644 --- a/menu/cbs/menu_cbs_sublabel.c +++ b/menu/cbs/menu_cbs_sublabel.c @@ -2434,6 +2434,10 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs, { MENU_ENUM_LABEL_CLOUD_SYNC_RESOLVE_KEEP_SERVER, MENU_ENUM_SUBLABEL_CLOUD_SYNC_RESOLVE_KEEP_SERVER }, { MENU_ENUM_LABEL_CORE_MANAGER_LIST, MENU_ENUM_SUBLABEL_CORE_MANAGER_LIST }, { MENU_ENUM_LABEL_VIDEO_POST_FILTER_RECORD, MENU_ENUM_SUBLABEL_VIDEO_POST_FILTER_RECORD }, + { MENU_ENUM_LABEL_VIDEO_RECORD_ALLOW_FRAME_DROP, MENU_ENUM_SUBLABEL_VIDEO_RECORD_ALLOW_FRAME_DROP }, + { MENU_ENUM_LABEL_VIDEO_RECORD_GAME_ONLY, MENU_ENUM_SUBLABEL_VIDEO_RECORD_GAME_ONLY }, + { MENU_ENUM_LABEL_VIDEO_RECORD_FIFO_FRAMES, MENU_ENUM_SUBLABEL_VIDEO_RECORD_FIFO_FRAMES }, + { MENU_ENUM_LABEL_VIDEO_RECORD_HW_ENCODER, MENU_ENUM_SUBLABEL_VIDEO_RECORD_HW_ENCODER }, { MENU_ENUM_LABEL_NETPLAY_NICKNAME, MENU_ENUM_SUBLABEL_NETPLAY_NICKNAME }, { MENU_ENUM_LABEL_CHEEVOS_USERNAME, MENU_ENUM_SUBLABEL_CHEEVOS_USERNAME }, { MENU_ENUM_LABEL_CHEEVOS_PASSWORD, MENU_ENUM_SUBLABEL_CHEEVOS_PASSWORD }, diff --git a/menu/menu_displaylist.c b/menu/menu_displaylist.c index 2544192fa5fc..24ddd1ba7ec5 100644 --- a/menu/menu_displaylist.c +++ b/menu/menu_displaylist.c @@ -11494,6 +11494,10 @@ unsigned menu_displaylist_build_list( {MENU_ENUM_LABEL_VIDEO_RECORD_THREADS, PARSE_ONLY_UINT, false}, {MENU_ENUM_LABEL_VIDEO_POST_FILTER_RECORD, PARSE_ONLY_BOOL, false}, {MENU_ENUM_LABEL_VIDEO_GPU_RECORD, PARSE_ONLY_BOOL, false}, + {MENU_ENUM_LABEL_VIDEO_RECORD_GAME_ONLY, PARSE_ONLY_BOOL, false}, + {MENU_ENUM_LABEL_VIDEO_RECORD_ALLOW_FRAME_DROP, PARSE_ONLY_BOOL, false}, + {MENU_ENUM_LABEL_VIDEO_RECORD_HW_ENCODER, PARSE_ONLY_BOOL, false}, + {MENU_ENUM_LABEL_VIDEO_RECORD_FIFO_FRAMES, PARSE_ONLY_UINT, false}, {MENU_ENUM_LABEL_STREAMING_MODE, PARSE_ONLY_UINT, false}, {MENU_ENUM_LABEL_VIDEO_STREAM_QUALITY, PARSE_ONLY_UINT, false}, {MENU_ENUM_LABEL_STREAM_CONFIG, PARSE_ONLY_PATH, false}, @@ -11506,11 +11510,15 @@ unsigned menu_displaylist_build_list( build_list[3].checked = is_ffmpeg; /* MENU_ENUM_LABEL_VIDEO_RECORD_THREADS */ build_list[4].checked = has_video; /* MENU_ENUM_LABEL_VIDEO_POST_FILTER_RECORD */ build_list[5].checked = has_video; /* MENU_ENUM_LABEL_VIDEO_GPU_RECORD */ - build_list[6].checked = is_ffmpeg; /* MENU_ENUM_LABEL_STREAMING_MODE */ - build_list[7].checked = is_ffmpeg; /* MENU_ENUM_LABEL_VIDEO_STREAM_QUALITY */ - build_list[8].checked = is_ffmpeg; /* MENU_ENUM_LABEL_STREAM_CONFIG */ - build_list[9].checked = is_ffmpeg; /* MENU_ENUM_LABEL_STREAMING_TITLE */ - build_list[10].checked = is_ffmpeg; /* MENU_ENUM_LABEL_STREAMING_URL */ + build_list[6].checked = has_video; /* MENU_ENUM_LABEL_VIDEO_RECORD_GAME_ONLY */ + build_list[7].checked = is_ffmpeg; /* MENU_ENUM_LABEL_VIDEO_RECORD_ALLOW_FRAME_DROP */ + build_list[8].checked = is_ffmpeg; /* MENU_ENUM_LABEL_VIDEO_RECORD_HW_ENCODER */ + build_list[9].checked = is_ffmpeg; /* MENU_ENUM_LABEL_VIDEO_RECORD_FIFO_FRAMES */ + build_list[10].checked = is_ffmpeg; /* MENU_ENUM_LABEL_STREAMING_MODE */ + build_list[11].checked = is_ffmpeg; /* MENU_ENUM_LABEL_VIDEO_STREAM_QUALITY */ + build_list[12].checked = is_ffmpeg; /* MENU_ENUM_LABEL_STREAM_CONFIG */ + build_list[13].checked = is_ffmpeg; /* MENU_ENUM_LABEL_STREAMING_TITLE */ + build_list[14].checked = is_ffmpeg; /* MENU_ENUM_LABEL_STREAMING_URL */ for (i = 0; i < ARRAY_SIZE(build_list); i++) { diff --git a/msg_hash_lbl_str.h b/msg_hash_lbl_str.h index b9e141c726e7..85e08da8f2c6 100644 --- a/msg_hash_lbl_str.h +++ b/msg_hash_lbl_str.h @@ -1719,6 +1719,10 @@ #define MENU_ENUM_LABEL_VIDEO_OVERSCAN_CORRECTION_TOP_STR "video_overscan_correction_top" #define MENU_ENUM_LABEL_VIDEO_POST_FILTER_RECORD_STR "video_post_filter_record" #define MENU_ENUM_LABEL_VIDEO_PRESENT_TIMING_FROM_DISPLAY_STR "video_present_timing_from_display" +#define MENU_ENUM_LABEL_VIDEO_RECORD_ALLOW_FRAME_DROP_STR "video_record_allow_frame_drop" +#define MENU_ENUM_LABEL_VIDEO_RECORD_FIFO_FRAMES_STR "video_record_fifo_frames" +#define MENU_ENUM_LABEL_VIDEO_RECORD_GAME_ONLY_STR "video_record_game_only" +#define MENU_ENUM_LABEL_VIDEO_RECORD_HW_ENCODER_STR "video_record_hw_encoder" #define MENU_ENUM_LABEL_VIDEO_RECORD_QUALITY_STR "video_record_quality" #define MENU_ENUM_LABEL_VIDEO_RECORD_THREADS_STR "video_record_threads" #define MENU_ENUM_LABEL_VIDEO_REFRESH_RATE_STR "video_refresh_rate" diff --git a/record/drivers/record_ffmpeg.c b/record/drivers/record_ffmpeg.c index 0b4b6a1190fa..8d2da4368634 100644 --- a/record/drivers/record_ffmpeg.c +++ b/record/drivers/record_ffmpeg.c @@ -44,6 +44,14 @@ #include #endif +#if defined(__linux__) +#include +#include +#include +#include +#define FFMPEG_HAVE_THREAD_NICE 1 +#endif + #include "record_ffmpeg.h" #ifdef __cplusplus @@ -70,6 +78,8 @@ extern "C" { #endif #include "../../retroarch.h" +#include "../../runloop.h" +#include "../../performance_counters.h" #include "../../verbosity.h" #ifndef FFMPEG3 @@ -204,6 +214,15 @@ struct ff_config_param bool video_qscale; int video_global_quality; int video_bit_rate; + /* Rate control / GOP for bitrate-constrained outputs (RTMP). + * 0 (or <0 for max_b_frames) leaves the encoder default. */ + int video_max_rate; + int video_buf_size; + float video_gop_seconds; + int video_max_b_frames; + /* Bits per pixel per frame used to derive a bitrate for hardware + * encoders when the preset is CRF-based (hardware has no CRF). */ + float video_hw_bpp; AVDictionary *video_opts; AVDictionary *audio_opts; @@ -247,15 +266,53 @@ typedef struct ffmpeg retro_eventcount_t space; bool data_init; bool space_init; + + /* Drop-on-full accounting. Written by the producer only; the + * totals are read after the encoder thread has been joined. */ + uint64_t video_frames_in; + uint64_t video_frames_dropped; + /* Frames dropped since the last queued one; rides along with the + * next frame so the encoder leaves a pts gap for them. */ + unsigned video_pts_pending; + /* With audio, video is placed on the audio clock: every audio frame + * the core produced is its real elapsed time, and video_next_pts is + * the pts the encoder gives the next queued frame. Cores that don't + * present every tick (30 fps games on a 60 Hz core, frame skip, + * run-ahead) then still get the right spacing. Producer only. */ + uint64_t audio_frames_in; + int64_t video_next_pts; + /* For cores that stop producing audio: ffmpeg_fill_audio_gap(). */ + uint64_t audio_frames_at_last_video; + unsigned video_frames_without_audio; + double silence_frac; + /* Encoder thread: scratch for params.rotation, fb_dims pixels, + * allocated on first use. */ + uint8_t *video_rot_buf; + unsigned video_fifo_frames; + bool allow_frame_drop; + + /* Audio bytes dropped on a full queue and still owed to it as + * silence, so the sample-count timeline never loses time. */ + size_t audio_silence_pending; + uint64_t audio_bytes_dropped; + + /* Encoder thread: pts of the last keyframe forced for + * video_gop_seconds. */ + int64_t video_last_key_pts; } ffmpeg_t; +/* attr_fifo record: the frame plus the drops right before it. */ +struct ff_video_attr +{ + struct record_video_data vid; + unsigned pts_skip; +}; + /* How long a push waits for room before looking at alive again. The * encoder notifies space on every read and deinit_thread() on the way * out, so this only bounds a wake that never comes. */ #define FFMPEG_PUSH_WAIT_US 100000 -AVFormatContext *ctx; - /* Returns the encoder's list of supported sample formats, terminated by * AV_SAMPLE_FMT_NONE, or NULL if the encoder does not restrict sample * formats (or the list could not be queried). */ @@ -516,66 +573,200 @@ static bool ffmpeg_init_audio(ffmpeg_t *handle, const char *audio_resampler) return true; } -static bool ffmpeg_init_video(ffmpeg_t *handle) +/* Returns the encoder's supported pixel formats, terminated by + * AV_PIX_FMT_NONE, or NULL if unrestricted / unknown. */ +static const enum AVPixelFormat *ffmpeg_codec_pix_fmts(const AVCodec *codec) +{ +#if HAVE_AVCODEC_GET_SUPPORTED_CONFIG + const void *fmts = NULL; + if (avcodec_get_supported_config(NULL, codec, + AV_CODEC_CONFIG_PIX_FORMAT, 0, &fmts, NULL) < 0) + return NULL; + return (const enum AVPixelFormat*)fmts; +#else + return codec->pix_fmts; +#endif +} + +/* H.264 encoders that take frames from system memory, tried in order. + * VAAPI is left out: it needs a hw_frames_ctx upload path. */ +static const char *const ffmpeg_hw_h264_encoders[] = { + "h264_nvv4l2", /* Tegra (L4T FFmpeg) */ + "h264_nvmpi", /* Tegra/Jetson (jetson-ffmpeg) */ + "h264_v4l2m2m", /* V4L2 mem2mem: RPi, Exynos, ... */ + "h264_rkmpp", /* Rockchip */ + "h264_nvenc", /* NVIDIA desktop */ + "h264_amf", /* AMD */ + "h264_qsv", /* Intel */ + "h264_videotoolbox", /* Apple */ + "h264_mediacodec", /* Android */ + NULL +}; + +/* Pick YUV420P, else NV12, from what the encoder accepts. */ +static enum AVPixelFormat ffmpeg_hw_pick_pix_fmt(const AVCodec *codec) +{ + unsigned i; + bool has_nv12 = false; + const enum AVPixelFormat *fmts = ffmpeg_codec_pix_fmts(codec); + + if (!fmts) + return AV_PIX_FMT_YUV420P; + + for (i = 0; fmts[i] != AV_PIX_FMT_NONE; i++) + { + if (fmts[i] == AV_PIX_FMT_YUV420P) + return AV_PIX_FMT_YUV420P; + if (fmts[i] == AV_PIX_FMT_NV12) + has_nv12 = true; + } + return has_nv12 ? AV_PIX_FMT_NV12 : AV_PIX_FMT_NONE; +} + +/* Allocates and opens handle->video.codec; frees it on failure. */ +static bool ffmpeg_open_video_codec(ffmpeg_t *handle, + const AVCodec *codec, enum AVPixelFormat pix_fmt, + unsigned out_w, unsigned out_h, bool hw, AVDictionary **opts) { - size_t size; - AVFrame *frame; struct ff_config_param *params = &handle->config; struct ff_video_info *video = &handle->video; struct record_params *param = &handle->params; - const AVCodec *codec = NULL; - unsigned out_w, out_h; + AVCodecContext *c; - if (*params->vcodec) - codec = avcodec_find_encoder_by_name(params->vcodec); - else + /* Ensure even dimensions for chroma-subsampled pixel formats. + * Odd dimensions cause encoder init failure with e.g. libx264. + * Round up (pad) rather than down so no source pixels are lost. */ + if ( pix_fmt == AV_PIX_FMT_YUV420P + || pix_fmt == AV_PIX_FMT_YUV422P + || pix_fmt == AV_PIX_FMT_NV12) { - /* By default, lossless video. */ - av_dict_set(¶ms->video_opts, "qp", "0", 0); - codec = avcodec_find_encoder_by_name("libx264rgb"); + out_w = (out_w + 1) & ~1; + out_h = (out_h + 1) & ~1; } - if (!codec) + if (!(c = avcodec_alloc_context3(codec))) + return false; + + c->codec_type = AVMEDIA_TYPE_VIDEO; + c->width = out_w; + c->height = out_h; + c->time_base = av_d2q((double) + params->frame_drop_ratio /param->fps, 1000000); /* Arbitrary big number. */ + c->framerate = av_inv_q(c->time_base); + c->sample_aspect_ratio = av_d2q( + param->aspect_ratio * out_h / out_w, 255); + c->pix_fmt = pix_fmt; + c->thread_count = params->threads; + + if (params->video_qscale) { - RARCH_ERR("[FFmpeg] Cannot find vcodec %s.\n", - *params->vcodec ? params->vcodec : "libx264rgb"); + c->flags |= AV_CODEC_FLAG_QSCALE; + c->global_quality = params->video_global_quality; + } + else if (params->video_bit_rate) + c->bit_rate = params->video_bit_rate; + else if (hw) + { + /* No CRF in hardware; derive a bitrate from the preset. */ + float bpp = params->video_hw_bpp > 0.0f + ? params->video_hw_bpp : 0.1f; + double rate = (double)bpp * out_w * out_h + * param->fps / params->frame_drop_ratio; + if (rate < 500000.0) + rate = 500000.0; + c->bit_rate = (int64_t)rate; + } + + if (params->video_max_rate) + c->rc_max_rate = params->video_max_rate; + if (params->video_buf_size) + c->rc_buffer_size = params->video_buf_size; + if (params->video_max_b_frames >= 0) + c->max_b_frames = params->video_max_b_frames; + if (params->video_gop_seconds > 0.0f) + { + int gop = (int)(params->video_gop_seconds * param->fps + / params->frame_drop_ratio + 0.5f); + if (gop < 1) + gop = 1; + c->gop_size = gop; + } + + if (handle->muxer.ctx->oformat->flags & AVFMT_GLOBALHEADER) + c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + + if (avcodec_open2(c, codec, opts) != 0) + { + avcodec_free_context(&c); return false; } - video->encoder = codec; + video->codec = c; + video->encoder = codec; + param->out_dims = VIDEO_SCALE_PACK(out_w, out_h); + return true; +} - /* Don't use swscaler unless format is not something "in-house" scaler - * supports. - * - * libswscale doesn't scale RGB -> RGB correctly (goes via YUV first), - * and it's non-trivial to fix upstream as it's heavily geared towards YUV. - * If we're dealing with strange formats or YUV, just use libswscale. - */ - if (params->out_pix_fmt != AV_PIX_FMT_NONE) +/* Try the hardware H.264 encoders this FFmpeg build has, in order. */ +static bool ffmpeg_open_hw_h264(ffmpeg_t *handle, + unsigned out_w, unsigned out_h, enum AVPixelFormat *out_fmt) +{ + unsigned i; + struct ff_config_param *params = &handle->config; + + for (i = 0; ffmpeg_hw_h264_encoders[i]; i++) { - video->pix_fmt = params->out_pix_fmt; - if (video->pix_fmt != AV_PIX_FMT_BGR24 && video->pix_fmt != AV_PIX_FMT_RGB32) - video->use_sws = true; + AVDictionary *opts = NULL; + enum AVPixelFormat fmt; + const AVCodec *codec = avcodec_find_encoder_by_name( + ffmpeg_hw_h264_encoders[i]); - switch (video->pix_fmt) + if (!codec) + continue; + if ((fmt = ffmpeg_hw_pick_pix_fmt(codec)) == AV_PIX_FMT_NONE) { - case AV_PIX_FMT_BGR24: - video->scaler.out_fmt = SCALER_FMT_BGR24; - break; + RARCH_LOG("[FFmpeg] %s: no YUV420P/NV12 input, skipping.\n", + codec->name); + continue; + } - case AV_PIX_FMT_RGB32: - video->scaler.out_fmt = SCALER_FMT_ARGB8888; - break; + /* x264's private options mean nothing to these. Ask for CBR where + * there's an "rc" knob; an encoder without one leaves the option + * unused, and the stream is then held only by the generic rate + * limits. */ + if (params->video_max_rate) + av_dict_set(&opts, "rc", "cbr", 0); - default: - break; + if (ffmpeg_open_video_codec(handle, codec, fmt, out_w, out_h, + true, &opts)) + { + if (av_dict_get(opts, "rc", NULL, 0)) + RARCH_WARN("[FFmpeg] %s has no rate control option: not CBR.\n", + codec->name); + av_dict_free(&opts); + *out_fmt = fmt; + RARCH_LOG("[FFmpeg] Using hardware encoder %s (%s).\n", + codec->name, av_get_pix_fmt_name(fmt)); + return true; } + av_dict_free(&opts); + RARCH_WARN("[FFmpeg] Hardware encoder %s failed to open.\n", + codec->name); } - else /* Use BGR24 as default out format. */ - { - video->pix_fmt = AV_PIX_FMT_BGR24; - video->scaler.out_fmt = SCALER_FMT_BGR24; - } + return false; +} + +static bool ffmpeg_init_video(ffmpeg_t *handle) +{ + size_t size; + AVFrame *frame; + struct ff_config_param *params = &handle->config; + struct ff_video_info *video = &handle->video; + struct record_params *param = &handle->params; + const AVCodec *codec = NULL; + enum AVPixelFormat pix_fmt = AV_PIX_FMT_BGR24; + bool opened = false; + unsigned out_w, out_h; switch (param->pix_fmt) { @@ -601,50 +792,73 @@ static bool ffmpeg_init_video(ffmpeg_t *handle) return false; } - video->codec = avcodec_alloc_context3(codec); - /* Useful to set scale_factor to 2 for chroma subsampled formats to * maintain full chroma resolution. (Or just use 4:4:4 or RGB ...) */ out_w = (float)VIDEO_SCALE_W(param->out_dims) * params->scale_factor; out_h = (float)VIDEO_SCALE_H(param->out_dims) * params->scale_factor; - /* Ensure even dimensions for chroma-subsampled pixel formats. - * Odd dimensions cause encoder init failure with e.g. libx264. - * Round up (pad) rather than down so no source pixels are lost. */ - if ( video->pix_fmt == AV_PIX_FMT_YUV420P - || video->pix_fmt == AV_PIX_FMT_YUV422P) + /* Hardware H.264 replaces libx264 for the built-in presets only; + * an explicit vcodec in a custom config is left alone. */ + if ( param->hw_encoder + && !params->conf + && string_is_equal(params->vcodec, "libx264")) { - out_w = (out_w + 1) & ~1; - out_h = (out_h + 1) & ~1; + opened = ffmpeg_open_hw_h264(handle, out_w, out_h, &pix_fmt); + if (!opened) + RARCH_WARN("[FFmpeg] No usable hardware H.264 encoder," + " falling back to libx264.\n"); } - param->out_dims = VIDEO_SCALE_PACK(out_w, out_h); - video->codec->codec_type = AVMEDIA_TYPE_VIDEO; - video->codec->width = out_w; - video->codec->height = out_h; - video->codec->time_base = av_d2q((double) - params->frame_drop_ratio /param->fps, 1000000); /* Arbitrary big number. */ - video->codec->sample_aspect_ratio = av_d2q( - param->aspect_ratio * out_h / out_w, 255); - video->codec->pix_fmt = video->pix_fmt; + if (!opened) + { + if (*params->vcodec) + codec = avcodec_find_encoder_by_name(params->vcodec); + else + { + /* By default, lossless video. */ + av_dict_set(¶ms->video_opts, "qp", "0", 0); + codec = avcodec_find_encoder_by_name("libx264rgb"); + } - video->codec->thread_count = params->threads; + if (!codec) + { + RARCH_ERR("[FFmpeg] Cannot find vcodec %s.\n", + *params->vcodec ? params->vcodec : "libx264rgb"); + return false; + } - if (params->video_qscale) - { - video->codec->flags |= AV_CODEC_FLAG_QSCALE; - video->codec->global_quality = params->video_global_quality; + if (params->out_pix_fmt != AV_PIX_FMT_NONE) + pix_fmt = params->out_pix_fmt; + + if (!ffmpeg_open_video_codec(handle, codec, pix_fmt, out_w, out_h, + false, params->video_opts ? ¶ms->video_opts : NULL)) + return false; } - else if (params->video_bit_rate) - video->codec->bit_rate = params->video_bit_rate; - if (handle->muxer.ctx->oformat->flags & AVFMT_GLOBALHEADER) - video->codec->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + out_w = VIDEO_SCALE_W(param->out_dims); + out_h = VIDEO_SCALE_H(param->out_dims); - if (avcodec_open2(video->codec, codec, params->video_opts ? - ¶ms->video_opts : NULL) != 0) - return false; + /* Don't use swscaler unless format is not something "in-house" scaler + * supports. + * + * libswscale doesn't scale RGB -> RGB correctly (goes via YUV first), + * and it's non-trivial to fix upstream as it's heavily geared towards YUV. + * If we're dealing with strange formats or YUV, just use libswscale. + */ + video->pix_fmt = pix_fmt; + switch (pix_fmt) + { + case AV_PIX_FMT_BGR24: + video->scaler.out_fmt = SCALER_FMT_BGR24; + break; + case AV_PIX_FMT_RGB32: + video->scaler.out_fmt = SCALER_FMT_ARGB8888; + break; + default: + video->use_sws = true; + break; + } video->frame_drop_ratio = params->frame_drop_ratio; @@ -672,6 +886,48 @@ static bool ffmpeg_init_video(ffmpeg_t *handle) return true; } +/* RTMP ingest (Twitch, YouTube, Facebook, Kick) wants H.264 + AAC-LC in + * FLV, CBR video, a fixed 2 s keyframe interval and 44.1/48 kHz audio. + * Twitch caps non-partner video at 6000 kbps and audio at 160 kbps. */ +static void ffmpeg_config_rtmp(struct ff_config_param *params, + unsigned preset) +{ + int kbps; + + switch (preset) + { + case RECORD_CONFIG_TYPE_STREAMING_LOW_QUALITY: + kbps = 2500; + break; + case RECORD_CONFIG_TYPE_STREAMING_MED_QUALITY: + kbps = 4500; + break; + default: + kbps = 6000; + break; + } + + params->video_qscale = false; + params->video_bit_rate = kbps * 1000; + params->video_max_rate = kbps * 1000; + params->video_buf_size = kbps * 1000; + params->video_gop_seconds = 2.0f; + params->video_max_b_frames = 0; + + /* CBR instead of CRF. nal-hrd=cbr makes x264 pad to a true CBR + * stream, which is what the ingest servers expect. */ + av_dict_set(¶ms->video_opts, "crf", NULL, 0); + av_dict_set(¶ms->video_opts, "nal-hrd", "cbr", 0); + /* No extra keyframes on scene cuts: ingest wants a fixed cadence. + * (keyint_min can't do this; x264 clamps it to keyint/2+1.) */ + av_dict_set(¶ms->video_opts, "x264-params", "scenecut=0", 0); + + params->audio_qscale = false; + params->audio_bit_rate = 160000; + params->sample_rate = 48000; + av_dict_set(¶ms->audio_opts, "audio_global_quality", NULL, 0); +} + static bool ffmpeg_init_config_common(struct ff_config_param *params, unsigned preset, bool video_gpu_record, @@ -680,6 +936,8 @@ static bool ffmpeg_init_config_common(struct ff_config_param *params, unsigned streaming_mode, unsigned video_record_threads) { + params->video_max_b_frames = -1; + switch (preset) { case RECORD_CONFIG_TYPE_RECORDING_LOW_QUALITY: @@ -693,6 +951,7 @@ static bool ffmpeg_init_config_common(struct ff_config_param *params, strlcpy_lit(params->vcodec, "libx264", sizeof(params->vcodec)); strlcpy_lit(params->acodec, "aac", sizeof(params->acodec)); + params->video_hw_bpp = 0.05f; av_dict_set(¶ms->video_opts, "preset", "ultrafast", 0); av_dict_set(¶ms->video_opts, "tune", "film", 0); av_dict_set(¶ms->video_opts, "crf", "35", 0); @@ -709,6 +968,7 @@ static bool ffmpeg_init_config_common(struct ff_config_param *params, strlcpy_lit(params->vcodec, "libx264", sizeof(params->vcodec)); strlcpy_lit(params->acodec, "aac", sizeof(params->acodec)); + params->video_hw_bpp = 0.1f; av_dict_set(¶ms->video_opts, "preset", "superfast", 0); av_dict_set(¶ms->video_opts, "tune", "film", 0); av_dict_set(¶ms->video_opts, "crf", "25", 0); @@ -725,6 +985,7 @@ static bool ffmpeg_init_config_common(struct ff_config_param *params, strlcpy_lit(params->vcodec, "libx264", sizeof(params->vcodec)); strlcpy_lit(params->acodec, "aac", sizeof(params->acodec)); + params->video_hw_bpp = 0.2f; av_dict_set(¶ms->video_opts, "preset", "superfast", 0); av_dict_set(¶ms->video_opts, "tune", "film", 0); av_dict_set(¶ms->video_opts, "crf", "15", 0); @@ -872,7 +1133,10 @@ static bool ffmpeg_init_config_common(struct ff_config_param *params, || streaming_mode == STREAMING_MODE_TWITCH || streaming_mode == STREAMING_MODE_FACEBOOK || streaming_mode == STREAMING_MODE_KICK) + { strlcpy_lit(params->format, "flv", sizeof(params->format)); + ffmpeg_config_rtmp(params, preset); + } else strlcpy_lit(params->format, "mpegts", sizeof(params->format)); } @@ -896,6 +1160,7 @@ static bool ffmpeg_init_config(struct ff_config_param *params, params->threads = 1; params->frame_drop_ratio = 1; params->audio_enable = true; + params->video_max_b_frames = -1; if (!config) return true; @@ -966,6 +1231,7 @@ static bool ffmpeg_init_config(struct ff_config_param *params, static bool ffmpeg_init_muxer_pre(ffmpeg_t *handle) { + AVFormatContext *ctx; #if !FFMPEG3 size_t _len; #endif @@ -1037,12 +1303,80 @@ static bool ffmpeg_init_muxer_post(ffmpeg_t *handle) return avformat_write_header(handle->muxer.ctx, NULL) >= 0; } -#define MAX_FRAMES 32 +/* Audio queue depth in seconds of input audio. */ +#define AUDIO_FIFO_SECONDS 2 +/* Video queue depth, from video_record_fifo_frames. */ +#define DEFAULT_VIDEO_FIFO_FRAMES 32 +#define MIN_VIDEO_FIFO_FRAMES 8 +#define MAX_VIDEO_FIFO_FRAMES 128 + +/* Reported at exit with perfcnt_enable, and readable live from the + * menu, which reads and resets them on the frontend's thread: so only + * the push_* side, which runs there. */ +static struct retro_perf_counter ffmpeg_perf_push_video = {0}; +static struct retro_perf_counter ffmpeg_perf_push_audio = {0}; + +static bool ffmpeg_perf_enabled(void) +{ + return runloop_state_get_ptr()->perfcnt_enable; +} + +static void ffmpeg_perf_init(void) +{ + performance_counter_init(ffmpeg_perf_push_video, "record_push_video"); + performance_counter_init(ffmpeg_perf_push_audio, "record_push_audio"); +} static void ffmpeg_thread(void *data); +/* Encoder threads run this much nicer than the core, in drop mode + * only: with blocking on, a starved encoder just stalls the game. */ +#define ENCODER_NICE_DELTA 10 + +#ifdef FFMPEG_HAVE_THREAD_NICE +/* Linux nice is per thread and a new thread takes its creator's, so a + * thread that lowers itself before it opens the encoders has every + * thread they spawn run lowered too, and nothing else. */ +static void ffmpeg_lower_this_thread(void) +{ + int cur; + id_t tid = (id_t)syscall(SYS_gettid); + + errno = 0; + cur = getpriority(PRIO_PROCESS, tid); + if (cur == -1 && errno) + return; + /* Raising nice never needs privileges. */ + setpriority(PRIO_PROCESS, tid, cur + ENCODER_NICE_DELTA); +} + +typedef struct +{ + ffmpeg_t *handle; + const char *audio_resampler; + bool ok; +} ffmpeg_open_job_t; + +static void ffmpeg_open_codecs_lowered(void *data) +{ + ffmpeg_open_job_t *job = (ffmpeg_open_job_t*)data; + ffmpeg_lower_this_thread(); + job->ok = ffmpeg_init_video(job->handle) + && ( !job->handle->config.audio_enable + || ffmpeg_init_audio(job->handle, job->audio_resampler)); +} +#endif + static bool init_thread(ffmpeg_t *handle) { + unsigned vf = handle->video_fifo_frames; + double rate = handle->params.samplerate > 0.0 + ? handle->params.samplerate : 48000.0; + + if (vf < MIN_VIDEO_FIFO_FRAMES || vf > MAX_VIDEO_FIFO_FRAMES) + vf = DEFAULT_VIDEO_FIFO_FRAMES; + handle->video_fifo_frames = vf; + handle->data_init = retro_eventcount_init(&handle->data); handle->space_init = retro_eventcount_init(&handle->space); if (!handle->data_init || !handle->space_init) @@ -1061,14 +1395,16 @@ static bool init_thread(ffmpeg_t *handle) * larger attr ring cannot admit a frame the video ring lacks * room for.) */ handle->fifos_init = - retro_spsc_init(&handle->audio_fifo, 32000 * sizeof(int16_t) * - handle->params.channels * MAX_FRAMES / 60) /* Some arbitrary max size. */ - && retro_spsc_init(&handle->attr_fifo, sizeof(struct record_video_data) * MAX_FRAMES) + retro_spsc_init(&handle->audio_fifo, (size_t)(rate * AUDIO_FIFO_SECONDS) + * handle->params.channels * sizeof(int16_t)) + && retro_spsc_init(&handle->attr_fifo, sizeof(struct ff_video_attr) * vf) && retro_spsc_init(&handle->video_fifo, - VIDEO_SCALE_AREA(handle->params.fb_dims) * - handle->video.pix_size * MAX_FRAMES); + (size_t)VIDEO_SCALE_AREA(handle->params.fb_dims) * + handle->video.pix_size * vf); if (!handle->fifos_init) { + RARCH_ERR("[FFmpeg] Failed to allocate recording queues" + " (%u video frames).\n", vf); retro_spsc_free(&handle->audio_fifo); retro_spsc_free(&handle->attr_fifo); retro_spsc_free(&handle->video_fifo); @@ -1080,6 +1416,20 @@ static bool init_thread(ffmpeg_t *handle) retro_atomic_store_release_int(&handle->alive, 1); handle->thread = sthread_create(ffmpeg_thread, handle); + if (!handle->thread) + { + RARCH_ERR("[FFmpeg] Failed to create encoder thread.\n"); + retro_atomic_store_release_int(&handle->alive, 0); + retro_eventcount_free(&handle->data); + retro_eventcount_free(&handle->space); + handle->data_init = handle->space_init = false; + return false; + } + + RARCH_LOG("[FFmpeg] Video queue: %u frames (%.1f MiB), drop-on-full: %s.\n", + vf, ((double)VIDEO_SCALE_AREA(handle->params.fb_dims) + * handle->video.pix_size * vf) / (1024.0 * 1024.0), + handle->allow_frame_drop ? "yes" : "no"); return true; } @@ -1120,6 +1470,23 @@ static void ffmpeg_free(void *data) deinit_thread(handle); deinit_thread_buf(handle); + if (handle->video_frames_in > 0) + RARCH_LOG("[FFmpeg] Recording ended: %llu video frames in," + " %llu dropped (%.2f%%).\n", + (unsigned long long)handle->video_frames_in, + (unsigned long long)handle->video_frames_dropped, + 100.0 * (double)handle->video_frames_dropped + / (double)handle->video_frames_in); + if (handle->audio_bytes_dropped && handle->params.samplerate > 0.0) + RARCH_LOG("[FFmpeg] Recording ended: %.2f s of audio replaced" + " with silence.\n", + (double)handle->audio_bytes_dropped + / (handle->params.samplerate + * handle->params.channels * sizeof(int16_t))); + + av_free(handle->video_rot_buf); + handle->video_rot_buf = NULL; + if (handle->audio.codec) { #if FFMPEG8 @@ -1185,16 +1552,122 @@ static void ffmpeg_free(void *data) /* The muxer context owns its streams, their codec parameters, its * metadata and its url; releasing just the struct left the rest * behind on every session. */ + /* Failed before finalize: the open ffmpeg_init_muxer_pre made */ + if (handle->muxer.ctx && handle->muxer.ctx->pb) + avio_closep(&handle->muxer.ctx->pb); avformat_free_context(handle->muxer.ctx); av_packet_free(&handle->pkt); free(handle); + av_log_set_callback(av_log_default_callback); + #if FFMPEG3 avformat_network_deinit(); #endif } +/* Route libav* logging into RetroArch's log, which is flushed per line + * and so survives a crash. FFmpeg's level follows the frontend log + * level: Debug gets AV_LOG_DEBUG (codec internals, e.g. the nvv4l2 + * encoder's steps), Info gets AV_LOG_INFO, and so on. */ +/* FFmpeg often builds one line out of several av_log() calls, so + * pieces are collected until a newline. Calls come from any thread. */ +static slock_t *ffmpeg_av_log_lock; +static char ffmpeg_av_log_line[1024]; +static size_t ffmpeg_av_log_len; +static int ffmpeg_av_log_level; +static int ffmpeg_av_log_prefix = 1; +static void *ffmpeg_av_log_ctx; + +static void ffmpeg_av_log_emit(int level, const char *line) +{ + if (level <= AV_LOG_ERROR) + RARCH_ERR("[FFmpeg/lav] %s\n", line); + else if (level <= AV_LOG_WARNING) + RARCH_WARN("[FFmpeg/lav] %s\n", line); + else if (level <= AV_LOG_INFO) + RARCH_LOG("[FFmpeg/lav] %s\n", line); + else + RARCH_DBG("[FFmpeg/lav] %s\n", line); +} + +static void ffmpeg_av_log_cb(void *avcl, int level, const char *fmt, + va_list vl) +{ + char piece[1024]; + size_t _len; + + if (level > av_log_get_level()) + return; + + if (ffmpeg_av_log_lock) + slock_lock(ffmpeg_av_log_lock); + + /* A partial line from another context (another thread or codec) + * is finished on its own rather than spliced with this one. */ + if (ffmpeg_av_log_len && avcl != ffmpeg_av_log_ctx) + { + ffmpeg_av_log_emit(ffmpeg_av_log_level, ffmpeg_av_log_line); + ffmpeg_av_log_len = 0; + ffmpeg_av_log_line[0] = '\0'; + ffmpeg_av_log_prefix = 1; + } + ffmpeg_av_log_ctx = avcl; + + av_log_format_line2(avcl, level, fmt, vl, piece, sizeof(piece), + &ffmpeg_av_log_prefix); + + if (!ffmpeg_av_log_len || level < ffmpeg_av_log_level) + ffmpeg_av_log_level = level; + ffmpeg_av_log_len += strlcpy(ffmpeg_av_log_line + ffmpeg_av_log_len, + piece, sizeof(ffmpeg_av_log_line) - ffmpeg_av_log_len); + if (ffmpeg_av_log_len >= sizeof(ffmpeg_av_log_line)) + ffmpeg_av_log_len = sizeof(ffmpeg_av_log_line) - 1; + + _len = ffmpeg_av_log_len; + if ( (_len && ffmpeg_av_log_line[_len - 1] == '\n') + || _len == sizeof(ffmpeg_av_log_line) - 1) + { + while (_len && (ffmpeg_av_log_line[_len - 1] == '\n' + || ffmpeg_av_log_line[_len - 1] == '\r')) + ffmpeg_av_log_line[--_len] = '\0'; + if (_len) + ffmpeg_av_log_emit(ffmpeg_av_log_level, ffmpeg_av_log_line); + ffmpeg_av_log_len = 0; + ffmpeg_av_log_line[0] = '\0'; + } + + if (ffmpeg_av_log_lock) + slock_unlock(ffmpeg_av_log_lock); +} + +static void ffmpeg_av_log_init(void) +{ + int av_level; + + if (!verbosity_is_enabled()) + av_level = AV_LOG_ERROR; + else + { + switch (verbosity_get_log_level()) + { + case 0: av_level = AV_LOG_DEBUG; break; + case 1: av_level = AV_LOG_INFO; break; + case 2: av_level = AV_LOG_WARNING; break; + default: av_level = AV_LOG_ERROR; break; + } + } + + /* Created once and kept: the callback may still be in use by + * another libav* user when a recording ends. */ + if (!ffmpeg_av_log_lock) + ffmpeg_av_log_lock = slock_new(); + + av_log_set_level(av_level); + av_log_set_callback(ffmpeg_av_log_cb); +} + static void *ffmpeg_new(const struct record_params *params) { ffmpeg_t *handle = (ffmpeg_t*)calloc(1, sizeof(*handle)); @@ -1206,8 +1679,14 @@ static void *ffmpeg_new(const struct record_params *params) avformat_network_init(); #endif - handle->params = *params; - handle->pkt = av_packet_alloc(); + ffmpeg_av_log_init(); + + ffmpeg_perf_init(); + + handle->params = *params; + handle->pkt = av_packet_alloc(); + handle->allow_frame_drop = params->allow_frame_drop; + handle->video_fifo_frames = params->video_fifo_frames; switch (params->preset) { @@ -1234,13 +1713,34 @@ static void *ffmpeg_new(const struct record_params *params) if (!ffmpeg_init_muxer_pre(handle)) goto error; - if (!ffmpeg_init_video(handle)) - goto error; - - if ( handle->config.audio_enable - && !ffmpeg_init_audio(handle, - params->audio_resampler)) - goto error; + { + bool ok; + bool opened = false; +#ifdef FFMPEG_HAVE_THREAD_NICE + /* In drop mode the encoders are opened on a thread that has + * lowered itself, so the threads they start run lowered. */ + if (handle->allow_frame_drop) + { + ffmpeg_open_job_t job; + sthread_t *opener; + job.handle = handle; + job.audio_resampler = params->audio_resampler; + job.ok = false; + if ((opener = sthread_create(ffmpeg_open_codecs_lowered, &job))) + { + sthread_join(opener); + ok = job.ok; + opened = true; + } + } +#endif + if (!opened) + ok = ffmpeg_init_video(handle) + && ( !handle->config.audio_enable + || ffmpeg_init_audio(handle, params->audio_resampler)); + if (!ok) + goto error; + } if (!ffmpeg_init_muxer_post(handle)) goto error; @@ -1255,12 +1755,68 @@ static void *ffmpeg_new(const struct record_params *params) return NULL; } -static bool ffmpeg_push_video(void *data, +static bool ffmpeg_video_fifo_has_room(ffmpeg_t *handle, size_t video_bytes) +{ + return retro_spsc_write_avail(&handle->attr_fifo) + >= sizeof(struct ff_video_attr) + && retro_spsc_write_avail(&handle->video_fifo) >= video_bytes; +} + +static void ffmpeg_audio_write_silence(ffmpeg_t *handle); + +/* Video frames in a row without audio before the core is taken to have + * gone silent: short enough to be imperceptible, long enough that cores + * delivering audio in bursts aren't affected. */ +#define SILENT_VIDEO_FRAMES 4 + +/* A core that produces no audio for a while (rather than silence) left + * that time out of the audio track, desyncing everything after it, and + * with video on the audio clock it would stall video too. After + * SILENT_VIDEO_FRAMES frames without audio, owe one frame period of + * silence per video frame, backfilling the ones already seen. */ +static void ffmpeg_fill_audio_gap(ffmpeg_t *handle) +{ + unsigned frames_to_fill; + double samples; + uint64_t whole; + + if ( !handle->config.audio_enable + || handle->params.samplerate <= 0.0 + || handle->params.fps <= 0.0) + return; + + if (handle->audio_frames_in != handle->audio_frames_at_last_video) + { + handle->audio_frames_at_last_video = handle->audio_frames_in; + handle->video_frames_without_audio = 0; + return; + } + + if (++handle->video_frames_without_audio < SILENT_VIDEO_FRAMES) + return; + + frames_to_fill = (handle->video_frames_without_audio == SILENT_VIDEO_FRAMES) + ? SILENT_VIDEO_FRAMES : 1; + samples = frames_to_fill * handle->params.samplerate + / handle->params.fps + handle->silence_frac; + whole = (uint64_t)samples; + handle->silence_frac = samples - (double)whole; + + handle->audio_frames_in += whole; + handle->audio_silence_pending += (size_t)whole + * handle->params.channels * sizeof(int16_t); + handle->audio_frames_at_last_video = handle->audio_frames_in; + ffmpeg_audio_write_silence(handle); + retro_eventcount_notify(&handle->data); +} + +static bool ffmpeg_push_video_impl(void *data, const struct record_video_data *vid) { unsigned y; unsigned rows; - struct record_video_data attr_data; + size_t video_bytes; + struct ff_video_attr attr; bool drop_frame = false; ffmpeg_t *handle = (ffmpeg_t*)data; int offset = 0; @@ -1268,6 +1824,11 @@ static bool ffmpeg_push_video(void *data, if (!handle || !vid) return false; + /* Every frame the core presents is a frame period of its time, + * whether or not frame_drop_ratio keeps it. */ + if (retro_atomic_load_acquire_int(&handle->alive)) + ffmpeg_fill_audio_gap(handle); + drop_frame = handle->video.frame_drop_count++ % handle->video.frame_drop_ratio; @@ -1276,62 +1837,108 @@ static bool ffmpeg_push_video(void *data, if (drop_frame) return true; + if (!retro_atomic_load_acquire_int(&handle->alive)) + return false; + /* Tightly pack our frame to conserve memory. * libretro tends to use a very large pitch. */ - attr_data = *vid; + attr.vid = *vid; + attr.pts_skip = 0; - if (attr_data.is_dupe) + if (attr.vid.is_dupe) { - attr_data.dims = 0; - attr_data.pitch = 0; + attr.vid.dims = 0; + attr.vid.pitch = 0; } else - attr_data.pitch = (int)(VIDEO_SCALE_W(attr_data.dims) + attr.vid.pitch = (int)(VIDEO_SCALE_W(attr.vid.dims) * handle->video.pix_size); - rows = VIDEO_SCALE_H(attr_data.dims); + rows = VIDEO_SCALE_H(attr.vid.dims); + video_bytes = (size_t)rows * attr.vid.pitch; + + handle->video_frames_in++; + + /* Drop mode: a full queue costs this frame, never the frontend. */ + if ( handle->allow_frame_drop + && !ffmpeg_video_fifo_has_room(handle, video_bytes)) + { + /* Counted, and said once when the recording ends */ + handle->video_frames_dropped++; + handle->video_pts_pending++; + return true; + } for (;;) { - /* Room for the attr and for the frame's bytes: the old check - * only asked the attr fifo, relying on the two being sized in - * step; the rings are sized independently now (power-of-two - * rounding), so ask both. */ int key; if (!retro_atomic_load_acquire_int(&handle->alive)) return false; - if ( retro_spsc_write_avail(&handle->attr_fifo) >= sizeof(attr_data) - && retro_spsc_write_avail(&handle->video_fifo) - >= (size_t)rows * attr_data.pitch) + if (ffmpeg_video_fifo_has_room(handle, video_bytes)) break; key = retro_eventcount_prepare_wait(&handle->space); if ( !retro_atomic_load_acquire_int(&handle->alive) - || ( retro_spsc_write_avail(&handle->attr_fifo) >= sizeof(attr_data) - && retro_spsc_write_avail(&handle->video_fifo) - >= (size_t)rows * attr_data.pitch)) + || ffmpeg_video_fifo_has_room(handle, video_bytes)) retro_eventcount_cancel_wait(&handle->space); else retro_eventcount_commit_wait_timeout(&handle->space, key, FFMPEG_PUSH_WAIT_US); } + attr.pts_skip = handle->video_pts_pending; + if ( handle->config.audio_enable + && handle->audio_frames_in + && handle->params.samplerate > 0.0) + { + /* This frame's place on the audio timeline, in codec time base + * units; never behind the previous frame. */ + double secs = (double)handle->audio_frames_in + / handle->params.samplerate; + int64_t target = (int64_t)(secs * handle->params.fps + / handle->video.frame_drop_ratio + 0.5); + attr.pts_skip = (target > handle->video_next_pts) + ? (unsigned)(target - handle->video_next_pts) : 0; + } + handle->video_next_pts += (int64_t)attr.pts_skip + 1; + handle->video_pts_pending = 0; + /* Frame first, attr last: the encoder takes the attr as the * signal that a whole frame is behind it, and the ring's * release/acquire on each write orders the rows before it. */ for (y = 0; y < rows; y++, offset += vid->pitch) retro_spsc_write(&handle->video_fifo, - (const uint8_t*)vid->data + offset, attr_data.pitch); + (const uint8_t*)vid->data + offset, attr.vid.pitch); - retro_spsc_write(&handle->attr_fifo, &attr_data, sizeof(attr_data)); + retro_spsc_write(&handle->attr_fifo, &attr, sizeof(attr)); retro_eventcount_notify(&handle->data); return true; } -static bool ffmpeg_push_audio(void *data, +/* Writes as much owed silence as fits, in whole frames. */ +static void ffmpeg_audio_write_silence(ffmpeg_t *handle) +{ + static const int16_t zeros[1024] = {0}; + size_t avail = retro_spsc_write_avail(&handle->audio_fifo); + size_t todo = handle->audio_silence_pending; + size_t frame = handle->params.channels * sizeof(int16_t); + + if (todo > avail) + todo = avail - (avail % frame); + + handle->audio_silence_pending -= todo; + while (todo) + { + size_t chunk = todo < sizeof(zeros) ? todo : sizeof(zeros); + retro_spsc_write(&handle->audio_fifo, zeros, chunk); + todo -= chunk; + } +} + +static bool ffmpeg_push_audio_impl(void *data, const struct record_audio_data *audio_data) { ffmpeg_t *handle = (ffmpeg_t*)data; @@ -1344,6 +1951,26 @@ static bool ffmpeg_push_audio(void *data, return true; need = audio_data->frames * handle->params.channels * sizeof(int16_t); + handle->audio_frames_in += audio_data->frames; + + if (handle->allow_frame_drop) + { + if (!retro_atomic_load_acquire_int(&handle->alive)) + return false; + /* A chunk that doesn't fit is owed as silence, not waited for. */ + if (handle->audio_silence_pending) + ffmpeg_audio_write_silence(handle); + if ( !handle->audio_silence_pending + && retro_spsc_write_avail(&handle->audio_fifo) >= need) + retro_spsc_write(&handle->audio_fifo, audio_data->data, need); + else + { + handle->audio_silence_pending += need; + handle->audio_bytes_dropped += need; + } + retro_eventcount_notify(&handle->data); + return true; + } for (;;) { @@ -1351,12 +1978,20 @@ static bool ffmpeg_push_audio(void *data, if (!retro_atomic_load_acquire_int(&handle->alive)) return false; - if (retro_spsc_write_avail(&handle->audio_fifo) >= need) + /* Silence owed from a gap goes in first, in order. */ + if (handle->audio_silence_pending) + { + ffmpeg_audio_write_silence(handle); + retro_eventcount_notify(&handle->data); + } + if ( !handle->audio_silence_pending + && retro_spsc_write_avail(&handle->audio_fifo) >= need) break; key = retro_eventcount_prepare_wait(&handle->space); if ( !retro_atomic_load_acquire_int(&handle->alive) - || retro_spsc_write_avail(&handle->audio_fifo) >= need) + || ( !handle->audio_silence_pending + && retro_spsc_write_avail(&handle->audio_fifo) >= need)) retro_eventcount_cancel_wait(&handle->space); else retro_eventcount_commit_wait_timeout(&handle->space, key, @@ -1369,6 +2004,28 @@ static bool ffmpeg_push_audio(void *data, return true; } +static bool ffmpeg_push_video(void *data, + const struct record_video_data *vid) +{ + bool ret; + bool perf = ffmpeg_perf_enabled(); + performance_counter_start_plus(perf, ffmpeg_perf_push_video); + ret = ffmpeg_push_video_impl(data, vid); + performance_counter_stop_plus(perf, ffmpeg_perf_push_video); + return ret; +} + +static bool ffmpeg_push_audio(void *data, + const struct record_audio_data *audio_data) +{ + bool ret; + bool perf = ffmpeg_perf_enabled(); + performance_counter_start_plus(perf, ffmpeg_perf_push_audio); + ret = ffmpeg_push_audio_impl(data, audio_data); + performance_counter_stop_plus(perf, ffmpeg_perf_push_audio); + return ret; +} + static bool encode_video(ffmpeg_t *handle, AVFrame *frame) { int ret; @@ -1427,12 +2084,12 @@ static void ffmpeg_scale_input(ffmpeg_t *handle, { unsigned src_w = VIDEO_SCALE_W(vid->dims); unsigned src_h = VIDEO_SCALE_H(vid->dims); - /* When output was padded to even dimensions, clamp the scaling - * destination to the source size. */ + /* Scale every frame to the output size, except one exactly a pixel + * short of it (the padding to even dimensions), which is copied 1:1. */ unsigned out_w = VIDEO_SCALE_W(handle->params.out_dims); unsigned out_h = VIDEO_SCALE_H(handle->params.out_dims); - unsigned dst_w = (src_w < out_w) ? src_w : out_w; - unsigned dst_h = (src_h < out_h) ? src_h : out_h; + unsigned dst_w = (src_w + 1 == out_w) ? src_w : out_w; + unsigned dst_h = (src_h + 1 == out_h) ? src_h : out_h; /* Attempt to preserve more information if we scale down. */ bool shrunk = dst_w < src_w || dst_h < src_h; @@ -1465,15 +2122,89 @@ static void ffmpeg_scale_input(ffmpeg_t *handle, shrunk); } +/* Rotate a packed frame by rot quarter turns counter-clockwise + * (libretro convention) from src (w x h, src_pitch bytes per row) into + * dst, tightly packed at the rotated width. */ +static void ffmpeg_rotate_frame(uint8_t *dst, const uint8_t *src, + unsigned w, unsigned h, int src_pitch, unsigned bpp, unsigned rot) +{ + unsigned x, y; + unsigned dw = (rot & 1) ? h : w; + + for (y = 0; y < h; y++) + { + const uint8_t *row = src + (ptrdiff_t)y * src_pitch; + for (x = 0; x < w; x++) + { + unsigned dx, dy; + switch (rot) + { + case 1: dx = y; dy = w - 1 - x; break; /* 90 CCW */ + case 2: dx = w - 1 - x; dy = h - 1 - y; break; /* 180 */ + default: dx = h - 1 - y; dy = x; break; /* 270 CCW */ + } + memcpy(dst + ((size_t)dy * dw + dx) * bpp, row + (size_t)x * bpp, + bpp); + } + } +} + static bool ffmpeg_push_video_thread(ffmpeg_t *handle, - const struct record_video_data *vid) + const struct record_video_data *vid, unsigned pts_skip) { + struct record_video_data rotated; + bool ok; + + if (handle->params.rotation && !vid->is_dupe && vid->data) + { + unsigned bpp = (unsigned)handle->video.pix_size; + unsigned w = VIDEO_SCALE_W(vid->dims); + unsigned h = VIDEO_SCALE_H(vid->dims); + if (!handle->video_rot_buf) + handle->video_rot_buf = (uint8_t*)av_malloc( + VIDEO_SCALE_AREA(handle->params.fb_dims) * bpp); + if ( handle->video_rot_buf + && (size_t)w * h <= VIDEO_SCALE_AREA(handle->params.fb_dims)) + { + unsigned rot = handle->params.rotation & 3; + ffmpeg_rotate_frame(handle->video_rot_buf, + (const uint8_t*)vid->data, w, h, vid->pitch, bpp, rot); + rotated = *vid; + rotated.data = handle->video_rot_buf; + rotated.dims = (rot & 1) ? VIDEO_SCALE_PACK(h, w) : vid->dims; + rotated.pitch = (int)(VIDEO_SCALE_W(rotated.dims) * bpp); + vid = &rotated; + } + } + if (!vid->is_dupe) + { ffmpeg_scale_input(handle, vid); + } + + /* Leave a gap for dropped frames so A/V stays aligned. */ + handle->video.frame_cnt += pts_skip; handle->video.conv_frame->pts = handle->video.frame_cnt; - if (!encode_video(handle, handle->video.conv_frame)) + /* gop_size counts frames, so pts gaps (drops, cores presenting + * every other tick) stretch it in time; force keyframes on time. */ + handle->video.conv_frame->pict_type = AV_PICTURE_TYPE_NONE; + if (handle->config.video_gop_seconds > 0.0f) + { + int64_t ticks = (int64_t)(handle->config.video_gop_seconds + * handle->params.fps / handle->video.frame_drop_ratio + 0.5); + if (ticks < 1) + ticks = 1; + if (handle->video.frame_cnt - handle->video_last_key_pts >= ticks) + { + handle->video.conv_frame->pict_type = AV_PICTURE_TYPE_I; + handle->video_last_key_pts = handle->video.frame_cnt; + } + } + + ok = encode_video(handle, handle->video.conv_frame); + if (!ok) return false; handle->video.frame_cnt++; @@ -1838,21 +2569,13 @@ static bool ffmpeg_push_audio_thread(ffmpeg_t *handle, return true; } -static void ffmpeg_flush_audio(ffmpeg_t *handle, void *audio_buf, - size_t audio_buf_size) +static void ffmpeg_flush_audio(ffmpeg_t *handle) { - size_t avail = retro_spsc_read_avail(&handle->audio_fifo); - - if (avail) + if ( handle->audio.frames_in_buffer + && encode_audio(handle, false)) { - struct record_audio_data aud = {0}; - - retro_spsc_read(&handle->audio_fifo, audio_buf, avail); - - aud.frames = avail / (sizeof(int16_t) * handle->params.channels); - aud.data = audio_buf; - - ffmpeg_push_audio_thread(handle, &aud, false); + handle->audio.frame_cnt += handle->audio.frames_in_buffer; + handle->audio.frames_in_buffer = 0; } encode_audio(handle, true); @@ -1875,7 +2598,7 @@ static void ffmpeg_flush_buffers(ffmpeg_t *handle) do { - struct record_video_data attr_buf; + struct ff_video_attr attr_buf; did_work = false; @@ -1906,19 +2629,48 @@ static void ffmpeg_flush_buffers(ffmpeg_t *handle) { retro_spsc_read(&handle->attr_fifo, &attr_buf, sizeof(attr_buf)); retro_spsc_read(&handle->video_fifo, video_buf, - VIDEO_SCALE_H(attr_buf.dims) * attr_buf.pitch); - attr_buf.data = video_buf; - ffmpeg_push_video_thread(handle, &attr_buf); + VIDEO_SCALE_H(attr_buf.vid.dims) * attr_buf.vid.pitch); + attr_buf.vid.data = video_buf; + ffmpeg_push_video_thread(handle, &attr_buf.vid, attr_buf.pts_skip); did_work = true; } }while (did_work); - /* Flush out last audio. Skip on OOM - audio_buf is the - * destination for ffmpeg_flush_audio's internal fifo_read - * (via ffmpeg_push_audio_thread) and NULL would NULL-deref. */ + /* The real audio left in the queue was queued ahead of any silence + * still owed: it goes first, held in the encoder's frame until the + * silence completes it. */ + if (handle->config.audio_enable && audio_buf) + { + size_t avail = retro_spsc_read_avail(&handle->audio_fifo); + if (avail) + { + struct record_audio_data aud = {0}; + retro_spsc_read(&handle->audio_fifo, audio_buf, avail); + aud.frames = avail / (sizeof(int16_t) * handle->params.channels); + aud.data = audio_buf; + ffmpeg_push_audio_thread(handle, &aud, true); + } + } + + /* Pay back silence still owed, so audio runs as long as video. */ if (handle->config.audio_enable && audio_buf) - ffmpeg_flush_audio(handle, audio_buf, audio_buf_size); + { + memset(audio_buf, 0, audio_buf_size); + while (handle->audio_silence_pending >= audio_buf_size) + { + struct record_audio_data aud = {0}; + aud.frames = handle->audio.codec->frame_size; + aud.data = audio_buf; + ffmpeg_push_audio_thread(handle, &aud, true); + handle->audio_silence_pending -= audio_buf_size; + } + handle->audio_silence_pending = 0; + } + + /* Flush out last audio: a short final frame, then the encoder. */ + if (handle->config.audio_enable && audio_buf) + ffmpeg_flush_audio(handle); /* Flush out last video. */ encode_video(handle, NULL); @@ -1943,7 +2695,7 @@ static bool ffmpeg_finalize(void *data) /* Write final data. */ av_write_trailer(handle->muxer.ctx); - avio_close(ctx->pb); + avio_closep(&handle->muxer.ctx->pb); return true; } @@ -1959,9 +2711,15 @@ static void ffmpeg_thread(void *data) (ff->audio.codec->frame_size * ff->params.channels * sizeof(int16_t)) : 0; void *audio_buf = audio_buf_size ? av_malloc(audio_buf_size) : NULL; +#ifdef FFMPEG_HAVE_THREAD_NICE + /* Also covers threads an encoder spawns lazily from here. */ + if (ff->allow_frame_drop) + ffmpeg_lower_this_thread(); +#endif + while (retro_atomic_load_acquire_int(&ff->alive)) { - struct record_video_data attr_buf; + struct ff_video_attr attr_buf; bool avail_video = false; bool avail_audio = false; @@ -1990,11 +2748,11 @@ static void ffmpeg_thread(void *data) { retro_spsc_read(&ff->attr_fifo, &attr_buf, sizeof(attr_buf)); retro_spsc_read(&ff->video_fifo, video_buf, - VIDEO_SCALE_H(attr_buf.dims) * attr_buf.pitch); + VIDEO_SCALE_H(attr_buf.vid.dims) * attr_buf.vid.pitch); retro_eventcount_notify(&ff->space); - attr_buf.data = video_buf; - ffmpeg_push_video_thread(ff, &attr_buf); + attr_buf.vid.data = video_buf; + ffmpeg_push_video_thread(ff, &attr_buf.vid, attr_buf.pts_skip); } if (avail_audio && audio_buf) diff --git a/record/record_driver.c b/record/record_driver.c index f861e57cbb14..e06633c425c9 100644 --- a/record/record_driver.c +++ b/record/record_driver.c @@ -300,6 +300,9 @@ bool recording_init(void) params.video_stream_scale_factor = settings->uints.video_stream_scale_factor; params.video_record_threads = settings->uints.video_record_threads; params.streaming_mode = settings->uints.streaming_mode; + params.allow_frame_drop = settings->bools.video_record_allow_frame_drop; + params.video_fifo_frames = settings->uints.video_record_fifo_frames; + params.hw_encoder = settings->bools.video_record_hw_encoder; params.out_dims = VIDEO_SCALE_PACK( av_info->geometry.base_width, av_info->geometry.base_height); @@ -374,9 +377,9 @@ bool recording_init(void) } params.out_dims = vp.dims; - params.fb_dims = VIDEO_SCALE_PACK( - next_pow2(VIDEO_SCALE_W(vp.dims)), - next_pow2(VIDEO_SCALE_H(vp.dims))); + /* Every frame is exactly the viewport (a resize ends the + * recording), so size the encoder's queue to it. */ + params.fb_dims = vp.dims; if (video_force_aspect && (VIDEO_DRIVER_ASPECT_RATIO(video_st) > 0.0f)) @@ -384,27 +387,43 @@ bool recording_init(void) else params.aspect_ratio = (float)VIDEO_SCALE_W(vp.dims) / VIDEO_SCALE_H(vp.dims); - params.pix_fmt = FFEMU_PIX_BGR24; + /* The driver's 32-bit readback skips a per-frame BGR24 + * conversion; FFEMU_PIX_ARGB8888 is a native-endian word, so B,G,R,X + * bytes only on little-endian hosts. The threaded wrapper reads back + * on its own thread and hands over BGR24. */ +#ifdef MSB_FIRST + recording_st->gpu_bgrx = false; +#else + recording_st->gpu_bgrx = + !video_st->thread_wrapper_active + && video_st->current_video->read_viewport_bgrx; +#endif + params.pix_fmt = recording_st->gpu_bgrx + ? FFEMU_PIX_ARGB8888 : FFEMU_PIX_BGR24; recording_st->gpu_dims = vp.dims; - RARCH_LOG("[Recording] %s %ux%u.\n", msg_hash_to_str(MSG_DETECTED_VIEWPORT_OF), - VIDEO_SCALE_W(vp.dims), VIDEO_SCALE_H(vp.dims)); + RARCH_LOG("[Recording] %s %ux%u (%s readback).\n", + msg_hash_to_str(MSG_DETECTED_VIEWPORT_OF), + VIDEO_SCALE_W(vp.dims), VIDEO_SCALE_H(vp.dims), + recording_st->gpu_bgrx ? "BGRX" : "BGR24"); - gpu_size = VIDEO_SCALE_AREA(vp.dims) * 3; + gpu_size = VIDEO_SCALE_AREA(vp.dims) * (recording_st->gpu_bgrx ? 4 : 3); if (!(video_st->record_gpu_buffer = (uint8_t*)malloc(gpu_size))) return false; } else { + recording_st->gpu_bgrx = false; if (recording_state.out_dims) params.out_dims = recording_state.out_dims; - if (video_force_aspect && - (VIDEO_DRIVER_ASPECT_RATIO(video_st) > 0.0f)) - params.aspect_ratio = VIDEO_DRIVER_ASPECT_RATIO(video_st); - else - params.aspect_ratio = (float)VIDEO_SCALE_W(params.out_dims) - / VIDEO_SCALE_H(params.out_dims); + /* Raw core frames are unrotated; the GPU path reads back the + * already-rotated screen. Rotate them like the display does. */ + params.rotation = retroarch_get_rotation() % 4; + record_raw_geometry(¶ms.out_dims, ¶ms.aspect_ratio, + params.rotation, + video_force_aspect ? VIDEO_DRIVER_ASPECT_RATIO(video_st) : 0.0f, + recording_state.out_dims != 0); #ifdef HAVE_VIDEO_FILTER if (settings->bools.video_post_filter_record diff --git a/record/record_driver.h b/record/record_driver.h index 69f3a9d93fe8..187c24713732 100644 --- a/record/record_driver.h +++ b/record/record_driver.h @@ -4,6 +4,8 @@ #include #include +#include + #include "../gfx/video_defines.h" /* VIDEO_SCALE_PACK */ enum ffemu_pix_format @@ -70,6 +72,12 @@ struct record_params unsigned video_stream_scale_factor; unsigned video_record_threads; unsigned streaming_mode; + /* Video queue depth in frames (driver clamps/defaults out-of-range). */ + unsigned video_fifo_frames; + /* Quarter turns counter-clockwise to rotate incoming frames by. + * out_dims and aspect_ratio describe the rotated output, fb_dims + * the incoming frames. */ + unsigned rotation; /* Aspect ratio of input video. Parameters are passed to the muxer, * the video itself is not scaled. @@ -82,6 +90,10 @@ struct record_params enum ffemu_pix_format pix_fmt; bool video_gpu_record; + /* Drop frames when the encoder queue is full instead of blocking. */ + bool allow_frame_drop; + /* Prefer a hardware H.264 encoder for the built-in presets. */ + bool hw_encoder; }; struct record_video_data @@ -112,6 +124,23 @@ typedef struct record_driver } record_driver_t; +/* A raw recording's output size and display aspect when the display + * shows the core's frames turned @rotation quarter turns. @out_dims + * holds the core's size, turned here, unless it is --size's + * (@user_dims), which is kept as given. @display_aspect is the + * viewport's, which is already the turned picture's; 0 lets the + * output size decide. */ +static INLINE void record_raw_geometry(unsigned *out_dims, float *aspect, + unsigned rotation, float display_aspect, bool user_dims) +{ + if ((rotation & 1) && !user_dims) + *out_dims = VIDEO_SCALE_PACK(VIDEO_SCALE_H(*out_dims), + VIDEO_SCALE_W(*out_dims)); + *aspect = (display_aspect > 0.0f) + ? display_aspect + : (float)VIDEO_SCALE_W(*out_dims) / VIDEO_SCALE_H(*out_dims); +} + struct recording { const record_driver_t *driver; @@ -121,6 +150,8 @@ struct recording * word - VIDEO_SCALE_PACK's layout, so a resize is one comparison * against a freshly packed viewport rather than two. */ unsigned gpu_dims; + /* GPU recording reads through read_viewport_bgrx. */ + bool gpu_bgrx; /* --size's override of the recording's output size, same layout; * zero when it was not given, which is one test instead of two. */ diff --git a/samples/gfx/threaded_video/threaded_video_test.c b/samples/gfx/threaded_video/threaded_video_test.c index 4ea79c8f53d6..51b48e46c374 100644 --- a/samples/gfx/threaded_video/threaded_video_test.c +++ b/samples/gfx/threaded_video/threaded_video_test.c @@ -41,6 +41,7 @@ #include "../../../gfx/video_driver.h" #include "../../../gfx/gfx_instrument.h" #include "../../../gfx/video_thread_wrapper.h" +#include "../../../record/record_driver.h" #include "../../../gfx/font_driver.h" #include "../../../menu/menu_driver.h" #include "../../../menu/menu_setting.h" @@ -6549,6 +6550,134 @@ static void readback_once(bool threaded) run_frames(4); } +/* ------------------------------------------------------------------ */ +/* Lane: a GPU recording's frames keep the format it was opened for */ +/* A GPU recording opened unthreaded on a driver with the 32-bit */ +/* readback takes B,G,R,X frames. Threaded Video switched on during */ +/* it puts the wrapper's readback in charge, which hands over BGR24 */ +/* from a buffer of its own: those frames were pushed as 32-bit, */ +/* reading a third more than the wrapper's buffer holds and showing */ +/* garbage. The recorder must get 32-bit rows of its own buffer */ +/* either way, every row of them readable. */ +/* ------------------------------------------------------------------ */ + +static unsigned reclane_pushes; +static int reclane_pitch; +static const uint8_t *reclane_data; +static unsigned reclane_rows; +static unsigned reclane_sum; + +static bool reclane_push_video(void *data, + const struct record_video_data *v) +{ + unsigned y; + size_t row; + (void)data; + if (v->is_dupe || !v->data) + return true; + reclane_pushes++; + reclane_pitch = v->pitch; + reclane_data = (const uint8_t*)v->data; + /* Each row's last byte, as the encoder's copy reads every row */ + row = (size_t)(v->pitch < 0 ? -v->pitch : v->pitch); + for (y = 0; y < reclane_rows; y++) + reclane_sum += ((const uint8_t*)v->data)[(ptrdiff_t)y * v->pitch + + (ptrdiff_t)row - 1]; + return true; +} + +static bool reclane_push_audio(void *data, + const struct record_audio_data *a) +{ + (void)data; + (void)a; + return true; +} + +static const record_driver_t reclane_driver = { + NULL, NULL, reclane_push_video, reclane_push_audio, NULL, "reclane" +}; + +static void record_format_once(bool threaded) +{ + struct video_viewport vp; + video_driver_state_t *video_st = video_state_get_ptr(); + recording_state_t *rec_st = recording_state_get_ptr(); + const char *mode = threaded ? "threaded" : "unthreaded"; + recording_state_t saved; + uint8_t *saved_buf; + uint8_t *buf; + size_t area; + unsigned i; + + set_threaded_via_setting(threaded); + run_frames(4); + expect_wrapper(threaded, "record format lane"); + /* Frames the core makes are recorded; the menu's dupes are not */ + if (menu_is_up()) + command_event(CMD_EVENT_MENU_TOGGLE, NULL); + CHECK(!menu_is_up(), "record format lane (%s): menu still up", mode); + run_frames(2); + if (threaded) + video_thread_wait_idle(); + + memset(&vp, 0, sizeof(vp)); + video_driver_get_viewport_info(&vp); + area = VIDEO_SCALE_AREA(vp.dims); + CHECK(area > 0, "record format lane (%s): no viewport", mode); + if (!area || !(buf = (uint8_t*)malloc(area * 4))) + return; + + /* A recording opened for 32-bit frames, as on this driver unthreaded */ + saved = *rec_st; + saved_buf = video_st->record_gpu_buffer; + reclane_pushes = 0; + reclane_rows = VIDEO_SCALE_H(vp.dims); + rec_st->driver = &reclane_driver; + rec_st->gpu_dims = vp.dims; + rec_st->gpu_bgrx = true; + rec_st->enable = true; + video_st->record_gpu_buffer = buf; + rec_st->data = (void*)&reclane_driver; + + /* The wrapper's readback starts a few frames behind */ + for (i = 0; i < 32 && !reclane_pushes; i++) + run_frames(1); + + rec_st->data = NULL; + if (threaded) + video_thread_wait_idle(); + *rec_st = saved; + video_st->record_gpu_buffer = saved_buf; + + CHECK(reclane_pushes > 0, "record format lane (%s): no frame recorded", + mode); + if (reclane_pushes) + { + CHECK(reclane_pitch == (int)VIDEO_SCALE_W(vp.dims) * 4 + || reclane_pitch == -(int)VIDEO_SCALE_W(vp.dims) * 4, + "record format lane (%s): rows of %d bytes for a %u wide " + "32-bit recording", mode, reclane_pitch, VIDEO_SCALE_W(vp.dims)); + CHECK(reclane_data >= buf && reclane_data < buf + area * 4, + "record format lane (%s): the frame is not in the recording's " + "own buffer", mode); + } + free(buf); +} + +static void lane_record_format(void) +{ + unsigned had = failures; + + record_format_once(false); + record_format_once(true); + set_threaded_via_setting(false); + run_frames(2); + + if (failures == had) + fprintf(stderr, "[pass] gpu record format lane\n"); +} + static void lane_gpu_readback(void) { unsigned had = failures; @@ -7241,6 +7370,8 @@ int main(int argc, char *argv[]) lane_x11_wsi_connection(); if (real_driver() && !strcmp(getenv("HARNESS_VIDEO_DRIVER"), "vulkan")) lane_gpu_readback(); + if (real_driver() && !strcmp(getenv("HARNESS_VIDEO_DRIVER"), "vulkan")) + lane_record_format(); if (real_driver()) lane_x11_grabbed_mouse(); if ( real_driver() diff --git a/samples/record/ffmpeg_pipeline/ffmpeg_pipeline_test.c b/samples/record/ffmpeg_pipeline/ffmpeg_pipeline_test.c index 449c3040dc09..82922dc26463 100644 --- a/samples/record/ffmpeg_pipeline/ffmpeg_pipeline_test.c +++ b/samples/record/ffmpeg_pipeline/ffmpeg_pipeline_test.c @@ -10,12 +10,25 @@ * (frames are pushed as fast as they are taken), the blocking wait * when the queues are full, and the drain after the encoder thread * is joined. A frame out of order, torn, or from the wrong slot - * fails the pixel check. */ + * fails the pixel check. + * + * A second pass runs in drop mode (allow_frame_drop, the smallest + * video queue): frames the encoder can't take in time are dropped, + * so it checks the survivors come out in order, pixel-exact, at the + * timestamp of the frame they were pushed as, that frames were in + * fact dropped, and that the frontend was told nothing while it + * recorded: no log line and no on-screen message from the pushes. + * On Linux, drop mode also has the encoder's own threads run at a + * lower priority than the thread that opened the recording. + * + * A geometry lane checks the output size and display aspect of a raw + * recording of a frame the display turns a quarter. */ #include #include #include #include +#include #include "../../../record/record_driver.h" #include "../../../record/drivers/record_ffmpeg.h" @@ -23,6 +36,17 @@ #include #include +#if defined(__linux__) +#include +#include +#include +#include +#include +#endif + +extern unsigned stub_warn_calls; +extern unsigned stub_msg_calls; + #define W 160 #define H 120 #define FRAMES 240 @@ -83,7 +107,53 @@ static int check_frame(const AVFrame *f, unsigned n) return 1; } -int main(void) +/* Which frame this is, from its top-left pixel. */ +static int frame_number(const AVFrame *f) +{ + unsigned n; + for (n = 0; n < FRAMES; n++) + { + uint32_t want = ((n * 37u) & 0xffu) << 16 + | ((n * 91u) & 0xffu) << 8 | ((n * 13u) & 0xffu); + uint32_t px; + if (f->format == AV_PIX_FMT_GBRP) + px = (f->data[2][0] << 16) | (f->data[0][0] << 8) | f->data[1][0]; + else + px = (f->data[0][2] << 16) | (f->data[0][1] << 8) | f->data[0][0]; + if (px == want) + return (int)n; + } + return -1; +} + +#if defined(__linux__) +/* Threads of this process running at a higher nice value than the + * calling thread. */ +static unsigned threads_lowered(void) +{ + struct dirent *ent; + unsigned n = 0; + int me = getpriority(PRIO_PROCESS, (id_t)syscall(SYS_gettid)); + DIR *dir = opendir("/proc/self/task"); + if (!dir) + return 0; + while ((ent = readdir(dir))) + { + long tid = strtol(ent->d_name, NULL, 10); + int nice; + if (tid <= 0) + continue; + errno = 0; + nice = getpriority(PRIO_PROCESS, (id_t)tid); + if (!errno && nice > me) + n++; + } + closedir(dir); + return n; +} +#endif + +static int run(bool drop) { struct record_params params; struct record_video_data vid; @@ -103,10 +173,13 @@ int main(void) params.channels = 2; params.video_record_scale_factor = 1; params.video_stream_scale_factor = 1; - params.video_record_threads = 1; + /* Drop mode with encoder threads of its own, to see them lowered */ + params.video_record_threads = drop ? 2 : 1; params.aspect_ratio = (float)W / H; params.preset = RECORD_CONFIG_TYPE_RECORDING_LOSSLESS_QUALITY; params.pix_fmt = FFEMU_PIX_ARGB8888; + params.allow_frame_drop = drop; + params.video_fifo_frames = drop ? 8 : 32; remove(OUT); rec = record_ffmpeg.init(¶ms); @@ -116,6 +189,16 @@ int main(void) return 1; } +#if defined(__linux__) + if (drop && !threads_lowered()) + { + fprintf(stderr, "drop mode: no encoder thread runs lowered\n"); + return 1; + } +#endif + + stub_warn_calls = 0; + stub_msg_calls = 0; memset(pcm, 0, sizeof(pcm)); for (n = 0; n < FRAMES; n++) { @@ -137,6 +220,12 @@ int main(void) return 1; } } + if (drop && (stub_warn_calls || stub_msg_calls)) + { + fprintf(stderr, "drop mode: %u warnings and %u on-screen messages" + " while recording\n", stub_warn_calls, stub_msg_calls); + return 1; + } record_ffmpeg.finalize(rec); record_ffmpeg.free(rec); free(fb); @@ -149,6 +238,7 @@ int main(void) AVPacket *pkt = av_packet_alloc(); AVFrame *frm = av_frame_alloc(); int vstream = -1, i; + int last = -1; unsigned got = 0; if (avformat_open_input(&fmt, OUT, NULL, NULL) < 0 @@ -188,7 +278,26 @@ int main(void) } while (avcodec_receive_frame(dec, frm) == 0) { - if (!check_frame(frm, got)) + int n = drop ? frame_number(frm) : (int)got; + if (drop) + { + /* In order, and at the pushed frame's own time. */ + double t = frm->best_effort_timestamp + * av_q2d(fmt->streams[vstream]->time_base); + if (n <= last) + { + fprintf(stderr, "frame %d after %d\n", n, last); + return 1; + } + if (t < n / FPS - 0.002 || t > n / FPS + 0.002) + { + fprintf(stderr, "frame %d at %.4f s, want %.4f s\n", + n, t, n / FPS); + return 1; + } + last = n; + } + if (n < 0 || !check_frame(frm, (unsigned)n)) return 1; got++; av_frame_unref(frm); @@ -196,7 +305,7 @@ int main(void) if (flushing) break; } - if (got != FRAMES) + if (drop ? (got == 0 || got >= FRAMES) : got != FRAMES) { fprintf(stderr, "decoded %u frames, pushed %u\n", got, FRAMES); return 1; @@ -205,9 +314,71 @@ int main(void) avformat_close_input(&fmt); av_packet_free(&pkt); av_frame_free(&frm); + if (drop) + printf("[pass] drop mode: %u of %u frames kept, in order, " + "pixel-exact, on time\n", got, FRAMES); + else + printf("[pass] %u frames through record_ffmpeg, all in order " + "and pixel-exact\n", FRAMES); } remove(OUT); - printf("[pass] %u frames through record_ffmpeg, all in order and pixel-exact\n", FRAMES); + return 0; +} + +/* A raw recording of frames the display turns a quarter: the output + * is turned, and its aspect is the display's, which is already the + * turned picture's - not that inverted again. */ +static int geometry(void) +{ + unsigned dims; + float aspect; + + /* 320x240 core frame, shown turned on a 3:4 display */ + dims = VIDEO_SCALE_PACK(320, 240); + record_raw_geometry(&dims, &aspect, 1, 0.75f, false); + if (dims != VIDEO_SCALE_PACK(240, 320) || aspect != 0.75f) + { + fprintf(stderr, "geometry: turned frame recorded %ux%u at %.3f," + " want 240x320 at 0.750\n", VIDEO_SCALE_W(dims), + VIDEO_SCALE_H(dims), aspect); + return 1; + } + /* No display aspect: the turned size decides */ + dims = VIDEO_SCALE_PACK(320, 240); + record_raw_geometry(&dims, &aspect, 3, 0.0f, false); + if (dims != VIDEO_SCALE_PACK(240, 320) || aspect != 0.75f) + { + fprintf(stderr, "geometry: turned size gives %ux%u at %.3f\n", + VIDEO_SCALE_W(dims), VIDEO_SCALE_H(dims), aspect); + return 1; + } + /* --size is kept as given, and its own shape is its aspect */ + dims = VIDEO_SCALE_PACK(640, 480); + record_raw_geometry(&dims, &aspect, 1, 0.0f, true); + if (dims != VIDEO_SCALE_PACK(640, 480) || aspect < 1.333f + || aspect > 1.334f) + { + fprintf(stderr, "geometry: --size became %ux%u at %.3f\n", + VIDEO_SCALE_W(dims), VIDEO_SCALE_H(dims), aspect); + return 1; + } + /* Unturned: as it was */ + dims = VIDEO_SCALE_PACK(320, 240); + record_raw_geometry(&dims, &aspect, 2, 1.25f, false); + if (dims != VIDEO_SCALE_PACK(320, 240) || aspect != 1.25f) + { + fprintf(stderr, "geometry: half turn gives %ux%u at %.3f\n", + VIDEO_SCALE_W(dims), VIDEO_SCALE_H(dims), aspect); + return 1; + } + printf("[pass] raw recording geometry under rotation\n"); + return 0; +} + +int main(void) +{ + if (geometry() || run(false) || run(true)) + return 1; printf("ALL OK\n"); return 0; } diff --git a/samples/record/ffmpeg_pipeline/stubs_retroarch.c b/samples/record/ffmpeg_pipeline/stubs_retroarch.c index 63b07e375ebb..91c83b377ea6 100644 --- a/samples/record/ffmpeg_pipeline/stubs_retroarch.c +++ b/samples/record/ffmpeg_pipeline/stubs_retroarch.c @@ -1,6 +1,10 @@ /* What record_ffmpeg.c reaches for in RetroArch, stubbed. */ #include #include +#include +#include +#include +#include "../../../runloop.h" void RARCH_LOG(const char *fmt, ...) { @@ -17,3 +21,51 @@ void RARCH_ERR(const char *fmt, ...) vfprintf(stderr, fmt, ap); va_end(ap); } + +/* What the frontend was told, counted: drop mode must say nothing + * while it records. */ +unsigned stub_warn_calls = 0; +unsigned stub_msg_calls = 0; + +void RARCH_WARN(const char *fmt, ...) +{ + va_list ap; + stub_warn_calls++; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); +} + +void runloop_msg_queue_push(const char *msg, size_t len, + unsigned prio, unsigned duration, bool flush, + char *title, enum message_queue_icon icon, + enum message_queue_category category) +{ + stub_msg_calls++; +} + +void RARCH_DBG(const char *fmt, ...) +{ +} + +bool verbosity_is_enabled(void) +{ + return false; +} + +unsigned verbosity_get_log_level(void) +{ + return 1; +} + +static runloop_state_t stub_runloop_state; + +runloop_state_t *runloop_state_get_ptr(void) +{ + return &stub_runloop_state; +} + +void rarch_perf_register(struct retro_perf_counter *perf) +{ + perf->registered = true; +} diff --git a/settings/settings_def_recording_video.h b/settings/settings_def_recording_video.h index b62c38f85dc1..8cec2e32c3e6 100644 --- a/settings/settings_def_recording_video.h +++ b/settings/settings_def_recording_video.h @@ -14,3 +14,23 @@ S_BOOL(video_gpu_record, VIDEO_GPU_RECORD, DEFAULT_GPU_RECORD, SD_FLAG_NONE, 0, 0, "Use GPU Recording", "Record output of GPU shaded material if available.") +S_BOOL(video_record_allow_frame_drop, VIDEO_RECORD_ALLOW_FRAME_DROP, + "video_record_allow_frame_drop", + DEFAULT_VIDEO_RECORD_ALLOW_FRAME_DROP, SD_FLAG_NONE, 0, 0, + "Drop Frames When Encoder Falls Behind", + "When the recording or streaming encoder can't keep up, skip video frames and fill audio gaps with silence instead of slowing down the game. Disable to keep every frame at the cost of stutter.") +S_BOOL(video_record_game_only, VIDEO_RECORD_GAME_ONLY, + "video_record_game_only", + DEFAULT_VIDEO_RECORD_GAME_ONLY, SD_FLAG_NONE, 0, 0, + "Record Game Only", + "Leave the menu, notifications, on-screen messages and overlays out of GPU recordings, and leave out the time spent in the menu. When off, recordings capture exactly what is on screen.") +S_BOOL(video_record_hw_encoder, VIDEO_RECORD_HW_ENCODER, + "video_record_hw_encoder", + DEFAULT_VIDEO_RECORD_HW_ENCODER, SD_FLAG_NONE, 0, 0, + "Use Hardware Video Encoder", + "Encode H.264 with a hardware encoder when one is available, falling back to software (libx264). Applies to the built-in quality presets, not to custom configs.") +S_UINT(video_record_fifo_frames, VIDEO_RECORD_FIFO_FRAMES, + "video_record_fifo_frames", + DEFAULT_VIDEO_RECORD_FIFO_FRAMES, SD_FLAG_LAKKA_ADVANCED, SDESC_RANGE_MINMAX, 0, MIN_VIDEO_RECORD_FIFO_FRAMES, MAX_VIDEO_RECORD_FIFO_FRAMES, 4, MIN_VIDEO_RECORD_FIFO_FRAMES, setting_action_ok_uint, NULL, + "Recording Video Buffer (Frames)", + "Frames queued between the game and the encoder. Larger values absorb encoder hiccups but use more memory. Applies to the next recording.") diff --git a/tools/settings_table.reference b/tools/settings_table.reference index 5470d8fbc838..86624af831c9 100644 --- a/tools/settings_table.reference +++ b/tools/settings_table.reference @@ -309,6 +309,9 @@ bool menu_show_reboot 1 1 bool menu_show_shutdown 1 1 bool video_post_filter_record 1 0 bool video_gpu_record 1 0 +bool video_record_allow_frame_drop 1 1 +bool video_record_game_only 1 0 +bool video_record_hw_encoder 1 0 bool menu_rgui_full_width_layout 1 1 bool video_shader_enable 1 1 bool quick_menu_show_save_core_overrides 1 1 @@ -601,6 +604,7 @@ uint audio_output_layout 1 0 uint microphone_block_frames 1 0 uint menu_thumbnail_preview_threads 1 1 uint menu_file_browser_extension_display 1 MENU_FILE_BROWSER_EXTENSION_DISPLAY_ALWAYS +uint video_record_fifo_frames 1 32 uint video_scale 1 3 uint video_window_opacity 1 100 uint video_stream_port 1 56400 @@ -999,6 +1003,9 @@ bool menu_show_reboot 1 1 bool menu_show_shutdown 1 1 bool video_post_filter_record 1 0 bool video_gpu_record 1 0 +bool video_record_allow_frame_drop 1 1 +bool video_record_game_only 1 0 +bool video_record_hw_encoder 1 0 bool menu_rgui_full_width_layout 1 1 bool video_shader_enable 1 1 bool quick_menu_show_save_core_overrides 1 1 @@ -1285,6 +1292,7 @@ uint audio_output_layout 1 0 uint microphone_block_frames 1 0 uint menu_thumbnail_preview_threads 1 1 uint menu_file_browser_extension_display 1 MENU_FILE_BROWSER_EXTENSION_DISPLAY_ALWAYS +uint video_record_fifo_frames 1 32 uint video_scale 1 3 uint video_window_opacity 1 100 uint video_stream_port 1 56400 @@ -1692,6 +1700,9 @@ bool menu_show_reboot 1 1 bool menu_show_shutdown 1 1 bool video_post_filter_record 1 0 bool video_gpu_record 1 0 +bool video_record_allow_frame_drop 1 1 +bool video_record_game_only 1 0 +bool video_record_hw_encoder 1 0 bool menu_rgui_full_width_layout 1 1 bool video_shader_enable 1 1 bool quick_menu_show_save_core_overrides 1 1 @@ -1994,6 +2005,7 @@ uint audio_output_layout 1 0 uint microphone_block_frames 1 0 uint menu_thumbnail_preview_threads 1 1 uint menu_file_browser_extension_display 1 MENU_FILE_BROWSER_EXTENSION_DISPLAY_ALWAYS +uint video_record_fifo_frames 1 32 uint video_scale 1 3 uint video_window_opacity 1 100 uint video_stream_port 1 56400 diff --git a/verbosity.c b/verbosity.c index 78d2ebc33627..ef2bc33d53db 100644 --- a/verbosity.c +++ b/verbosity.c @@ -149,6 +149,11 @@ void verbosity_set_log_level(unsigned level) verbosity_log_level = level; } +unsigned verbosity_get_log_level(void) +{ + return verbosity_log_level; +} + void verbosity_enable(void) { main_verbosity_st.verbosity = true; diff --git a/verbosity.h b/verbosity.h index 6bacb5f93bfe..ea6dbdc70fbb 100644 --- a/verbosity.h +++ b/verbosity.h @@ -47,6 +47,8 @@ void verbosity_disable(void); void verbosity_set_log_level(unsigned level); +unsigned verbosity_get_log_level(void); + bool *verbosity_get_ptr(void); void retro_main_log_file_deinit(void);