mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-07 08:36:02 -10:00
strlcpy() begins with strlen() of its source. It lives in another translation unit, so with no LTO that scan happens at runtime even when the source is a literal whose length the compiler already measured, and the call itself cannot be folded away either. strlcpy_lit() passes the length over as sizeof(). When the destination size is also a constant -- sizeof(buf), the usual case -- nothing is left but the stores memcpy() compiles to: strlcpy "core_options" sizeof(buf) 7.01 ns strlcpy_lit "core_options" sizeof(buf) 0.42 ns strlcpy 41-char literal sizeof(buf) 7.74 ns strlcpy_lit 41-char literal sizeof(buf) 0.71 ns strlcpy "core_options" runtime len 7.11 ns strlcpy_lit "core_options" runtime len 0.70 ns The destination bound is kept. When the literal does not fit it defers to strlcpy(), so the copy truncates as before rather than overrunning; a form that took only the literal would have turned every one of these sites into an overflow waiting for a small buffer. The literal is concatenated with "" so a non-literal source is a compile error rather than a sizeof() that silently measures a pointer. 818 sites across 104 files were converted by parsing arguments rather than by pattern-matching text: only a three-argument call whose second argument is a bare string literal is rewritten, so a macro that happens to expand to a literal is left alone. Line endings are preserved, so the CRLF files carry only their own changed lines. test/compat/test_strl.c compares the two against each other over every destination size from 0 past the literal's length, for literals that are empty, single-character, path-shaped, adjacently concatenated and escape-bearing, checking the return, the terminator and the whole buffer so an overrun past the terminator is caught. ASan+UBSan+LSan and TSan both 4/4. The full menu_displaylist_build_list walk over every displaylist type is byte-identical to before the change (md5 a2a081bcac09cdc8d72d67efd14ac40e).
1057 lines
33 KiB
C
1057 lines
33 KiB
C
/* RetroArch - A frontend for libretro.
|
|
* Copyright (C) 2010-2014 - Hans-Kristian Arntzen
|
|
* Copyright (C) 2011-2017 - Daniel De Matteis
|
|
* Copyright (C) 2016-2019 - Brad Parker
|
|
*
|
|
* RetroArch is free software: you can redistribute it and/or modify it under the terms
|
|
* of the GNU General Public License as published by the Free Software Found-
|
|
* ation, either version 3 of the License, or (at your option) any later version.
|
|
*
|
|
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
|
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
|
* PURPOSE. See the GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with RetroArch.
|
|
* If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <stdint.h>
|
|
#include <stddef.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#include <boolean.h>
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "../config.h"
|
|
#endif
|
|
|
|
#include <file/file_path.h>
|
|
#include <compat/strl.h>
|
|
#include <gfx/video_frame.h>
|
|
#include <streams/file_stream.h>
|
|
#include <streams/interface_stream.h>
|
|
|
|
#ifdef HAVE_RBMP
|
|
#include <formats/rbmp.h>
|
|
#endif
|
|
|
|
#ifdef HAVE_RPNG
|
|
#include <formats/rpng.h>
|
|
#define IMG_EXT "png"
|
|
#else
|
|
#define IMG_EXT "bmp"
|
|
#endif
|
|
|
|
#if defined(HAVE_GFX_WIDGETS)
|
|
#include "../gfx/gfx_widgets.h"
|
|
#endif
|
|
|
|
#include "../defaults.h"
|
|
#include "../command.h"
|
|
#include "../configuration.h"
|
|
#include "../core.h"
|
|
#include "../gfx/video_driver.h"
|
|
#include "../paths.h"
|
|
#include "../msg_hash.h"
|
|
#include "../runloop.h"
|
|
|
|
#include "tasks_internal.h"
|
|
|
|
enum screenshot_task_flags
|
|
{
|
|
SS_TASK_FLAG_BGR24 = (1 << 0),
|
|
SS_TASK_FLAG_SILENCE = (1 << 1),
|
|
SS_TASK_FLAG_IS_IDLE = (1 << 2),
|
|
SS_TASK_FLAG_IS_PAUSED = (1 << 3),
|
|
SS_TASK_FLAG_HISTORY_LIST_ENABLE = (1 << 4),
|
|
SS_TASK_FLAG_WIDGETS_READY = (1 << 5),
|
|
SS_TASK_FLAG_HDR = (1 << 6)
|
|
};
|
|
|
|
typedef struct screenshot_task_state screenshot_task_state_t;
|
|
|
|
struct screenshot_task_state
|
|
{
|
|
struct scaler_ctx scaler;
|
|
uint8_t *out_buffer;
|
|
const void *frame;
|
|
void *userbuf;
|
|
|
|
int pitch;
|
|
unsigned width;
|
|
unsigned height;
|
|
unsigned out_width;
|
|
unsigned out_height;
|
|
unsigned pixel_format_type;
|
|
|
|
uint8_t flags;
|
|
|
|
char filename[PATH_MAX_LENGTH];
|
|
char shotname[NAME_MAX_LENGTH];
|
|
/* Colour-space metadata for an HDR screenshot (SS_TASK_FLAG_HDR). The
|
|
* frame buffer then holds three uint16_t per pixel (48-bit RGB). */
|
|
struct rpng_hdr_metadata hdr;
|
|
};
|
|
|
|
/* The image encoders are pure (bytes in -> bytes out); this task owns
|
|
* all file I/O at the edge. The preferred shape is encode-to-memory
|
|
* followed by one open/bulk-write/close (filestream_write_file): the
|
|
* compressor is never interleaved with many small VFS writes, the file
|
|
* handle is held for the shortest possible time, and slow media (SD
|
|
* cards, console filesystems) see a single large sequential write.
|
|
* When the in-memory encode cannot allocate (very large frames on
|
|
* memory-constrained platforms), we fall back to streaming the encode
|
|
* straight into the file, which needs only a few rows of scratch. */
|
|
|
|
#if defined(HAVE_RPNG)
|
|
static bool screenshot_save_png(const char *path, const uint8_t *data,
|
|
unsigned width, unsigned height, signed pitch,
|
|
enum rpng_pixfmt fmt, const struct rpng_hdr_metadata *hdr)
|
|
{
|
|
bool ret;
|
|
intfstream_t *intf_s = intfstream_open_file(path,
|
|
RETRO_VFS_FILE_ACCESS_WRITE,
|
|
RETRO_VFS_FILE_ACCESS_HINT_NONE);
|
|
|
|
/* Stream the encode straight into the file. The VFS gives every
|
|
* file a 64 KiB stdio buffer, so the encoder's 16 KiB IDAT chunks
|
|
* already coalesce into large writes; measured at 4K, buffering the
|
|
* whole compressed image first and writing it in one call is
|
|
* indistinguishable in wall time (deflate dominates by two orders
|
|
* of magnitude) while costing a raw-frame-sized allocation that
|
|
* no-overcommit platforms would have to commit up front. Peak
|
|
* scratch this way is a handful of rows plus the deflate window. */
|
|
if (!intf_s)
|
|
return false;
|
|
|
|
ret = rpng_save_image_stream_fmt(data, intf_s,
|
|
width, height, pitch, fmt, hdr);
|
|
|
|
intfstream_close(intf_s);
|
|
free(intf_s);
|
|
return ret;
|
|
}
|
|
#elif defined(HAVE_RBMP)
|
|
static bool screenshot_save_bmp(const char *path, const void *frame,
|
|
unsigned width, unsigned height, unsigned pitch,
|
|
enum rbmp_source_type type)
|
|
{
|
|
bool ret;
|
|
intfstream_t *intf_s = intfstream_open_file(path,
|
|
RETRO_VFS_FILE_ACCESS_WRITE,
|
|
RETRO_VFS_FILE_ACCESS_HINT_NONE);
|
|
|
|
/* BMP has no compression - the file is the raw pixel block - so
|
|
* buffering the whole image first would only add a full-frame
|
|
* allocation and an extra copy of every byte. The stream encoder
|
|
* writes the pixel block in a single call straight from the source
|
|
* when the stride matches the padded row size (the common case),
|
|
* and per-row through the VFS's 64 KiB stdio buffer otherwise. */
|
|
if (!intf_s)
|
|
return false;
|
|
|
|
ret = rbmp_save_image_stream(intf_s, frame,
|
|
width, height, pitch, type);
|
|
|
|
intfstream_close(intf_s);
|
|
free(intf_s);
|
|
return ret;
|
|
}
|
|
#endif
|
|
|
|
static bool screenshot_dump_direct(screenshot_task_state_t *state)
|
|
{
|
|
struct scaler_ctx *scaler = (struct scaler_ctx*)&state->scaler;
|
|
bool ret = false;
|
|
|
|
#if defined(HAVE_RPNG)
|
|
const uint8_t* input = (const uint8_t*)state->frame + ((int)state->height - 1) * state->pitch;
|
|
|
|
if (!input)
|
|
return ret;
|
|
|
|
/* HDR screenshot: the frame is 48-bit RGB (three uint16_t per pixel),
|
|
* bottom-up, and carries colour-space metadata. Encode a 16-bit PNG
|
|
* tagged with the HDR chunks, using the same negative-pitch top-down
|
|
* trick as the BGR24 fast path. Never resampled. */
|
|
if (state->flags & SS_TASK_FLAG_HDR)
|
|
{
|
|
ret = screenshot_save_png(
|
|
state->filename,
|
|
input,
|
|
state->out_width,
|
|
state->out_height,
|
|
-state->pitch,
|
|
RPNG_PIXFMT_RGB48,
|
|
&state->hdr);
|
|
if (state->out_buffer)
|
|
free(state->out_buffer);
|
|
return ret;
|
|
}
|
|
|
|
/* Fast path: source is already BGR24 and no resampling is
|
|
* needed, so hand the source buffer directly to the PNG
|
|
* encoder. rpng_save_image_stream walks rows via `data +=
|
|
* pitch` with a signed pitch, so a bottom-up source is
|
|
* encoded top-down for free by starting at the last row and
|
|
* passing a negative row stride (same trick take_screenshot_raw
|
|
* uses via screenshot_dump's pitch argument).
|
|
*
|
|
* This avoids allocating a second full-frame BGR24 buffer and
|
|
* the flip-and-copy the scaler would otherwise do between them;
|
|
* at 4K that is ~48 MiB of allocation and copy per screenshot. */
|
|
if ( (state->flags & SS_TASK_FLAG_BGR24)
|
|
&& state->out_width == state->width
|
|
&& state->out_height == state->height)
|
|
{
|
|
ret = screenshot_save_png(
|
|
state->filename,
|
|
input,
|
|
state->out_width,
|
|
state->out_height,
|
|
-state->pitch,
|
|
RPNG_PIXFMT_BGR24,
|
|
NULL);
|
|
/* state->out_buffer is NULL in this path (see screenshot_dump);
|
|
* nothing to free. */
|
|
return ret;
|
|
}
|
|
|
|
/* Same idea for the raw-framebuffer formats: when no resampling is
|
|
* needed, feed the core's XRGB8888/RGB565 rows straight to the
|
|
* encoder (which converts per row) instead of converting the whole
|
|
* frame into a BGR24 buffer first - that is a width*height*3
|
|
* allocation plus a full extra pass over every pixel (~24 MiB at
|
|
* 4K), for output that is pixel-identical. */
|
|
if ( !(state->flags & SS_TASK_FLAG_BGR24)
|
|
&& state->out_width == state->width
|
|
&& state->out_height == state->height)
|
|
{
|
|
ret = screenshot_save_png(
|
|
state->filename,
|
|
input,
|
|
state->out_width,
|
|
state->out_height,
|
|
-state->pitch,
|
|
(state->pixel_format_type == RETRO_PIXEL_FORMAT_XRGB8888)
|
|
? RPNG_PIXFMT_XRGB8888
|
|
: RPNG_PIXFMT_RGB565,
|
|
NULL);
|
|
/* state->out_buffer is NULL in this path (see screenshot_dump). */
|
|
return ret;
|
|
}
|
|
|
|
if (state->flags & SS_TASK_FLAG_BGR24)
|
|
scaler->in_fmt = SCALER_FMT_BGR24;
|
|
else if (state->pixel_format_type == RETRO_PIXEL_FORMAT_XRGB8888)
|
|
scaler->in_fmt = SCALER_FMT_ARGB8888;
|
|
else
|
|
scaler->in_fmt = SCALER_FMT_RGB565;
|
|
|
|
video_frame_convert_to_bgr24(
|
|
scaler,
|
|
state->out_buffer,
|
|
input,
|
|
state->width,
|
|
state->height,
|
|
-state->pitch,
|
|
state->out_width,
|
|
state->out_height,
|
|
state->out_width * 3
|
|
);
|
|
|
|
scaler_ctx_gen_reset(&state->scaler);
|
|
|
|
ret = screenshot_save_png(
|
|
state->filename,
|
|
state->out_buffer,
|
|
state->out_width,
|
|
state->out_height,
|
|
(signed)(state->out_width * 3),
|
|
RPNG_PIXFMT_BGR24,
|
|
NULL);
|
|
|
|
free(state->out_buffer);
|
|
#elif defined(HAVE_RBMP)
|
|
{
|
|
enum rbmp_source_type bmp_type = RBMP_SOURCE_TYPE_DONT_CARE;
|
|
if (state->flags & SS_TASK_FLAG_BGR24)
|
|
bmp_type = RBMP_SOURCE_TYPE_BGR24;
|
|
else if (state->pixel_format_type == RETRO_PIXEL_FORMAT_XRGB8888)
|
|
bmp_type = RBMP_SOURCE_TYPE_XRGB888;
|
|
|
|
ret = screenshot_save_bmp(state->filename,
|
|
state->frame,
|
|
state->width,
|
|
state->height,
|
|
state->pitch,
|
|
bmp_type);
|
|
}
|
|
#endif
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* task_screenshot_handler:
|
|
* @task : the task being worked on
|
|
*
|
|
* Saves a screenshot to disk.
|
|
**/
|
|
static void task_screenshot_handler(retro_task_t *task)
|
|
{
|
|
uint8_t flg;
|
|
screenshot_task_state_t *state = NULL;
|
|
bool ret = false;
|
|
|
|
if (!task)
|
|
return;
|
|
|
|
if (!(state = (screenshot_task_state_t*)task->state))
|
|
goto task_finished;
|
|
|
|
flg = task_get_flags(task);
|
|
|
|
if ((flg & RETRO_TASK_FLG_CANCELLED) > 0)
|
|
goto task_finished;
|
|
if (task_get_progress(task) == 100)
|
|
goto task_finished;
|
|
|
|
/* Take screenshot */
|
|
ret = screenshot_dump_direct(state);
|
|
|
|
/* Push screenshot to image history playlist */
|
|
#ifdef HAVE_IMAGEVIEWER
|
|
if ( ret
|
|
&& !(state->flags & SS_TASK_FLAG_SILENCE)
|
|
&& (state->flags & SS_TASK_FLAG_HISTORY_LIST_ENABLE)
|
|
)
|
|
{
|
|
struct playlist_entry entry = {0};
|
|
|
|
/* the push function reads our entry as const, so these casts are safe */
|
|
entry.path = state->filename;
|
|
entry.core_path = (char*)"builtin";
|
|
entry.core_name = (char*)"imageviewer";
|
|
|
|
command_playlist_push_write(g_defaults.image_history, &entry);
|
|
}
|
|
#endif
|
|
|
|
task_set_progress(task, 100);
|
|
|
|
/* Report any errors */
|
|
if (!ret)
|
|
{
|
|
const char *_msg = msg_hash_to_str(MSG_FAILED_TO_TAKE_SCREENSHOT);
|
|
runloop_msg_queue_push(_msg, strlen(_msg), 1,
|
|
(state->flags & SS_TASK_FLAG_IS_PAUSED) ? 1 : 180, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR);
|
|
|
|
#if defined(HAVE_GFX_WIDGETS)
|
|
/* Do not show empty widget success on error */
|
|
if (state)
|
|
state->flags |= SS_TASK_FLAG_SILENCE;
|
|
#endif
|
|
}
|
|
|
|
if (task->title)
|
|
task_free_title(task);
|
|
|
|
return;
|
|
|
|
task_finished:
|
|
if (task)
|
|
task_set_flags(task, RETRO_TASK_FLG_FINISHED, true);
|
|
|
|
if (task->title)
|
|
task_free_title(task);
|
|
|
|
if (state && state->userbuf)
|
|
free(state->userbuf);
|
|
|
|
#if defined(HAVE_GFX_WIDGETS)
|
|
/* If display widgets are enabled, state is freed
|
|
in the callback after the notification
|
|
is displayed */
|
|
if (state && !(state->flags & SS_TASK_FLAG_WIDGETS_READY))
|
|
#endif
|
|
{
|
|
free(state);
|
|
/* Must explicitly set task->state to NULL here,
|
|
* to avoid potential heap-use-after-free errors */
|
|
state = NULL;
|
|
task->state = NULL;
|
|
}
|
|
}
|
|
|
|
#if defined(HAVE_GFX_WIDGETS)
|
|
static void task_screenshot_callback(retro_task_t *task,
|
|
void *task_data,
|
|
void *user_data, const char *error)
|
|
{
|
|
screenshot_task_state_t *state = NULL;
|
|
|
|
if (!task)
|
|
return;
|
|
|
|
if (!(state = (screenshot_task_state_t*)task->state))
|
|
return;
|
|
|
|
if ( !(state->flags & SS_TASK_FLAG_SILENCE)
|
|
&& (state->flags & SS_TASK_FLAG_WIDGETS_READY))
|
|
gfx_widget_screenshot_taken(dispwidget_get_ptr(),
|
|
state->shotname, state->filename);
|
|
|
|
free(state);
|
|
/* Must explicitly set task->state to NULL here,
|
|
* to avoid potential heap-use-after-free errors */
|
|
state = NULL;
|
|
task->state = NULL;
|
|
}
|
|
#endif
|
|
|
|
/**
|
|
* screenshot_rotate:
|
|
*
|
|
* Copies rotated @source image to @target
|
|
* according to @rotate_type 'enum video_rotation_type'.
|
|
**/
|
|
static void screenshot_rotate(
|
|
uint32_t *target,
|
|
uint32_t *source,
|
|
size_t size,
|
|
unsigned *width,
|
|
unsigned *height,
|
|
size_t *pitch,
|
|
uint8_t rotate_type)
|
|
{
|
|
int y;
|
|
size_t x;
|
|
size_t bpp = *pitch / *width;
|
|
size_t source_pitch = *pitch;
|
|
int source_width = *width;
|
|
int source_height = *height;
|
|
int target_width = source_width;
|
|
int target_height = source_height;
|
|
size_t target_pitch = source_pitch;
|
|
|
|
/* 90 deg dimension flip */
|
|
if ( rotate_type == VIDEO_ROTATION_90_DEG
|
|
|| rotate_type == VIDEO_ROTATION_270_DEG)
|
|
{
|
|
target_width = source_height;
|
|
target_height = source_width;
|
|
target_pitch = target_width * bpp;
|
|
}
|
|
|
|
for (y = 0; y < target_height; y++)
|
|
{
|
|
for (x = 0; x < target_pitch; x++)
|
|
{
|
|
size_t pixel_source = (y * source_width) + x;
|
|
size_t pixel_target = (y * target_width) + x;
|
|
|
|
switch (rotate_type)
|
|
{
|
|
default:
|
|
case VIDEO_ROTATION_NORMAL:
|
|
break;
|
|
case VIDEO_ROTATION_90_DEG:
|
|
pixel_source = ((target_height - 1 - y) + (x * source_width));
|
|
break;
|
|
case VIDEO_ROTATION_180_DEG:
|
|
pixel_source = ((target_height - 1 - y) * target_width) + (target_width - 1 - x);
|
|
break;
|
|
case VIDEO_ROTATION_270_DEG:
|
|
pixel_source = y + ((target_width - 1 - x) * source_width);
|
|
break;
|
|
}
|
|
|
|
if ( pixel_source < 0
|
|
|| pixel_target < 0
|
|
|| pixel_source > size / bpp
|
|
|| pixel_target > size / bpp)
|
|
continue;
|
|
|
|
*(target + pixel_target) = *(source + pixel_source);
|
|
}
|
|
}
|
|
|
|
/* Replace source values with target values */
|
|
*width = target_width;
|
|
*height = target_height;
|
|
*pitch = target_pitch;
|
|
}
|
|
|
|
/* Take frame bottom-up. */
|
|
static bool screenshot_dump(
|
|
const char *screenshot_dir,
|
|
const char *name_base,
|
|
const void *frame,
|
|
unsigned width,
|
|
unsigned height,
|
|
int pitch,
|
|
bool bgr24,
|
|
void *userbuf,
|
|
bool savestate,
|
|
uint32_t runloop_flags,
|
|
bool fullpath,
|
|
bool use_thread,
|
|
unsigned pixel_format_type,
|
|
const struct rpng_hdr_metadata *hdr)
|
|
{
|
|
uint8_t *buf = NULL;
|
|
settings_t *settings = config_get_ptr();
|
|
bool history_list_enable = settings->bools.history_list_enable;
|
|
screenshot_task_state_t *state = (screenshot_task_state_t*)
|
|
calloc(1, sizeof(*state));
|
|
|
|
if (!state)
|
|
return false;
|
|
|
|
/* If fullpath is true, name_base already contains a
|
|
* static path + filename to save the screenshot to. */
|
|
if (fullpath)
|
|
strlcpy(state->filename, name_base, sizeof(state->filename));
|
|
|
|
if (runloop_flags & RUNLOOP_FLAG_IDLE)
|
|
state->flags |= SS_TASK_FLAG_IS_IDLE;
|
|
if (runloop_flags & RUNLOOP_FLAG_PAUSED)
|
|
state->flags |= SS_TASK_FLAG_IS_PAUSED;
|
|
if (bgr24)
|
|
state->flags |= SS_TASK_FLAG_BGR24;
|
|
if (hdr)
|
|
{
|
|
state->flags |= SS_TASK_FLAG_HDR;
|
|
state->hdr = *hdr;
|
|
}
|
|
state->width = width;
|
|
state->height = height;
|
|
state->out_width = width;
|
|
state->out_height = height;
|
|
state->pitch = pitch;
|
|
state->frame = frame;
|
|
state->userbuf = userbuf;
|
|
#if defined(HAVE_GFX_WIDGETS)
|
|
if (gfx_widgets_ready())
|
|
state->flags |= SS_TASK_FLAG_WIDGETS_READY;
|
|
#endif
|
|
if (savestate)
|
|
{
|
|
/* Use native core output dimensions */
|
|
unsigned cache_w = 0, cache_h = 0;
|
|
video_driver_state_t *video_st = video_state_get_ptr();
|
|
video_driver_cached_frame_info(&cache_w, &cache_h, NULL, NULL);
|
|
if (video_st)
|
|
{
|
|
state->out_width = (cache_w <= 4)
|
|
? video_st->av_info.geometry.base_width
|
|
: cache_w;
|
|
state->out_height = (cache_h <= 4)
|
|
? video_st->av_info.geometry.base_height
|
|
: cache_h;
|
|
}
|
|
|
|
/* Fallback to display size if smaller than core output */
|
|
if (state->out_width > width || state->out_height > height)
|
|
{
|
|
state->out_width = width;
|
|
state->out_height = height;
|
|
}
|
|
|
|
state->flags |= SS_TASK_FLAG_SILENCE;
|
|
}
|
|
|
|
if (history_list_enable)
|
|
state->flags |= SS_TASK_FLAG_HISTORY_LIST_ENABLE;
|
|
state->pixel_format_type = pixel_format_type;
|
|
|
|
if (!fullpath)
|
|
{
|
|
if (savestate)
|
|
{
|
|
size_t _len = strlcpy(state->filename,
|
|
name_base, sizeof(state->filename));
|
|
strlcpy_lit(state->filename + _len,
|
|
".png",
|
|
sizeof(state->filename) - _len);
|
|
}
|
|
else
|
|
{
|
|
char new_screenshot_dir[DIR_MAX_LENGTH];
|
|
|
|
if (screenshot_dir && *screenshot_dir)
|
|
{
|
|
const char *content_dir = path_get(RARCH_PATH_BASENAME);
|
|
|
|
/* Append content directory name to screenshot
|
|
* path, if required */
|
|
if ( settings->bools.sort_screenshots_by_content_enable
|
|
&& content_dir && *content_dir)
|
|
{
|
|
char content_dir_name[DIR_MAX_LENGTH];
|
|
fill_pathname_parent_dir_name(content_dir_name,
|
|
content_dir, sizeof(content_dir_name));
|
|
fill_pathname_join_special(
|
|
new_screenshot_dir,
|
|
screenshot_dir,
|
|
content_dir_name,
|
|
sizeof(new_screenshot_dir));
|
|
}
|
|
else
|
|
strlcpy(new_screenshot_dir, screenshot_dir,
|
|
sizeof(new_screenshot_dir));
|
|
}
|
|
|
|
if (settings->bools.auto_screenshot_filename)
|
|
{
|
|
const char *screenshot_name = NULL;
|
|
|
|
if (path_is_empty(RARCH_PATH_CONTENT))
|
|
{
|
|
struct retro_system_info sysinfo;
|
|
if (!core_get_system_info(&sysinfo))
|
|
{
|
|
free(state);
|
|
return false;
|
|
}
|
|
|
|
if (!sysinfo.library_name || !*sysinfo.library_name)
|
|
screenshot_name = "RetroArch";
|
|
else
|
|
screenshot_name = sysinfo.library_name;
|
|
}
|
|
else
|
|
screenshot_name = path_basename_nocompression(name_base);
|
|
|
|
fill_str_dated_filename(state->shotname, screenshot_name,
|
|
IMG_EXT, sizeof(state->shotname));
|
|
}
|
|
else
|
|
{
|
|
size_t _len = strlcpy(state->shotname,
|
|
path_basename_nocompression(name_base),
|
|
sizeof(state->shotname));
|
|
strlcpy_lit(state->shotname + _len,
|
|
".png",
|
|
sizeof(state->shotname) - _len);
|
|
}
|
|
|
|
if ( !*new_screenshot_dir
|
|
|| settings->bools.screenshots_in_content_dir)
|
|
fill_pathname_basedir(new_screenshot_dir, name_base,
|
|
sizeof(new_screenshot_dir));
|
|
|
|
fill_pathname_join_special(state->filename, new_screenshot_dir,
|
|
state->shotname, sizeof(state->filename));
|
|
|
|
/* Create screenshot directory, if required */
|
|
if (!path_is_directory(new_screenshot_dir))
|
|
if (!path_mkdir(new_screenshot_dir))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
#if defined(HAVE_RPNG)
|
|
/* Only allocate the BGR24 output buffer when screenshot_dump_direct
|
|
* will actually use the scaler, i.e. when the output dimensions
|
|
* differ from the source. At matching dimensions every source
|
|
* format (BGR24 viewport read-backs, XRGB8888 and RGB565 core
|
|
* framebuffers, HDR) is encoded directly with a negative pitch and
|
|
* no intermediate buffer is needed. */
|
|
if ( !(state->flags & SS_TASK_FLAG_HDR)
|
|
&& ( state->out_width != width
|
|
|| state->out_height != height))
|
|
{
|
|
if (!(buf = (uint8_t*)malloc(state->out_width * state->out_height * 3)))
|
|
{
|
|
free(state);
|
|
return false;
|
|
}
|
|
state->out_buffer = buf;
|
|
}
|
|
#endif
|
|
|
|
if (use_thread)
|
|
{
|
|
retro_task_t *task = task_init();
|
|
|
|
task->type = TASK_TYPE_BLOCKING;
|
|
task->state = state;
|
|
task->handler = task_screenshot_handler;
|
|
if (savestate)
|
|
task->flags |= RETRO_TASK_FLG_MUTE;
|
|
else
|
|
task->flags &= ~RETRO_TASK_FLG_MUTE;
|
|
#if defined(HAVE_GFX_WIDGETS)
|
|
/* This callback is only required when
|
|
* widgets are enabled */
|
|
if (state->flags & SS_TASK_FLAG_WIDGETS_READY)
|
|
task->callback = task_screenshot_callback;
|
|
|
|
if ((state->flags & SS_TASK_FLAG_WIDGETS_READY) && !savestate)
|
|
task_free_title(task);
|
|
else
|
|
#endif
|
|
{
|
|
if (!savestate && settings->bools.notification_show_screenshot)
|
|
task->title = strdup(msg_hash_to_str(MSG_TAKING_SCREENSHOT));
|
|
}
|
|
|
|
if (task_queue_push(task))
|
|
return true;
|
|
|
|
/* There is already a blocking task going on */
|
|
if (task->title)
|
|
task_free_title(task);
|
|
|
|
free(task);
|
|
|
|
if (state->out_buffer)
|
|
free(state->out_buffer);
|
|
|
|
free(state);
|
|
|
|
return false;
|
|
}
|
|
|
|
return screenshot_dump_direct(state);
|
|
}
|
|
|
|
static bool take_screenshot_viewport(
|
|
const char *screenshot_dir,
|
|
const char *name_base,
|
|
bool savestate,
|
|
uint32_t runloop_flags,
|
|
bool fullpath,
|
|
bool use_thread,
|
|
unsigned pixel_format_type)
|
|
{
|
|
struct video_viewport vp;
|
|
video_driver_state_t *video_st = video_state_get_ptr();
|
|
uint8_t *buffer = NULL;
|
|
|
|
vp.x = 0;
|
|
vp.y = 0;
|
|
vp.width = 0;
|
|
vp.height = 0;
|
|
vp.full_width = 0;
|
|
vp.full_height = 0;
|
|
|
|
video_driver_get_viewport_info(&vp);
|
|
|
|
if (!vp.width || !vp.height)
|
|
return false;
|
|
|
|
/* Prefer a native HDR read-back when the driver offers one. This path is
|
|
* entirely additive: if read_viewport_hdr is absent or returns false
|
|
* (e.g. not in HDR mode, or the driver has no HDR read-back), we fall
|
|
* straight through to the ordinary 8-bit SDR read_viewport below, so
|
|
* normal screenshots are unaffected. */
|
|
if (video_st->current_video->read_viewport_hdr)
|
|
{
|
|
struct rpng_hdr_metadata hdr;
|
|
uint16_t *hdr_buffer = (uint16_t*)malloc((size_t)vp.width * vp.height * 6);
|
|
|
|
memset(&hdr, 0, sizeof(hdr));
|
|
if (hdr_buffer)
|
|
{
|
|
if (video_st->current_video->read_viewport_hdr(
|
|
video_st->data, hdr_buffer,
|
|
runloop_flags & RUNLOOP_FLAG_IDLE, &hdr))
|
|
{
|
|
if (vp.width > video_st->width)
|
|
vp.width = video_st->width;
|
|
if (vp.height > video_st->height)
|
|
vp.height = video_st->height;
|
|
|
|
/* 48-bit RGB, bottom-up (pitch = width*6, negated top-down
|
|
* inside screenshot_dump_direct like the BGR24 path). */
|
|
if (screenshot_dump(screenshot_dir,
|
|
name_base,
|
|
hdr_buffer, vp.width, vp.height,
|
|
vp.width * 6, false, hdr_buffer,
|
|
savestate, runloop_flags, fullpath, use_thread,
|
|
pixel_format_type, &hdr))
|
|
return true;
|
|
}
|
|
free(hdr_buffer);
|
|
}
|
|
}
|
|
|
|
if (!(buffer = (uint8_t*)malloc(vp.width * vp.height * 3)))
|
|
return false;
|
|
|
|
if (( video_st->current_video->read_viewport
|
|
&& video_st->current_video->read_viewport(
|
|
video_st->data, buffer, runloop_flags & RUNLOOP_FLAG_IDLE)))
|
|
{
|
|
/* Limit image to screen size */
|
|
if (vp.width > video_st->width)
|
|
vp.width = video_st->width;
|
|
if (vp.height > video_st->height)
|
|
vp.height = video_st->height;
|
|
|
|
/* Data read from viewport is in bottom-up order, suitable for BMP. */
|
|
if (screenshot_dump(screenshot_dir,
|
|
name_base,
|
|
buffer, vp.width, vp.height,
|
|
vp.width * 3, true, buffer,
|
|
savestate, runloop_flags, fullpath, use_thread,
|
|
pixel_format_type, NULL))
|
|
return true;
|
|
}
|
|
|
|
free(buffer);
|
|
return false;
|
|
}
|
|
|
|
/* Read-side callback for take_screenshot_raw: allocate a copy of
|
|
* the cached frame and stash it in this small POD so the caller
|
|
* gets a stable pointer it owns. The copy is necessary because
|
|
* the screenshot task may run on a worker thread and the
|
|
* cached_frame's lifetime can end (core close, driver reinit) at
|
|
* any point after the callback returns.
|
|
*/
|
|
struct ss_raw_copy
|
|
{
|
|
void *buffer;
|
|
unsigned width;
|
|
unsigned height;
|
|
size_t pitch;
|
|
};
|
|
|
|
static void ss_raw_copy_cb(void *userdata,
|
|
const void *data,
|
|
unsigned width, unsigned height, size_t pitch)
|
|
{
|
|
struct ss_raw_copy *out = (struct ss_raw_copy*)userdata;
|
|
size_t size;
|
|
|
|
if (!data || !width || !height || !pitch)
|
|
return;
|
|
|
|
size = (size_t)height * pitch;
|
|
if (!(out->buffer = malloc(size)))
|
|
return;
|
|
|
|
memcpy(out->buffer, data, size);
|
|
out->width = width;
|
|
out->height = height;
|
|
out->pitch = pitch;
|
|
}
|
|
|
|
static bool take_screenshot_raw(
|
|
video_driver_state_t *video_st,
|
|
const char *screenshot_dir,
|
|
const char *name_base, void *userbuf,
|
|
bool savestate, uint32_t runloop_flags,
|
|
bool fullpath, bool use_thread,
|
|
unsigned pixel_format_type)
|
|
{
|
|
const void *frame_ptr = NULL;
|
|
void *owned = NULL; /* heap copy owned by us */
|
|
unsigned width = 0;
|
|
unsigned height = 0;
|
|
size_t pitch = 0;
|
|
|
|
if (userbuf)
|
|
{
|
|
/* The supports_read_frame_raw caller path mallocs a fresh
|
|
* BGR24 buffer via video_driver_read_frame_raw and passes
|
|
* it here, having also updated frame_cache_* to match. The
|
|
* buffer is already an owned snapshot -- no need to re-copy;
|
|
* just use it. Ownership transfers to the screenshot task
|
|
* via the userbuf passthrough (existing cleanup at
|
|
* task_finished frees state->userbuf). */
|
|
bool has_pixels = false;
|
|
if ( !video_driver_cached_frame_info(&width, &height, &pitch,
|
|
&has_pixels)
|
|
|| !has_pixels)
|
|
return false;
|
|
frame_ptr = userbuf;
|
|
}
|
|
else
|
|
{
|
|
/* Standard CPU-path screenshot: pull a heap-owned copy of
|
|
* the cached frame's pixels via the lifetime-safe callback
|
|
* API. The screenshot task is deferred onto a worker
|
|
* thread; without copying we'd risk a UAF if the core
|
|
* closes or the driver reinit's between this enqueue and
|
|
* the worker dequeuing it. */
|
|
struct ss_raw_copy copy = { NULL, 0, 0, 0 };
|
|
video_driver_cached_frame_read(©, ss_raw_copy_cb);
|
|
|
|
if (!copy.buffer || !copy.width || !copy.height || !copy.pitch)
|
|
{
|
|
free(copy.buffer);
|
|
return false;
|
|
}
|
|
|
|
owned = copy.buffer;
|
|
frame_ptr = copy.buffer;
|
|
width = copy.width;
|
|
height = copy.height;
|
|
pitch = copy.pitch;
|
|
|
|
/* Rotate the buffer according to core SET_ROTATION */
|
|
{
|
|
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
|
rarch_system_info_t *sys_info = &runloop_st->system;
|
|
|
|
if (sys_info && sys_info->rotation)
|
|
{
|
|
uint32_t *buf = NULL;
|
|
uint8_t bpp = pitch / width;
|
|
size_t size = width * height * bpp;
|
|
|
|
if ((buf = (uint32_t*)calloc(1, size)))
|
|
{
|
|
screenshot_rotate(buf, (uint32_t*)copy.buffer, size,
|
|
&width, &height, &pitch,
|
|
sys_info->rotation);
|
|
memcpy(copy.buffer, buf, size);
|
|
|
|
free(buf);
|
|
buf = NULL;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Negative pitch is needed as screenshot takes bottom-up, but
|
|
* we use top-down. */
|
|
if (screenshot_dump(screenshot_dir,
|
|
name_base,
|
|
(const uint8_t*)frame_ptr + (height - 1) * pitch,
|
|
width,
|
|
height,
|
|
(int)(-pitch),
|
|
false,
|
|
owned ? owned : userbuf, /* userbuf: cleanup frees it */
|
|
savestate,
|
|
runloop_flags,
|
|
fullpath,
|
|
use_thread,
|
|
pixel_format_type,
|
|
NULL))
|
|
return true;
|
|
|
|
/* screenshot_dump only takes ownership on success; on failure
|
|
* we have to free our copy. The caller-owned userbuf isn't
|
|
* ours to free here. */
|
|
free(owned);
|
|
return false;
|
|
}
|
|
|
|
static bool take_screenshot_choice(
|
|
video_driver_state_t *video_st,
|
|
const char *screenshot_dir,
|
|
const char *name_base,
|
|
bool savestate,
|
|
uint32_t runloop_flags,
|
|
bool has_valid_framebuffer,
|
|
bool fullpath,
|
|
bool use_thread,
|
|
bool supports_vp_read,
|
|
bool supports_read_frame_raw,
|
|
unsigned pixel_format_type
|
|
)
|
|
{
|
|
if (supports_vp_read)
|
|
{
|
|
/* Avoid taking screenshot of GUI overlays. */
|
|
if (video_st->poke && video_st->poke->set_texture_enable)
|
|
video_st->poke->set_texture_enable(video_st->data,
|
|
false, false);
|
|
if (!(runloop_flags & RUNLOOP_FLAG_IDLE))
|
|
video_driver_cached_frame();
|
|
return take_screenshot_viewport(screenshot_dir,
|
|
name_base, savestate, runloop_flags, fullpath, use_thread,
|
|
pixel_format_type);
|
|
}
|
|
|
|
if (!has_valid_framebuffer)
|
|
return take_screenshot_raw(video_st, screenshot_dir,
|
|
name_base, NULL, savestate, runloop_flags, fullpath, use_thread,
|
|
pixel_format_type);
|
|
|
|
if (supports_read_frame_raw)
|
|
{
|
|
/* video_driver_read_frame_raw's w/h/p are pure out-params --
|
|
* the driver writes the dimensions of the buffer it
|
|
* returns. On success we publish (frame_data, dims) into
|
|
* the cache before invoking take_screenshot_raw, which then
|
|
* pulls dims via cached_frame_info() and uses frame_data
|
|
* directly as a caller-owned snapshot. On failure
|
|
* (frame_data == NULL) we leave the existing cache state
|
|
* untouched -- cleaner than the previous save-and-restore
|
|
* dance that mixed old data with new dims. */
|
|
unsigned w = 0;
|
|
unsigned h = 0;
|
|
size_t p = 0;
|
|
void *frame_data = video_driver_read_frame_raw(&w, &h, &p);
|
|
|
|
if (frame_data)
|
|
{
|
|
video_driver_cached_frame_publish(frame_data, w, h, p);
|
|
return take_screenshot_raw(video_st, screenshot_dir,
|
|
name_base, frame_data, savestate, runloop_flags, fullpath, use_thread,
|
|
pixel_format_type);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool take_screenshot(
|
|
const char *screenshot_dir,
|
|
const char *name_base,
|
|
bool savestate, bool has_valid_framebuffer,
|
|
bool fullpath, bool use_thread)
|
|
{
|
|
bool ret = false;
|
|
uint32_t runloop_flags = runloop_get_flags();
|
|
settings_t *settings = config_get_ptr();
|
|
video_driver_state_t *video_st = video_state_get_ptr();
|
|
bool video_gpu_screenshot = settings->bools.video_gpu_screenshot;
|
|
bool supports_vp_read = video_st->current_video->read_viewport
|
|
&& (video_st->current_video->viewport_info);
|
|
bool prefer_vp_read = false;
|
|
if (supports_vp_read)
|
|
{
|
|
/* Use VP read screenshots if it's a HW context core
|
|
* and read_frame_raw is not implemented */
|
|
if ( video_driver_is_hw_context()
|
|
&& !video_st->current_video->read_frame_raw)
|
|
prefer_vp_read = true;
|
|
/* Avoid GPU screenshots with savestates */
|
|
if (video_gpu_screenshot && !savestate)
|
|
prefer_vp_read = true;
|
|
}
|
|
/* No way to infer screenshot directory. */
|
|
if ( (!screenshot_dir || !*screenshot_dir)
|
|
&& (!name_base || !*name_base))
|
|
return false;
|
|
ret = take_screenshot_choice(
|
|
video_st,
|
|
screenshot_dir,
|
|
name_base,
|
|
savestate,
|
|
runloop_flags,
|
|
has_valid_framebuffer,
|
|
fullpath,
|
|
use_thread,
|
|
prefer_vp_read,
|
|
(video_st->current_video->read_frame_raw != NULL),
|
|
video_st->pix_fmt
|
|
);
|
|
if ( (runloop_flags & RUNLOOP_FLAG_PAUSED)
|
|
&& (!(runloop_flags & RUNLOOP_FLAG_IDLE)))
|
|
video_driver_cached_frame();
|
|
return ret;
|
|
}
|
|
|