formats: decouple file I/O from rpng/rbmp/cdfs, bulk-write screenshots

Make rpng_encode, rbmp_encode and cdfs pure format modules - bytes or
streams in, bytes or streams out - with path open/close reduced to
thin adapters, following the pattern already applied to rchd, rm3u,
logiqx_dat and rxml.  The file I/O that moves to the call sites is
redesigned rather than relocated: the screenshot task now encodes to
memory and hits the filesystem with a single bulk write.

rpng: rpng_encode.c already had pure stream/string entry points; the
four path wrappers (rpng_save_image_argb / _bgr24 / _rgba /
_rgb48_hdr) move to a new adapter TU, formats/png/rpng_file.c, with
unchanged signatures and symbols.  rpng_save_image_stream_fmt() and
enum rpng_pixfmt gain declarations in rpng.h so adapters and callers
can drive the encoder without duplicating per-format plumbing.  The
pure TU no longer includes file_stream.h.

rbmp: rbmp_encode.c is reworked into a pure encoder.  New primary API
rbmp_save_image_string() encodes the whole image into a single
exactly-sized allocation (a BMP's size is fully determined by its
dimensions); rbmp_save_image_stream() encodes onto a caller-provided
intfstream_t with one padded row of scratch and byte-identical
output.  The old rbmp_save_image(path) survives unchanged as a
deprecated adapter in formats/bmp/rbmp_file.c.  Two latent hazards in
the old writer are fixed: the BGR24 path copied stride-many bytes per
row (negative strides went through filestream_write as negative
lengths; over-wide strides corrupted the fixed row layout) and is now
bounded by the row payload with padding explicitly zeroed, so
tightly-allocated viewport readback buffers with unaligned widths are
never read past their final row; unknown source types now emit a
valid zero-filled image instead of a truncated header-only file.

cdfs: cdfs.c is restructured into a documented pure layer and a path
adapter layer (the physical TU split is deliberately not taken -
every core vendoring cdfs.c would need build churn for no behavioural
gain).  cdfs_track_from_stream() publishes the previously private
stream wrap with documented ownership and a checked allocation.
cdfs_parse_cue() extracts cue interpretation as a pure, bounded,
non-mutating parser, removing several out-of-bounds hazards of the
old in-place parser (reliance on unchecked NUL termination,
atoi(track + 6) past short final lines, mis-tokenised tab-separated
cues).  The cdfs_open_*_track family remains as adapters with
unchanged signatures; the cue adapter now does one bulk read of the
cue and releases that handle before opening the bin; CHD stays
confined to the adapter layer.

cdfs positioning fixes: the byte-granular seek layer had three
compounding bugs that made cdfs_seek / cdfs_tell unusable on files
opened by name (first_sector != 0): cdfs_seek compared a
file-relative sector against the disc-absolute current_sector, so
every seek rewound reads to offset 0; the post-seek cache refill
ignored current_sector_offset; and the cache-hit read fast path never
advanced file->pos, so cdfs_tell / SEEK_CUR drifted.  All three are
fixed and locked in by the new sample test.

task_screenshot: all PNG paths and the RBMP fallback encode into an
in-memory buffer via the *_string APIs and write with a single
filestream_write_file() - one open, one sequential write, one close -
instead of interleaving small VFS writes through the whole
compression run while holding the handle.  The BGR24 no-resize fast
path feeds the viewport readback straight to the encoder with a
negative stride, deleting a full-frame flip-and-copy (~48 MiB of
allocation plus copy per 4K screenshot).  If the whole-image
allocation fails, the task falls back to streaming the encode with a
few rows of scratch, preserving the old memory profile as a degraded
mode instead of failing the screenshot.  Encoding already runs on the
task worker thread, so the UI-facing cost is one bulk write.

Measured at 3840x2160 (local fs, avg of 3-5 runs): PNG 182.0 ->
168.6 ms/frame with peak RSS 49.6 -> 25.8 MiB on compressible
frames (the string encoder's worst-case bound is virtual; only
compressed pages are touched) and 689.6 -> 672.0 ms at equal RSS on
incompressible noise; BMP 28.4 -> 25.8 ms with write syscalls per
frame dropping 4322 -> 3.  The BMP buffered transient equals the
file size, which on the !HAVE_RPNG platforms that actually reach the
BMP path (low-resolution builds) is a few hundred KB, with the
stream fallback as the safety net.  On slow or contended media (SD,
network mounts, Android SAF) the single-write pattern is the point;
the in-tree benchmark below reproduces the comparison on target
hardware.

Tests and tooling: cdfs_pure_api_test drives the pure cdfs layer
entirely in memory (cue parsing incl. quoted names, CRLF, tabs,
pregap sector arithmetic, audio-track skipping, truncation fuzzing;
raw MODE1/2352 ISO-9660 walk, multi-sector reads, cross-sector seeks,
tell accounting).  rbmp_encode_test covers all four source types,
positive and negative pitch, unaligned-width padding, header fields,
string/stream/path-adapter byte identity, and pixel-exact decoding
through the in-tree rbmp decoder.  rpng_roundtrip_test gains
XRGB8888/RGB565 coverage of rpng_save_image_stream_fmt, the exact
call shape the screenshot fast paths use.  samples/formats/bench/
screenshot_io_bench replicates the old and new task I/O patterns for
reproducible measurement.  All suites run clean under ASan, UBSan,
LeakSan (strict) and TSan; touched libretro-common TUs compile clean
under the C89 lane, including -DHAVE_CHD.

External ABI: rbmp_save_image, the rpng_save_image_* path wrappers
and the cdfs_open_*_track family keep their signatures and symbol
names; out-of-tree users keep linking so long as they pull in the
adapter TUs, which are wired into Makefile.common, griffin and the
sample Makefiles here.
This commit is contained in:
libretroadmin committed 2026-08-17 02:45:22 +02:00
1 parent 6c38313bdd
commit 253c16e2b5
19 files changed
+2282 -292

No files matched your search

+3 -1
View File
@@ -2277,7 +2277,8 @@ ifeq ($(HAVE_RPNG), 1)
DEFINES += -DHAVE_RPNG
OBJ += $(LIBRETRO_COMM_DIR)/formats/png/rpng.o \
$(LIBRETRO_COMM_DIR)/formats/png/rpng_apng.o \
$(LIBRETRO_COMM_DIR)/formats/png/rpng_encode.o
$(LIBRETRO_COMM_DIR)/formats/png/rpng_encode.o \
$(LIBRETRO_COMM_DIR)/formats/png/rpng_file.o
endif
ifeq ($(HAVE_RJPEG), 1)
@@ -2349,6 +2350,7 @@ ifeq ($(HAVE_WEBMPLAYER), 1)
endif
OBJ += $(LIBRETRO_COMM_DIR)/formats/bmp/rbmp_encode.o \
$(LIBRETRO_COMM_DIR)/formats/bmp/rbmp_file.o \
$(LIBRETRO_COMM_DIR)/formats/json/rjson.o \
$(LIBRETRO_COMM_DIR)/formats/image_transfer.o \
$(LIBRETRO_COMM_DIR)/formats/audio_transfer.o \
+2
View File
@@ -464,6 +464,7 @@ VIDEO IMAGE
#include "../libretro-common/formats/png/rpng.c"
#include "../libretro-common/formats/png/rpng_apng.c"
#include "../libretro-common/formats/png/rpng_encode.c"
#include "../libretro-common/formats/png/rpng_file.c"
#endif
#ifdef HAVE_RJPEG
#include "../libretro-common/formats/jpeg/rjpeg.c"
@@ -510,6 +511,7 @@ VIDEO IMAGE
#endif
#include "../libretro-common/formats/bmp/rbmp_encode.c"
#include "../libretro-common/formats/bmp/rbmp_file.c"
#ifdef HAVE_RWAV
#include "../libretro-common/formats/wav/rwav.c"
+143 -74
View File
@@ -23,9 +23,16 @@
#include <stdlib.h>
#include <string.h>
#include <streams/file_stream.h>
#include <streams/interface_stream.h>
#include <formats/rbmp.h>
/* This TU is a pure encoder: bytes in -> bytes out. It never opens a
* path and has no dependency on file_stream.h / the VFS. The primary
* entry point is rbmp_save_image_string() (exact-size heap buffer);
* rbmp_save_image_stream() writes to any already-open intfstream. The
* legacy path-based rbmp_save_image() lives in rbmp_file.c as a thin
* deprecated adapter. */
void form_bmp_header(uint8_t *header,
unsigned width, unsigned height,
bool is32bpp)
@@ -106,11 +113,10 @@ void form_bmp_header(uint8_t *header,
header[53] = 0;
}
static bool write_header_bmp(RFILE *file, unsigned width, unsigned height, bool is32bpp)
static size_t rbmp_line_size(unsigned width, enum rbmp_source_type type)
{
uint8_t header[54];
form_bmp_header(header, width, height, is32bpp);
return filestream_write(file, header, sizeof(header)) == sizeof(header);
unsigned bpp = (type == RBMP_SOURCE_TYPE_ARGB8888) ? 4 : 3;
return ((size_t)width * bpp + 3) & ~(size_t)3;
}
static void dump_line_565_to_24(uint8_t *line, const uint16_t *src, unsigned width)
@@ -142,95 +148,158 @@ static void dump_line_32_to_24(uint8_t *line, const uint32_t *src, unsigned widt
}
}
static void dump_content(RFILE *file, const void *frame,
int width, int height, int pitch, enum rbmp_source_type type)
/* Produce one padded output row. Returns either @line (row was
* converted/padded into the scratch buffer) or @src directly when the
* source row can be emitted as-is, letting callers skip a copy. */
static const uint8_t *rbmp_fill_row(uint8_t *line, const uint8_t *src,
unsigned width, size_t line_size, unsigned pitch,
enum rbmp_source_type type)
{
int j;
size_t line_size;
uint8_t *line = NULL;
int bytes_per_pixel = (type==RBMP_SOURCE_TYPE_ARGB8888?4:3);
union
{
const uint8_t *u8;
const uint16_t *u16;
const uint32_t *u32;
} u;
u.u8 = (const uint8_t*)frame;
line_size = (width * bytes_per_pixel + 3) & ~3;
size_t copy;
switch (type)
{
case RBMP_SOURCE_TYPE_BGR24:
{
/* BGR24 byte order input matches output. Can directly copy, but... need to make sure we pad it. */
uint32_t zeros = 0;
int padding = (int)(line_size-pitch);
for (j = 0; j < height; j++, u.u8 += pitch)
{
filestream_write(file, u.u8, pitch);
if (padding != 0)
filestream_write(file, &zeros, padding);
}
}
break;
case RBMP_SOURCE_TYPE_ARGB8888:
/* ARGB8888 byte order input matches output. Can directly copy. */
for (j = 0; j < height; j++, u.u8 += pitch)
filestream_write(file, u.u8, line_size);
return;
default:
break;
}
/* allocate line buffer, and initialize the final four bytes to zero, for deterministic padding */
if (!(line = (uint8_t*)malloc(line_size)))
return;
*(uint32_t*)(line + line_size - 4) = 0;
switch (type)
{
/* ARGB8888 byte order matches the output and a 32bpp row is
* naturally 4-byte aligned, so no padding exists. */
return src;
case RBMP_SOURCE_TYPE_BGR24:
/* BGR24 byte order matches the output; only row padding may
* need to be synthesised. Rows are handed to the writer
* as-is only when the stride equals the padded row size;
* otherwise copy exactly the row payload (never the stride,
* which may be negative or wider than the row) and zero the
* padding so tightly-allocated source buffers are never read
* past their final row. */
if ((int)pitch >= 0 && (size_t)pitch == line_size)
return src;
copy = (size_t)width * 3;
if ((int)pitch >= 0 && (size_t)pitch < copy)
copy = (size_t)pitch;
memcpy(line, src, copy);
memset(line + copy, 0, line_size - copy);
return line;
case RBMP_SOURCE_TYPE_XRGB888:
for (j = 0; j < height; j++, u.u8 += pitch)
{
dump_line_32_to_24(line, u.u32, width);
filestream_write(file, line, line_size);
}
dump_line_32_to_24(line, (const uint32_t*)(const void*)src, width);
break;
case RBMP_SOURCE_TYPE_RGB565:
for (j = 0; j < height; j++, u.u8 += pitch)
{
dump_line_565_to_24(line, u.u16, width);
filestream_write(file, line, line_size);
}
dump_line_565_to_24(line, (const uint16_t*)(const void*)src, width);
break;
default:
break;
/* Unknown source type: emit a zeroed (black) row so the file
* still matches the size declared in its header. (The old
* path-based writer emitted a header-only, truncated file
* here.) */
memset(line, 0, line_size);
return line;
}
/* Free allocated line buffer */
free(line);
/* Deterministic 0-3 bytes of row padding for the converted formats. */
memset(line + (size_t)width * 3, 0, line_size - (size_t)width * 3);
return line;
}
bool rbmp_save_image(
const char *filename,
uint8_t *rbmp_save_image_string(
const void *frame,
unsigned width, unsigned height,
unsigned pitch, enum rbmp_source_type type)
unsigned width, unsigned height, unsigned pitch,
enum rbmp_source_type type,
size_t *out_len)
{
bool ret = false;
RFILE *file = filestream_open(filename,
RETRO_VFS_FILE_ACCESS_WRITE,
RETRO_VFS_FILE_ACCESS_HINT_NONE);
if (!file)
unsigned j;
size_t line_size;
size_t _len;
uint8_t *buf = NULL;
uint8_t *dst = NULL;
const uint8_t *src = (const uint8_t*)frame;
/* pitch travels through the public API as unsigned (historical),
* but callers pass negative strides to walk bottom-up sources
* top-down; reinterpret it as signed for the row walk, exactly as
* the old RFILE-based writer did. */
int s_pitch = (int)pitch;
if (!frame || !out_len || !width || !height)
return NULL;
line_size = rbmp_line_size(width, type);
/* BMP output size is exact and fixed:
* 54-byte header + line_size * height rows. */
if (line_size > ((size_t)-1 - 54) / height)
return NULL;
_len = 54 + line_size * height;
if (!(buf = (uint8_t*)malloc(_len)))
return NULL;
form_bmp_header(buf, width, height, type == RBMP_SOURCE_TYPE_ARGB8888);
dst = buf + 54;
for (j = 0; j < height; j++, src += s_pitch, dst += line_size)
{
const uint8_t *row = rbmp_fill_row(dst, src, width,
line_size, pitch, type);
if (row != dst)
memcpy(dst, row, line_size);
}
*out_len = _len;
return buf;
}
bool rbmp_save_image_stream(
intfstream_t *intf_s,
const void *frame,
unsigned width, unsigned height, unsigned pitch,
enum rbmp_source_type type)
{
unsigned j;
uint8_t header[54];
size_t line_size;
uint8_t *line = NULL;
bool ret = true;
const uint8_t *src = (const uint8_t*)frame;
/* See rbmp_save_image_string regarding the signedness of pitch. */
int s_pitch = (int)pitch;
if (!intf_s || !frame || !width || !height)
return false;
ret = write_header_bmp(file, width, height, type==RBMP_SOURCE_TYPE_ARGB8888);
line_size = rbmp_line_size(width, type);
if (ret)
dump_content(file, frame, width, height, pitch, type);
form_bmp_header(header, width, height, type == RBMP_SOURCE_TYPE_ARGB8888);
if (intfstream_write(intf_s, header, sizeof(header))
!= (int64_t)sizeof(header))
return false;
filestream_close(file);
/* Whole-block fast path: when the source stride equals the padded
* BMP row size and the rows already carry the output byte order
* (BGR24 with 4-byte-aligned width, or ARGB whose memory layout is
* BMP's 32bpp layout verbatim), the pixel block on disk is the
* source buffer byte for byte. Hand it to the stream in one write:
* no line buffer, no per-row calls, zero copies on this side. */
if ( s_pitch > 0
&& (size_t)s_pitch == line_size
&& ( type == RBMP_SOURCE_TYPE_BGR24
|| type == RBMP_SOURCE_TYPE_ARGB8888))
{
int64_t block = (int64_t)line_size * height;
return intfstream_write(intf_s, src, block) == block;
}
if (!(line = (uint8_t*)malloc(line_size)))
return false;
for (j = 0; j < height; j++, src += s_pitch)
{
const uint8_t *row = rbmp_fill_row(line, src, width,
line_size, pitch, type);
if (intfstream_write(intf_s, row, line_size) != (int64_t)line_size)
{
ret = false;
break;
}
}
free(line);
return ret;
}
+60
View File
@@ -0,0 +1,60 @@
/* Copyright (C) 2010-2026 The RetroArch team
*
* ---------------------------------------------------------------------------------------
* The following license statement only applies to this file (rbmp_file.c).
* ---------------------------------------------------------------------------------------
*
* Permission is hereby granted, free of charge,
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/* Path-based convenience adapter for the pure BMP encoder in
* rbmp_encode.c. This TU is the only part of the rbmp encode surface
* that touches the filesystem; the encoder itself only speaks buffers
* and intfstreams.
*
* Deprecated: new callers should prefer rbmp_save_image_string() plus a
* single filestream_write_file() at the edge (one open/bulk-write/close
* instead of a write per row), or open an intfstream themselves and use
* rbmp_save_image_stream(). */
#include <stdlib.h>
#include <libretro.h>
#include <streams/interface_stream.h>
#include <formats/rbmp.h>
bool rbmp_save_image(
const char *filename,
const void *frame,
unsigned width, unsigned height,
unsigned pitch, enum rbmp_source_type type)
{
bool ret = false;
intfstream_t *intf_s = intfstream_open_file(filename,
RETRO_VFS_FILE_ACCESS_WRITE,
RETRO_VFS_FILE_ACCESS_HINT_NONE);
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;
}
+225 -109
View File
@@ -9,6 +9,22 @@
#include <streams/chd_stream.h>
#endif
/* This TU is deliberately kept as a single translation unit but is
* organised in two layers:
*
* - the pure layer (everything through cdfs_parse_cue): ISO-9660
* sector/directory/file logic plus cue-sheet parsing. It only ever
* reads through an already-open track->stream or a caller-supplied
* memory buffer, and never opens a path. Entry points:
* cdfs_track_from_stream(), cdfs_parse_cue(), cdfs_open_file() and
* the read/seek family.
*
* - the path adapter layer (cdfs_open_cue_track onward): resolves
* filenames, opens cue/bin/iso/chd files via intfstream, then hands
* the resulting stream to the pure layer. file_path.h and the VFS
* are only used here. Callers that already hold a stream (or an
* in-memory image) can skip this layer entirely. */
static void cdfs_determine_sector_size(cdfs_track_t* track)
{
uint8_t buffer[32];
@@ -316,6 +332,20 @@ int64_t cdfs_read_file(cdfs_file_t* file, void* buffer, uint64_t len)
if (len == 0)
return 0;
/* A seek that changed sectors leaves the position mid-sector with
* no cached data. Refill the cache for the current sector first so
* the consume path below starts at current_sector_offset; without
* this the refill at the bottom of this function copies from the
* start of the sector regardless of the seek target. */
if ( !file->sector_buffer_valid
&& file->current_sector_offset != 0
&& file->current_sector >= file->first_sector)
{
cdfs_seek_track_sector(file->track, file->current_sector);
intfstream_read(file->track->stream, file->sector_buffer, 2048);
file->sector_buffer_valid = 1;
}
if (file->sector_buffer_valid)
{
size_t remaining = 2048 - file->current_sector_offset;
@@ -327,6 +357,11 @@ int64_t cdfs_read_file(cdfs_file_t* file, void* buffer, uint64_t len)
&file->sector_buffer[file->current_sector_offset],
(size_t)len);
file->current_sector_offset += len;
/* This early return previously skipped the position
* accounting at the bottom of the function, leaving
* file->pos (and thus cdfs_tell / SEEK_CUR) stale after
* any read served entirely from the sector cache. */
file->pos += (unsigned int)len;
return len;
}
@@ -434,7 +469,15 @@ int64_t cdfs_seek(cdfs_file_t* file, int64_t offset, int whence)
file->pos = (unsigned int)new_pos;
file->current_sector_offset = file->pos % 2048;
new_sector = file->pos / 2048;
/* current_sector holds a disc-absolute sector index (it is handed
* straight to cdfs_seek_track_sector by the read path), so the
* byte position must be rebased onto the file's first sector.
* Using the bare file-relative sector here made any seek on a
* named file (first_sector != 0) compare/store mismatched sector
* spaces: the subsequent read saw current_sector < first_sector,
* clamped back to the start of the file and reset the intra-sector
* offset, returning data from offset 0 instead of the seek target. */
new_sector = file->first_sector + file->pos / 2048;
if (new_sector != file->current_sector)
{
file->current_sector = new_sector;
@@ -444,13 +487,34 @@ int64_t cdfs_seek(cdfs_file_t* file, int64_t offset, int whence)
return 0;
}
static void cdfs_skip_spaces(const char** ptr)
/* --- Pure layer: cue-sheet parsing and stream wrapping ---------------- */
static const char *cdfs_skip_spaces(const char *p, const char *end)
{
while (**ptr && (**ptr == ' ' || **ptr == '\t'))
++(*ptr);
while (p < end && (*p == ' ' || *p == '\t'))
++p;
return p;
}
static cdfs_track_t* cdfs_wrap_stream(
/* Bounded base-10 parse; advances *p past the digits consumed.
* Returns 0 when no digit is present (mirrors atoi/strtol behaviour
* for the inputs this parser sees). */
static unsigned cdfs_parse_unsigned(const char **p, const char *end)
{
unsigned v = 0;
const char *s = *p;
while (s < end && *s >= '0' && *s <= '9')
{
v = v * 10 + (unsigned)(*s - '0');
++s;
}
*p = s;
return v;
}
cdfs_track_t* cdfs_track_from_stream(
intfstream_t* stream,
unsigned first_sector_offset,
unsigned first_sector_index)
@@ -460,8 +524,15 @@ static cdfs_track_t* cdfs_wrap_stream(
if (!stream)
return NULL;
track = (cdfs_track_t*)
calloc(1, sizeof(*track));
if (!(track = (cdfs_track_t*)calloc(1, sizeof(*track))))
{
/* Ownership of the stream transfers on this call; close it so
* the caller never has to distinguish failure modes. */
intfstream_close(stream);
free(stream);
return NULL;
}
track->stream = stream;
track->first_sector_offset = first_sector_offset;
track->first_sector_index = first_sector_index;
@@ -471,130 +542,126 @@ static cdfs_track_t* cdfs_wrap_stream(
return track;
}
static cdfs_track_t* cdfs_open_cue_track(
const char* path, unsigned int track_index)
bool cdfs_parse_cue(const char *cue_text, size_t len,
unsigned track_index, cdfs_cue_info_t *out)
{
char* cue = NULL;
const char* line = NULL;
int found_track = 0;
char current_track_path[PATH_MAX_LENGTH] = {0};
char track_path[PATH_MAX_LENGTH] = {0};
unsigned int sector_size = 0;
unsigned int previous_sector_size = 0;
unsigned int previous_index_sector_offset= 0;
unsigned int track_offset = 0;
intfstream_t *cue_stream = intfstream_open_file(path, RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE);
int64_t stream_size = intfstream_get_size(cue_stream);
char *cue_contents = (char*)malloc((size_t)(stream_size + 1));
cdfs_track_t* track = NULL;
char current_track_path[PATH_MAX_LENGTH];
const char *p = cue_text;
const char *end = cue_text + len;
unsigned found_track = 0;
unsigned sector_size = 0;
unsigned previous_sector_size = 0;
unsigned previous_index_sector_offset = 0;
unsigned track_offset = 0;
if (!cue_contents)
if (!cue_text || !out)
return false;
current_track_path[0] = '\0';
memset(out, 0, sizeof(*out));
while (p < end)
{
intfstream_close(cue_stream);
free(cue_stream);
return NULL;
}
const char *line;
const char *line_end;
intfstream_read(cue_stream, cue_contents, stream_size);
intfstream_close(cue_stream);
free(cue_stream);
p = cdfs_skip_spaces(p, end);
line = p;
while (p < end && *p != '\n')
++p;
line_end = p;
if (p < end)
++p; /* consume the newline */
cue_contents[stream_size] = '\0';
cue = cue_contents;
while (*cue)
{
cdfs_skip_spaces((const char**)&cue);
line = cue;
while (*cue && *cue != '\n')
++cue;
if (*cue)
*cue++ = '\0';
if (!strncasecmp(line, "FILE", 4))
if (line_end - line >= 4 && !strncasecmp(line, "FILE", 4))
{
const char *file = line + 4;
cdfs_skip_spaces(&file);
const char *file = cdfs_skip_spaces(line + 4, line_end);
if (file[0])
if (file < line_end)
{
const char *file_end = cue - 1;
while (file_end > file && *file_end != ' ' && *file_end != '\t')
/* Walk back from the end of the line to the whitespace
* preceding the trailing type token (e.g. BINARY), leaving
* [file, file_end) covering the (possibly quoted) name.
* When the line has no terminator we start on the last
* character instead of the terminator position. */
const char *file_end =
(line_end < end) ? line_end : (line_end - 1);
while ( file_end > file
&& *file_end != ' '
&& *file_end != '\t')
--file_end;
if ( file[0] == '"'
if ( file_end > file
&& file[0] == '"'
&& file_end[-1] == '"')
{
++file;
--file_end;
}
if (file_end - file < PATH_MAX_LENGTH)
if (file_end >= file)
{
memcpy(current_track_path, file, file_end - file);
current_track_path[file_end - file] = '\0';
}
else
{
memcpy(current_track_path, file, PATH_MAX_LENGTH - 1);
current_track_path[PATH_MAX_LENGTH - 1] = '\0';
size_t name_len = (size_t)(file_end - file);
if (name_len >= sizeof(current_track_path))
name_len = sizeof(current_track_path) - 1;
memcpy(current_track_path, file, name_len);
current_track_path[name_len] = '\0';
}
}
previous_sector_size = 0;
previous_sector_size = 0;
previous_index_sector_offset = 0;
track_offset = 0;
track_offset = 0;
}
else if (!strncasecmp(line, "TRACK", 5))
else if (line_end - line >= 5 && !strncasecmp(line, "TRACK", 5))
{
char *ptr = NULL;
unsigned track_number = 0;
const char *track = line + 5;
cdfs_skip_spaces(&track);
const char *track = cdfs_skip_spaces(line + 5, line_end);
const char *num = track;
unsigned track_number = cdfs_parse_unsigned(&num, line_end);
track_number = (unsigned)strtol(track, &ptr, 10);
while (*track && *track != ' ' && *track != '\n')
while (track < line_end && *track != ' ' && *track != '\t')
++track;
previous_sector_size = sector_size;
cdfs_skip_spaces(&track);
track = cdfs_skip_spaces(track, line_end);
if (!strncasecmp(track, "MODE", 4))
if (line_end - track >= 4 && !strncasecmp(track, "MODE", 4))
{
const char *size_str = track + 6;
/* track_index = 0 means find the first data track */
if (!track_index || track_index == track_number)
found_track = track_number;
sector_size = atoi(track + 6);
if (size_str < line_end)
sector_size = cdfs_parse_unsigned(&size_str, line_end);
else
sector_size = 0;
}
else /* assume AUDIO */
sector_size = 2352;
}
else if (!strncasecmp(line, "INDEX", 5))
else if (line_end - line >= 5 && !strncasecmp(line, "INDEX", 5))
{
unsigned sector_offset;
unsigned min = 0, sec = 0, frame = 0;
unsigned index_number = 0;
const char *index = line + 5;
unsigned min = 0, sec = 0, frame = 0;
const char *index = cdfs_skip_spaces(line + 5, line_end);
const char *num = index;
unsigned index_number = cdfs_parse_unsigned(&num, line_end);
cdfs_skip_spaces(&index);
index_number = (unsigned)strtol(index, NULL, 10);
while (*index && *index != ' ' && *index != '\n')
while (index < line_end && *index != ' ' && *index != '\t')
++index;
cdfs_skip_spaces(&index);
index = cdfs_skip_spaces(index, line_end);
min = cdfs_parse_unsigned(&index, line_end);
if (index < line_end && *index == ':')
++index;
sec = cdfs_parse_unsigned(&index, line_end);
if (index < line_end && *index == ':')
++index;
frame = cdfs_parse_unsigned(&index, line_end);
{
char *end;
min = (unsigned)strtol(index, &end, 10);
if (*end == ':')
++end;
sec = (unsigned)strtol(end, &end, 10);
if (*end == ':')
++end;
frame = (unsigned)strtol(end, NULL, 10);
}
sector_offset = ((min * 60) + sec) * 75 + frame;
sector_offset -= previous_index_sector_offset;
track_offset += sector_offset * previous_sector_size;
@@ -603,39 +670,88 @@ static cdfs_track_t* cdfs_open_cue_track(
if (found_track && index_number == 1)
{
if ( strchr(current_track_path, '/')
|| strchr(current_track_path, '\\'))
strlcpy(track_path, current_track_path, sizeof(track_path));
else
{
fill_pathname_basedir(track_path, path, sizeof(track_path));
strlcat(track_path, current_track_path, sizeof(track_path));
}
if (!current_track_path[0])
return false;
break;
strlcpy(out->bin_name, current_track_path,
sizeof(out->bin_name));
out->track_index = found_track;
out->sector_size = sector_size;
out->first_sector_offset = track_offset;
/* NOTE: only valid if all tracks are in the same BIN file.
* Otherwise we would need to know how many sectors each
* previous BIN file holds, which a cue sheet does not
* store. This affects cdfs_get_first_sector, which
* luckily isn't used much. */
out->first_sector_index = previous_index_sector_offset;
return true;
}
}
}
free(cue_contents);
return false;
}
if (!*track_path)
/* --- Path adapter layer ------------------------------------------------ */
static cdfs_track_t* cdfs_open_cue_track(
const char* path, unsigned int track_index)
{
cdfs_cue_info_t info;
char track_path[PATH_MAX_LENGTH];
int64_t _len;
bool parsed = false;
char *cue_contents = NULL;
cdfs_track_t *track = NULL;
intfstream_t *cue_stream = intfstream_open_file(path,
RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE);
if (!cue_stream)
return NULL;
/* NOTE: previous_index_sector_offset will only be valid if all tracks are in the same BIN file.
* Otherwise, we need to determine how many tracks are in each previous BIN file, which is not
* stored in the CUE file. This will affect cdfs_get_first_sector, which luckily isn't used much. */
track = cdfs_wrap_stream(intfstream_open_file(
/* A cue sheet is a small text file: read it with one bulk read and
* release the handle before the (potentially slow) bin open below,
* instead of interleaving parsing with per-line stream reads. */
_len = intfstream_get_size(cue_stream);
if (_len > 0 && (cue_contents = (char*)malloc((size_t)_len)))
parsed = (intfstream_read(cue_stream, cue_contents, _len) == _len);
intfstream_close(cue_stream);
free(cue_stream);
if (parsed)
parsed = cdfs_parse_cue(cue_contents, (size_t)_len,
track_index, &info);
if (cue_contents)
free(cue_contents);
if (!parsed)
return NULL;
/* Resolve the FILE name from the cue against the cue's own
* directory unless it already carries a path. */
if ( strchr(info.bin_name, '/')
|| strchr(info.bin_name, '\\'))
strlcpy(track_path, info.bin_name, sizeof(track_path));
else
{
fill_pathname_basedir(track_path, path, sizeof(track_path));
strlcat(track_path, info.bin_name, sizeof(track_path));
}
track = cdfs_track_from_stream(intfstream_open_file(
track_path, RETRO_VFS_FILE_ACCESS_READ,
RETRO_VFS_FILE_ACCESS_HINT_NONE), track_offset, previous_index_sector_offset);
RETRO_VFS_FILE_ACCESS_HINT_NONE),
info.first_sector_offset, info.first_sector_index);
if (track && track->stream_sector_size == 0)
{
track->stream_sector_size = sector_size;
track->stream_sector_size = info.sector_size;
if (sector_size == 2352)
if (info.sector_size == 2352)
track->stream_sector_header_size = 16;
else if (sector_size == 2336)
else if (info.sector_size == 2336)
track->stream_sector_header_size = 8;
}
@@ -652,7 +768,7 @@ static cdfs_track_t* cdfs_open_chd_track(const char* path, int32_t track_index)
if (!intf_stream)
return NULL;
track = cdfs_wrap_stream(intf_stream,
track = cdfs_track_from_stream(intf_stream,
intfstream_get_offset_to_start(intf_stream),
intfstream_get_first_sector(intf_stream));
@@ -718,7 +834,7 @@ cdfs_track_t* cdfs_open_raw_track(const char* path)
intfstream_t* file = intfstream_open_file(path,
RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE);
track = cdfs_wrap_stream(file, 0, 0);
track = cdfs_track_from_stream(file, 0, 0);
if (track && track->stream_sector_size == 0)
{
cdfs_determine_sector_size_from_file_size(track);
+50 -85
View File
@@ -211,17 +211,9 @@ static bool png_write_iend_string(intfstream_t* intf_s)
sizeof(data) - sizeof(uint32_t));
}
/* Source pixel format. Bytes-per-pixel alone cannot select the 32-bit
* path: ARGB32 and RGBA32 are both four bytes per pixel and differ only
* in channel order, so the format is carried separately and the depth
* derived from it. */
enum rpng_pixfmt
{
RPNG_PIXFMT_BGR24 = 0,
RPNG_PIXFMT_ARGB32,
RPNG_PIXFMT_RGBA32,
RPNG_PIXFMT_RGB48
};
/* enum rpng_pixfmt lives in rpng.h so that the path-based convenience
* adapters in rpng_file.c (and any external stream caller) can select
* the format without this TU exposing anything path-related. */
static unsigned rpng_pixfmt_bpp(enum rpng_pixfmt fmt)
{
@@ -232,6 +224,9 @@ static unsigned rpng_pixfmt_bpp(enum rpng_pixfmt fmt)
return 4;
case RPNG_PIXFMT_RGB48:
return 6;
case RPNG_PIXFMT_XRGB8888:
case RPNG_PIXFMT_RGB565:
/* 32/16-bit sources, but the PNG side is 8-bit RGB. */
case RPNG_PIXFMT_BGR24:
default:
break;
@@ -276,6 +271,39 @@ static void copy_bgr24_line(uint8_t *dst, const uint8_t *src, unsigned width)
}
}
/* X8R8G8B8 -> PNG RGB. The X byte is ignored. */
static void copy_xrgb8888_line(uint8_t *dst, const uint32_t *src,
unsigned width)
{
unsigned i;
for (i = 0; i < width; i++)
{
uint32_t px = src[i];
*dst++ = (uint8_t)(px >> 16); /* R */
*dst++ = (uint8_t)(px >> 8); /* G */
*dst++ = (uint8_t)(px >> 0); /* B */
}
}
/* R5G6B5 -> PNG RGB, expanding channels exactly as the scaler's
* conv_rgb565_bgr24 does so this path is pixel-identical to the
* historical convert-then-encode path. */
static void copy_rgb565_line(uint8_t *dst, const uint16_t *src,
unsigned width)
{
unsigned i;
for (i = 0; i < width; i++)
{
uint16_t px = src[i];
uint8_t r = (px >> 11) & 0x1f;
uint8_t g = (px >> 5) & 0x3f;
uint8_t b = (px >> 0) & 0x1f;
*dst++ = (uint8_t)((r << 3) | (r >> 2));
*dst++ = (uint8_t)((g << 2) | (g >> 4));
*dst++ = (uint8_t)((b << 3) | (b >> 2));
}
}
/* 16-bit-per-channel RGB. PNG stores 16-bit samples big-endian, so each
* host-order R,G,B sample is written most-significant byte first. Input is
* three uint16_t per pixel in R,G,B order (already the PNG channel order,
@@ -633,7 +661,7 @@ static bool flush_idat_chunk(intfstream_t *intf_s,
return png_write_idat_string(intf_s, chunk_buf, payload_len + 8);
}
static bool rpng_save_image_stream_fmt(const uint8_t *data,
bool rpng_save_image_stream_fmt(const uint8_t *data,
intfstream_t* intf_s, unsigned width, unsigned height, signed pitch,
enum rpng_pixfmt fmt, const struct rpng_hdr_metadata *hdr)
{
@@ -802,6 +830,12 @@ static bool rpng_save_image_stream_fmt(const uint8_t *data,
case RPNG_PIXFMT_RGB48:
copy_rgb48_line(rgba_line, (const uint16_t*)data, width);
break;
case RPNG_PIXFMT_XRGB8888:
copy_xrgb8888_line(rgba_line, (const uint32_t*)(const void*)data, width);
break;
case RPNG_PIXFMT_RGB565:
copy_rgb565_line(rgba_line, (const uint16_t*)(const void*)data, width);
break;
case RPNG_PIXFMT_BGR24:
default:
copy_bgr24_line(rgba_line, data, width);
@@ -977,79 +1011,10 @@ bool rpng_save_image_stream(const uint8_t *data, intfstream_t* intf_s,
pitch, fmt, hdr);
}
/* Straight RGBA byte order - R,G,B,A ascending in memory, which on a
* little-endian host is the uint32_t 0xAABBGGRR, the mirror of what
* rpng_save_image_argb() takes. Callers holding GL_RGBA / VK_FORMAT_
* R8G8B8A8 surfaces would otherwise have to swizzle a whole frame into a
* scratch buffer just to have the encoder swizzle it back. */
bool rpng_save_image_rgba(const char *path, const uint8_t *data,
unsigned width, unsigned height, unsigned pitch)
{
bool ret = false;
intfstream_t* intf_s = NULL;
intf_s = intfstream_open_file(path,
RETRO_VFS_FILE_ACCESS_WRITE,
RETRO_VFS_FILE_ACCESS_HINT_NONE);
ret = rpng_save_image_stream_fmt(data, intf_s,
width, height,
(signed) pitch, RPNG_PIXFMT_RGBA32, NULL);
intfstream_close(intf_s);
free(intf_s);
return ret;
}
bool rpng_save_image_argb(const char *path, const uint32_t *data,
unsigned width, unsigned height, unsigned pitch)
{
bool ret = false;
intfstream_t* intf_s = NULL;
intf_s = intfstream_open_file(path,
RETRO_VFS_FILE_ACCESS_WRITE,
RETRO_VFS_FILE_ACCESS_HINT_NONE);
ret = rpng_save_image_stream_fmt((const uint8_t*) data, intf_s,
width, height,
(signed) pitch, RPNG_PIXFMT_ARGB32, NULL);
intfstream_close(intf_s);
free(intf_s);
return ret;
}
bool rpng_save_image_bgr24(const char *path, const uint8_t *data,
unsigned width, unsigned height, unsigned pitch)
{
bool ret = false;
intfstream_t* intf_s = NULL;
intf_s = intfstream_open_file(path,
RETRO_VFS_FILE_ACCESS_WRITE,
RETRO_VFS_FILE_ACCESS_HINT_NONE);
ret = rpng_save_image_stream_fmt(data, intf_s, width, height,
(signed) pitch, RPNG_PIXFMT_BGR24, NULL);
intfstream_close(intf_s);
free(intf_s);
return ret;
}
bool rpng_save_image_rgb48_hdr(const char *path, const uint16_t *data,
unsigned width, unsigned height, unsigned pitch,
const struct rpng_hdr_metadata *hdr)
{
bool ret = false;
intfstream_t* intf_s = NULL;
intf_s = intfstream_open_file(path,
RETRO_VFS_FILE_ACCESS_WRITE,
RETRO_VFS_FILE_ACCESS_HINT_NONE);
ret = rpng_save_image_stream_fmt((const uint8_t*)data, intf_s, width, height,
(signed) pitch, RPNG_PIXFMT_RGB48, hdr);
intfstream_close(intf_s);
free(intf_s);
return ret;
}
/* The path-based convenience wrappers (rpng_save_image_argb / bgr24 /
* rgba / rgb48_hdr) live in rpng_file.c: this TU is a pure encoder and
* never opens a path. Memory-backed intfstreams (used by the *_string
* entry points below) are the only streams it creates itself. */
uint8_t* rpng_save_image_bgr24_hdr_string(const uint8_t *data,
+92
View File
@@ -0,0 +1,92 @@
/* Copyright (C) 2010-2026 The RetroArch team
*
* ---------------------------------------------------------------------------------------
* The following license statement only applies to this file (rpng_file.c).
* ---------------------------------------------------------------------------------------
*
* Permission is hereby granted, free of charge,
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/* Path-based convenience adapters for the pure PNG encoder in
* rpng_encode.c. This TU is the only part of the rpng encode surface
* that opens a path; the encoder itself only speaks intfstreams and
* memory buffers.
*
* Deprecated: new callers should prefer the *_string entry points plus
* a single filestream_write_file() at the edge (one open/bulk-write/
* close instead of interleaving many small chunk writes with the
* compressor), or open an intfstream themselves and call
* rpng_save_image_stream_fmt() / rpng_save_image_stream(). */
#include <stdlib.h>
#include <libretro.h>
#include <streams/interface_stream.h>
#include <formats/rpng.h>
static bool rpng_save_image_file(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 = false;
intfstream_t *intf_s = intfstream_open_file(path,
RETRO_VFS_FILE_ACCESS_WRITE,
RETRO_VFS_FILE_ACCESS_HINT_NONE);
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;
}
/* Straight RGBA byte order - R,G,B,A ascending in memory, which on a
* little-endian host is the uint32_t 0xAABBGGRR, the mirror of what
* rpng_save_image_argb() takes. Callers holding GL_RGBA / VK_FORMAT_
* R8G8B8A8 surfaces would otherwise have to swizzle a whole frame into a
* scratch buffer just to have the encoder swizzle it back. */
bool rpng_save_image_rgba(const char *path, const uint8_t *data,
unsigned width, unsigned height, unsigned pitch)
{
return rpng_save_image_file(path, data,
width, height, (signed)pitch, RPNG_PIXFMT_RGBA32, NULL);
}
bool rpng_save_image_argb(const char *path, const uint32_t *data,
unsigned width, unsigned height, unsigned pitch)
{
return rpng_save_image_file(path, (const uint8_t*)data,
width, height, (signed)pitch, RPNG_PIXFMT_ARGB32, NULL);
}
bool rpng_save_image_bgr24(const char *path, const uint8_t *data,
unsigned width, unsigned height, unsigned pitch)
{
return rpng_save_image_file(path, data,
width, height, (signed)pitch, RPNG_PIXFMT_BGR24, NULL);
}
bool rpng_save_image_rgb48_hdr(const char *path, const uint16_t *data,
unsigned width, unsigned height, unsigned pitch,
const struct rpng_hdr_metadata *hdr)
{
return rpng_save_image_file(path, (const uint8_t*)data,
width, height, (signed)pitch, RPNG_PIXFMT_RGB48, hdr);
}
+54
View File
@@ -23,7 +23,10 @@
#ifndef __RARCH_CDFS_H
#define __RARCH_CDFS_H
#include <stddef.h>
#include <streams/interface_stream.h>
#include <retro_miscellaneous.h>
RETRO_BEGIN_DECLS
@@ -75,6 +78,57 @@ uint32_t cdfs_get_num_sectors(cdfs_file_t* file);
uint32_t cdfs_get_first_sector(cdfs_file_t* file);
/* --- Pure API: no path or VFS I/O ------------------------------------- */
/* Wraps an already-open, seekable stream (file, memory, CHD, custom)
* holding a single track's data and returns a track handle for use with
* cdfs_open_file. The track takes ownership of the stream: it is
* closed and freed by cdfs_close_track, and also on failure of this
* call (except when stream is NULL, in which case NULL is returned and
* there is nothing to own).
*
* first_sector_offset is the byte offset of the track's first sector
* within the stream; first_sector_index is that sector's absolute
* index on the disc (both 0 for a plain single-track image).
*
* The sector layout is probed from the ISO-9660 volume descriptor at
* sector 16, assuming raw 2352-byte sectors. If the probe fails
* (e.g. a cooked 2048-byte image), stream_sector_size is left 0 and
* the caller must fill in stream_sector_size / stream_sector_header_size
* itself (the path adapters below derive them from the file size or
* the cue sheet). */
cdfs_track_t* cdfs_track_from_stream(
intfstream_t* stream,
unsigned first_sector_offset,
unsigned first_sector_index);
/* Result of parsing one track out of a cue sheet held in memory. */
typedef struct cdfs_cue_info
{
/* Name from the FILE line, verbatim (usually relative to the cue's
* directory - resolution against the cue path is the caller's job). */
char bin_name[PATH_MAX_LENGTH];
unsigned track_index; /* track number actually selected */
unsigned sector_size; /* from MODEx/nnnn, 0 if not stated */
unsigned first_sector_offset; /* byte offset of INDEX 01 in the bin */
unsigned first_sector_index; /* absolute sector index of INDEX 01 */
} cdfs_cue_info_t;
/* Parses a cue sheet from a memory buffer (need not be NUL-terminated)
* and locates data track @track_index (0 = first data track). Performs
* no I/O and no path resolution. Returns true and fills *out when the
* track is found; the caller then opens out->bin_name (resolved against
* the cue's location) and passes the stream to cdfs_track_from_stream
* with out->first_sector_offset / out->first_sector_index.
*
* NOTE: first_sector_index is only valid when all tracks live in the
* same BIN file; a cue sheet does not store how many sectors earlier
* BIN files contain. This affects cdfs_get_first_sector only. */
bool cdfs_parse_cue(const char *cue_text, size_t len,
unsigned track_index, cdfs_cue_info_t *out);
/* --- Path adapters: open by filename, then wrap ------------------------ */
/* opens the specified track in a CD or virtual CD file - the resulting stream should be passed to
* cdfs_open_file to get access to a file within the CD.
*
+34
View File
@@ -23,9 +23,13 @@
#ifndef __LIBRETRO_SDK_FORMAT_RBMP_H__
#define __LIBRETRO_SDK_FORMAT_RBMP_H__
#include <stdint.h>
#include <stddef.h>
#include <retro_common_api.h>
#include <boolean.h>
#include <streams/interface_stream.h>
RETRO_BEGIN_DECLS
@@ -40,6 +44,36 @@ enum rbmp_source_type
typedef struct rbmp rbmp_t;
/* Pure encode: returns a heap buffer holding the complete BMP file and
* stores its exact size in *out_len. BMP output size is fixed
* (54 + ((width * bpp + 3) & ~3) * height) so the buffer is allocated
* exactly once and never trimmed. Caller frees. Returns NULL on
* allocation failure or invalid arguments. No file I/O is performed;
* pair with e.g. filestream_write_file() to save to disk with a single
* bulk write. */
uint8_t *rbmp_save_image_string(
const void *frame,
unsigned width,
unsigned height,
unsigned pitch,
enum rbmp_source_type type,
size_t *out_len);
/* Pure encode into an already-open intfstream (file, memory, or
* custom). Writes the 54-byte header followed by one padded row per
* line; peak extra memory is one row. Returns false on short writes. */
bool rbmp_save_image_stream(
intfstream_t *intf_s,
const void *frame,
unsigned width,
unsigned height,
unsigned pitch,
enum rbmp_source_type type);
/* Deprecated path-based convenience wrapper (open + stream encode +
* close), implemented in rbmp_file.c so the pure encoder TU carries no
* filesystem dependency. Prefer rbmp_save_image_string() +
* filestream_write_file(), or rbmp_save_image_stream(). */
bool rbmp_save_image(
const char *filename,
const void *frame,
+52
View File
@@ -29,6 +29,7 @@
#include <retro_common_api.h>
#include <boolean.h>
#include <streams/interface_stream.h>
RETRO_BEGIN_DECLS
@@ -103,6 +104,54 @@ int rpng_process_image(rpng_t *rpng,
bool rpng_start(rpng_t *rpng);
/* Source pixel format for the encoder. Bytes-per-pixel alone cannot
* select the 32-bit path: ARGB32 and RGBA32 are both four bytes per
* pixel and differ only in channel order, so the format is carried
* explicitly and the PNG depth/colour type derived from it. */
enum rpng_pixfmt
{
RPNG_PIXFMT_BGR24 = 0,
RPNG_PIXFMT_ARGB32,
RPNG_PIXFMT_RGBA32,
RPNG_PIXFMT_RGB48,
/* 32-bit X8R8G8B8 (RETRO_PIXEL_FORMAT_XRGB8888) and 16-bit R5G6B5
* (RETRO_PIXEL_FORMAT_RGB565) sources, encoded as 8-bit RGB
* (colour type 2). The per-row conversion happens inside the
* encoder, so callers can feed core framebuffers directly instead
* of allocating a whole-frame BGR24 conversion buffer first.
* RGB565 expands channels as (c << 3) | (c >> 2) (and the 6-bit
* green as (g << 2) | (g >> 4)), matching conv_rgb565_bgr24 in the
* scaler, so the emitted pixels are identical to the historical
* convert-then-encode path. */
RPNG_PIXFMT_XRGB8888,
RPNG_PIXFMT_RGB565
};
struct rpng_hdr_metadata;
/* Pure encode into an already-open intfstream (file, memory, or
* custom). This is the core entry point: everything else - the
* *_string buffer encoders here and the deprecated path wrappers in
* rpng_file.c - is built on top of it. `pitch` is the row stride in
* bytes and may be negative to walk a bottom-up source top-down.
* Peak scratch memory is a handful of rows regardless of image size;
* output is produced in ~16 KiB IDAT chunks as compression proceeds. */
bool rpng_save_image_stream_fmt(const uint8_t *data,
intfstream_t *intf_s, unsigned width, unsigned height, signed pitch,
enum rpng_pixfmt fmt, const struct rpng_hdr_metadata *hdr);
/* Bytes-per-pixel variant of rpng_save_image_stream_fmt, kept for
* callers that predate the format enum: 3 = BGR24, 4 = ARGB32,
* 6 = RGB48. RGBA32 is only reachable through the fmt entry point. */
bool rpng_save_image_stream(const uint8_t *data, intfstream_t *intf_s,
unsigned width, unsigned height, signed pitch, unsigned bpp,
const struct rpng_hdr_metadata *hdr);
/* Deprecated path-based convenience wrappers (open + stream encode +
* close), implemented in rpng_file.c so the pure encoder TU carries no
* filesystem dependency. Prefer the *_string entry points plus a
* single filestream_write_file() at the edge, or the stream entry
* points above. */
bool rpng_save_image_argb(const char *path, const uint32_t *data,
unsigned width, unsigned height, unsigned pitch);
bool rpng_save_image_bgr24(const char *path, const uint8_t *data,
@@ -117,6 +166,9 @@ bool rpng_save_image_bgr24(const char *path, const uint8_t *data,
bool rpng_save_image_rgba(const char *path, const uint8_t *data,
unsigned width, unsigned height, unsigned pitch);
/* Pure encode: returns a heap buffer holding the complete PNG file and
* stores its size in *bytes. Caller frees. Pair with e.g.
* filestream_write_file() to save to disk with a single bulk write. */
uint8_t* rpng_save_image_bgr24_string(const uint8_t *data,
unsigned width, unsigned height, signed pitch, uint64_t *bytes);
@@ -0,0 +1,54 @@
TARGET := screenshot_io_bench
LIBRETRO_COMM_DIR := ../../..
SOURCES := \
screenshot_io_bench.c \
$(LIBRETRO_COMM_DIR)/formats/png/rpng_encode.c \
$(LIBRETRO_COMM_DIR)/formats/bmp/rbmp_encode.c \
$(LIBRETRO_COMM_DIR)/streams/interface_stream.c \
$(LIBRETRO_COMM_DIR)/streams/memory_stream.c \
$(LIBRETRO_COMM_DIR)/streams/file_stream.c \
$(LIBRETRO_COMM_DIR)/streams/rzip_stream.c \
$(LIBRETRO_COMM_DIR)/streams/trans_stream.c \
$(LIBRETRO_COMM_DIR)/streams/trans_stream_pipe.c \
$(LIBRETRO_COMM_DIR)/streams/trans_stream_deflate.c \
$(LIBRETRO_COMM_DIR)/encodings/encoding_deflate.c \
$(LIBRETRO_COMM_DIR)/encodings/encoding_crc32.c \
$(LIBRETRO_COMM_DIR)/encodings/encoding_utf.c \
$(LIBRETRO_COMM_DIR)/vfs/vfs_implementation.c \
$(LIBRETRO_COMM_DIR)/file/file_path.c \
$(LIBRETRO_COMM_DIR)/file/file_path_io.c \
$(LIBRETRO_COMM_DIR)/file/archive_file.c \
$(LIBRETRO_COMM_DIR)/file/archive_file_zlib.c \
$(LIBRETRO_COMM_DIR)/string/stdstring.c \
$(LIBRETRO_COMM_DIR)/compat/compat_strl.c \
$(LIBRETRO_COMM_DIR)/compat/compat_posix_string.c \
$(LIBRETRO_COMM_DIR)/compat/compat_strcasestr.c \
$(LIBRETRO_COMM_DIR)/compat/fopen_utf8.c \
$(LIBRETRO_COMM_DIR)/features/features_cpu.c \
$(LIBRETRO_COMM_DIR)/time/rtime.c \
$(LIBRETRO_COMM_DIR)/lists/string_list.c
OBJS := $(SOURCES:.c=.o)
CFLAGS += -Wall -pedantic -std=gnu99 -O2 -I$(LIBRETRO_COMM_DIR)/include
LDFLAGS += -lz
ifneq ($(SANITIZER),)
CFLAGS := -fsanitize=$(SANITIZER) -fno-omit-frame-pointer -g $(CFLAGS)
LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS)
endif
all: $(TARGET)
%.o: %.c
$(CC) -c -o $@ $< $(CFLAGS)
$(TARGET): $(OBJS)
$(CC) -o $@ $^ $(LDFLAGS)
clean:
rm -f $(TARGET) $(OBJS)
.PHONY: clean
@@ -0,0 +1,261 @@
/* Benchmark for the screenshot-task edge I/O strategy introduced by the
* formats I/O decoupling work: whole-image in-memory encode followed by
* a single bulk filestream_write_file() versus the historical pattern
* of streaming the encode straight into the target file.
*
* The "old" modes faithfully replicate the pre-decoupling task flow so
* the comparison stays honest as the tree moves on:
*
* png_old : allocate a full-frame BGR24 buffer, flip-copy the source
* into it (what the scaler did for the no-resize case),
* then stream-encode into the target file.
* png_new : string-encode directly from the source with a negative
* stride (no copy), then one bulk write.
* bmp_old : header write plus one filestream_write() per row through
* an RFILE, matching the old rbmp_save_image().
* bmp_new : rbmp_save_image_string() plus one bulk write.
*
* Wall time per frame is reported; peak RSS (getrusage) is reported
* for single-mode runs, where the process-wide peak is attributable
* to one strategy. Point
* --out at slow or contended media (SD card, network mount, Android
* SAF-backed storage) to measure the syscall-pattern difference the
* redesign targets; on fast local filesystems stdio buffering hides
* most of it and the delta is dominated by the removed flip-copy.
*
* Usage:
* screenshot_io_bench [--mode all|png_old|png_new|bmp_old|bmp_new]
* [--pattern gradient|noise]
* [--width N] [--height N] [--iters N]
* [--out PATH]
*
* Build (see Makefile):
* make -C libretro-common/samples/formats/bench
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/resource.h>
#include <libretro.h>
#include <formats/rpng.h>
#include <formats/rbmp.h>
#include <streams/interface_stream.h>
#include <streams/file_stream.h>
static uint32_t prng_state = 0x1234567u;
static uint32_t prng(void)
{
prng_state ^= prng_state << 13;
prng_state ^= prng_state >> 17;
prng_state ^= prng_state << 5;
return prng_state;
}
static double now_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec * 1000.0 + ts.tv_nsec / 1e6;
}
static int run_png_old(const uint8_t *frame, unsigned w, unsigned h,
const char *path)
{
/* Old task flow: full-frame conversion buffer, flip-copy, then a
* streamed encode holding the file handle for the whole run. */
int ok = 0;
size_t frame_sz = (size_t)w * h * 3;
uint8_t *out = (uint8_t*)malloc(frame_sz);
unsigned j;
if (!out)
return 0;
for (j = 0; j < h; j++)
memcpy(out + (size_t)j * w * 3,
frame + (size_t)(h - 1 - j) * w * 3, (size_t)w * 3);
{
intfstream_t *f = intfstream_open_file(path,
RETRO_VFS_FILE_ACCESS_WRITE,
RETRO_VFS_FILE_ACCESS_HINT_NONE);
if (f)
{
ok = rpng_save_image_stream(out, f, w, h, (signed)(w * 3),
3, NULL);
intfstream_close(f);
free(f);
}
}
free(out);
return ok;
}
static int run_png_new(const uint8_t *frame, unsigned w, unsigned h,
const char *path)
{
uint64_t n = 0;
int ok = 0;
uint8_t *buf = rpng_save_image_bgr24_string(
frame + (size_t)(h - 1) * w * 3, w, h,
-(signed)(w * 3), &n);
if (buf)
{
ok = filestream_write_file(path, buf, (int64_t)n);
free(buf);
}
return ok;
}
static int run_bmp_old(const uint8_t *frame, unsigned w, unsigned h,
const char *path)
{
/* Old rbmp_save_image(): header write plus one filestream_write per
* row. The header content is irrelevant to the I/O pattern being
* measured, so a zeroed placeholder is written. */
uint8_t hdr[54];
unsigned j;
RFILE *f = filestream_open(path,
RETRO_VFS_FILE_ACCESS_WRITE,
RETRO_VFS_FILE_ACCESS_HINT_NONE);
if (!f)
return 0;
memset(hdr, 0, sizeof(hdr));
hdr[0] = 'B';
hdr[1] = 'M';
filestream_write(f, hdr, sizeof(hdr));
for (j = 0; j < h; j++)
filestream_write(f, frame + (size_t)j * w * 3, (size_t)w * 3);
filestream_close(f);
return 1;
}
static int run_bmp_new(const uint8_t *frame, unsigned w, unsigned h,
const char *path)
{
size_t n = 0;
int ok = 0;
uint8_t *buf = rbmp_save_image_string(frame, w, h, w * 3,
RBMP_SOURCE_TYPE_BGR24, &n);
if (buf)
{
ok = filestream_write_file(path, buf, (int64_t)n);
free(buf);
}
return ok;
}
typedef int (*bench_fn)(const uint8_t*, unsigned, unsigned, const char*);
static const struct
{
const char *name;
bench_fn fn;
} modes[] = {
{ "png_old", run_png_old },
{ "png_new", run_png_new },
{ "bmp_old", run_bmp_old },
{ "bmp_new", run_bmp_new },
};
int main(int argc, char **argv)
{
const char *mode = "all";
const char *pattern = "gradient";
const char *path = "/tmp/screenshot_io_bench.img";
unsigned w = 3840;
unsigned h = 2160;
int iters = 3;
size_t frame_sz;
uint8_t *frame;
size_t i;
int a;
for (a = 1; a + 1 < argc; a += 2)
{
if (!strcmp(argv[a], "--mode"))
mode = argv[a + 1];
else if (!strcmp(argv[a], "--pattern"))
pattern = argv[a + 1];
else if (!strcmp(argv[a], "--width"))
w = (unsigned)atoi(argv[a + 1]);
else if (!strcmp(argv[a], "--height"))
h = (unsigned)atoi(argv[a + 1]);
else if (!strcmp(argv[a], "--iters"))
iters = atoi(argv[a + 1]);
else if (!strcmp(argv[a], "--out"))
path = argv[a + 1];
else
{
fprintf(stderr, "unknown option %s\n", argv[a]);
return 1;
}
}
if (!w || !h || iters < 1)
{
fprintf(stderr, "invalid dimensions or iteration count\n");
return 1;
}
frame_sz = (size_t)w * h * 3;
frame = (uint8_t*)malloc(frame_sz);
if (!frame)
{
fprintf(stderr, "frame allocation failed\n");
return 1;
}
if (!strcmp(pattern, "noise"))
for (i = 0; i < frame_sz; i++)
frame[i] = (uint8_t)prng();
else
for (i = 0; i < frame_sz; i++)
frame[i] = (uint8_t)((i / 3) & 0xFF);
printf("%ux%u %s, %d iteration(s), out=%s\n",
w, h, pattern, iters, path);
for (i = 0; i < sizeof(modes) / sizeof(modes[0]); i++)
{
double total = 0.0;
double best = 0.0;
int it;
if (strcmp(mode, "all") && strcmp(mode, modes[i].name))
continue;
for (it = 0; it < iters; it++)
{
double t0 = now_ms();
double dt;
if (!modes[i].fn(frame, w, h, path))
{
fprintf(stderr, "%s: run failed\n", modes[i].name);
free(frame);
return 1;
}
dt = now_ms() - t0;
total += dt;
if (it == 0 || dt < best)
best = dt;
}
if (!strcmp(mode, "all"))
printf("%-8s avg %8.1f ms best %8.1f ms\n",
modes[i].name, total / iters, best);
else
{
/* getrusage peaks are process-wide, so RSS is only meaningful
* when a single mode ran in this process. */
struct rusage ru;
getrusage(RUSAGE_SELF, &ru);
printf("%-8s avg %8.1f ms best %8.1f ms peakRSS %7.1f MiB\n",
modes[i].name, total / iters, best,
ru.ru_maxrss / 1024.0);
}
}
filestream_delete(path);
free(frame);
return 0;
}
+38 -2
View File
@@ -1,4 +1,5 @@
TARGET := rbmp_test
TARGET_TEST2 := rbmp_encode_test
LIBRETRO_COMM_DIR := ../../..
@@ -6,7 +7,39 @@ SOURCES := \
rbmp_test.c \
$(LIBRETRO_COMM_DIR)/formats/bmp/rbmp.c
# The encode round-trip test drives the pure encoder, the memory-stream
# variant and the path adapter, so it needs the intfstream/file stack.
TEST2_SOURCES := \
rbmp_encode_test.c \
$(LIBRETRO_COMM_DIR)/formats/bmp/rbmp.c \
$(LIBRETRO_COMM_DIR)/formats/bmp/rbmp_encode.c \
$(LIBRETRO_COMM_DIR)/formats/bmp/rbmp_file.c \
$(LIBRETRO_COMM_DIR)/streams/interface_stream.c \
$(LIBRETRO_COMM_DIR)/streams/memory_stream.c \
$(LIBRETRO_COMM_DIR)/streams/file_stream.c \
$(LIBRETRO_COMM_DIR)/streams/rzip_stream.c \
$(LIBRETRO_COMM_DIR)/streams/trans_stream.c \
$(LIBRETRO_COMM_DIR)/streams/trans_stream_pipe.c \
$(LIBRETRO_COMM_DIR)/streams/trans_stream_deflate.c \
$(LIBRETRO_COMM_DIR)/encodings/encoding_deflate.c \
$(LIBRETRO_COMM_DIR)/encodings/encoding_crc32.c \
$(LIBRETRO_COMM_DIR)/encodings/encoding_utf.c \
$(LIBRETRO_COMM_DIR)/vfs/vfs_implementation.c \
$(LIBRETRO_COMM_DIR)/file/file_path.c \
$(LIBRETRO_COMM_DIR)/file/file_path_io.c \
$(LIBRETRO_COMM_DIR)/file/archive_file.c \
$(LIBRETRO_COMM_DIR)/file/archive_file_zlib.c \
$(LIBRETRO_COMM_DIR)/string/stdstring.c \
$(LIBRETRO_COMM_DIR)/compat/compat_strl.c \
$(LIBRETRO_COMM_DIR)/compat/compat_posix_string.c \
$(LIBRETRO_COMM_DIR)/compat/compat_strcasestr.c \
$(LIBRETRO_COMM_DIR)/compat/fopen_utf8.c \
$(LIBRETRO_COMM_DIR)/features/features_cpu.c \
$(LIBRETRO_COMM_DIR)/time/rtime.c \
$(LIBRETRO_COMM_DIR)/lists/string_list.c
OBJS := $(SOURCES:.c=.o)
TEST2_OBJS := $(TEST2_SOURCES:.c=.o)
CFLAGS += -Wall -pedantic -std=gnu99 -g -I$(LIBRETRO_COMM_DIR)/include
@@ -20,7 +53,7 @@ ifneq ($(SANITIZER),)
endif
all: $(TARGET)
all: $(TARGET) $(TARGET_TEST2)
%.o: %.c
$(CC) -c -o $@ $< $(CFLAGS)
@@ -28,7 +61,10 @@ all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) -o $@ $^ $(LDFLAGS)
$(TARGET_TEST2): $(TEST2_OBJS)
$(CC) -o $@ $^ $(LDFLAGS) -lz
clean:
rm -f $(TARGET) $(OBJS)
rm -f $(TARGET) $(TARGET_TEST2) $(OBJS) $(TEST2_OBJS)
.PHONY: clean
@@ -0,0 +1,441 @@
/* Regression tests for the pure BMP encoder split
* (libretro-common/formats/bmp/rbmp_encode.c + rbmp_file.c):
*
* - rbmp_save_image_string(): exact-size buffer encode for every
* source type, positive and negative pitch, unaligned widths
* (row padding), header field sanity.
* - rbmp_save_image_stream(): byte-identical output to the string
* encoder through a memory intfstream.
* - rbmp_save_image(): the deprecated path adapter writes a file
* byte-identical to the string encoder's buffer.
* - Round-trips through the in-tree decoder (rbmp_process_image)
* with pixel-exact comparison, including the decoder's bottom-up
* flip and the encoder's RGB565 -> 888 expansion.
*
* Build (see Makefile):
* make -C libretro-common/samples/formats/bmp rbmp_encode_test
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libretro.h>
#include <formats/rbmp.h>
#include <formats/image.h>
#include <streams/interface_stream.h>
#include <streams/file_stream.h>
static int failures = 0;
#define CHECK(cond, name) \
do { \
if (cond) \
printf("[SUCCESS] %s\n", name); \
else \
{ \
printf("[ERROR] %s (line %d)\n", name, __LINE__); \
failures++; \
} \
} while (0)
static uint32_t prng_state = 0x12345678u;
static uint32_t prng(void)
{
prng_state ^= prng_state << 13;
prng_state ^= prng_state >> 17;
prng_state ^= prng_state << 5;
return prng_state;
}
/* Decode a BMP held in memory with the in-tree decoder. Returns the
* malloc'd ARGB pixel array (top-down) or NULL. */
static uint32_t *decode_bmp(const uint8_t *bmp, size_t _len,
unsigned *w, unsigned *h)
{
void *out = NULL;
rbmp_t *rbmp = rbmp_alloc();
if (!rbmp)
return NULL;
rbmp_set_buf_ptr(rbmp, (void*)bmp);
{
/* rbmp_process_image decodes incrementally; pump it. */
int rc;
do
{
rc = rbmp_process_image(rbmp, &out, _len, w, h, false);
} while (rc == IMAGE_PROCESS_NEXT);
if (rc != IMAGE_PROCESS_END)
{
free(out);
out = NULL;
}
}
rbmp_free(rbmp);
return (uint32_t*)out;
}
/* Encode the same frame through the stream API into a memory stream
* and require byte identity with the string encoder's output. */
static void check_stream_matches_string(const uint8_t *golden,
size_t golden_len, const void *frame,
unsigned width, unsigned height, unsigned pitch,
enum rbmp_source_type type, const char *name)
{
bool ok = false;
uint8_t *buf = (uint8_t*)malloc(golden_len);
intfstream_t *intf_s = NULL;
if (buf)
{
intf_s = intfstream_open_memory(buf,
RETRO_VFS_FILE_ACCESS_WRITE,
RETRO_VFS_FILE_ACCESS_HINT_NONE,
golden_len);
if (intf_s)
{
ok = rbmp_save_image_stream(intf_s, frame,
width, height, pitch, type);
ok = ok && (intfstream_get_ptr(intf_s) == (int64_t)golden_len);
ok = ok && !memcmp(buf, golden, golden_len);
intfstream_close(intf_s);
free(intf_s);
}
free(buf);
}
CHECK(ok, name);
}
static void test_xrgb888_roundtrip(void)
{
unsigned w = 31, h = 17; /* unaligned width: 3-byte row padding */
unsigned dw = 0, dh = 0;
size_t _len = 0;
size_t i, j;
int ok;
uint32_t *frame = (uint32_t*)malloc((size_t)w * h * 4);
uint8_t *bmp = NULL;
uint32_t *px = NULL;
if (!frame)
return;
for (i = 0; i < (size_t)w * h; i++)
frame[i] = prng() & 0x00FFFFFFu;
bmp = rbmp_save_image_string(frame, w, h, w * 4,
RBMP_SOURCE_TYPE_XRGB888, &_len);
CHECK(bmp != NULL, "xrgb888: string encode");
if (!bmp)
{
free(frame);
return;
}
/* 24bpp: line = (31*3 + 3) & ~3 = 96, file = 54 + 96*17 */
CHECK(_len == 54 + 96 * 17, "xrgb888: exact output size");
CHECK(bmp[0] == 'B' && bmp[1] == 'M' && bmp[10] == 54
&& bmp[28] == 24, "xrgb888: header fields");
/* padding bytes of the first row must be deterministic zeros */
CHECK(bmp[54 + 93] == 0 && bmp[54 + 94] == 0 && bmp[54 + 95] == 0,
"xrgb888: row padding zeroed");
px = decode_bmp(bmp, _len, &dw, &dh);
CHECK(px != NULL && dw == w && dh == h, "xrgb888: decode");
ok = (px != NULL && dw == w && dh == h);
/* Positive pitch stores rows in walk order; BMP is bottom-up, so
* decoded row j is frame row h-1-j. */
for (j = 0; ok && j < h; j++)
for (i = 0; ok && i < w; i++)
if ((px[j * w + i] & 0x00FFFFFFu)
!= (frame[(h - 1 - j) * w + i] & 0x00FFFFFFu))
ok = 0;
CHECK(ok, "xrgb888: pixel round-trip (bottom-up)");
free(px);
check_stream_matches_string(bmp, _len, frame, w, h, w * 4,
RBMP_SOURCE_TYPE_XRGB888, "xrgb888: stream == string");
/* Negative pitch: hand in the last row and walk upwards; the
* decoded image must now equal the frame top-down. */
{
size_t _len2 = 0;
uint8_t *bmp2 = rbmp_save_image_string(
(const uint8_t*)frame + (size_t)(h - 1) * w * 4,
w, h, (unsigned)-(int)(w * 4),
RBMP_SOURCE_TYPE_XRGB888, &_len2);
CHECK(bmp2 != NULL && _len2 == _len, "xrgb888: negative pitch encode");
if (bmp2)
{
px = decode_bmp(bmp2, _len2, &dw, &dh);
ok = (px != NULL && dw == w && dh == h);
for (j = 0; ok && j < h; j++)
for (i = 0; ok && i < w; i++)
if ((px[j * w + i] & 0x00FFFFFFu)
!= (frame[j * w + i] & 0x00FFFFFFu))
ok = 0;
CHECK(ok, "xrgb888: pixel round-trip (top-down via negative pitch)");
free(px);
free(bmp2);
}
}
free(bmp);
free(frame);
}
static void test_rgb565_roundtrip(void)
{
unsigned w = 13, h = 7;
unsigned dw = 0, dh = 0;
size_t _len = 0;
size_t i, j;
int ok;
uint16_t *frame = (uint16_t*)malloc((size_t)w * h * 2);
uint8_t *bmp = NULL;
uint32_t *px = NULL;
if (!frame)
return;
for (i = 0; i < (size_t)w * h; i++)
frame[i] = (uint16_t)prng();
bmp = rbmp_save_image_string(frame, w, h, w * 2,
RBMP_SOURCE_TYPE_RGB565, &_len);
CHECK(bmp != NULL, "rgb565: string encode");
if (!bmp)
{
free(frame);
return;
}
px = decode_bmp(bmp, _len, &dw, &dh);
ok = (px != NULL && dw == w && dh == h);
for (j = 0; ok && j < h; j++)
{
for (i = 0; ok && i < w; i++)
{
uint16_t p = frame[(h - 1 - j) * w + i];
uint8_t b5 = (p >> 0) & 0x1f;
uint8_t g6 = (p >> 5) & 0x3f;
uint8_t r5 = (p >> 11) & 0x1f;
uint32_t expect = 0
| ((uint32_t)((r5 << 3) | (r5 >> 2)) << 16)
| ((uint32_t)((g6 << 2) | (g6 >> 4)) << 8)
| ((uint32_t)((b5 << 3) | (b5 >> 2)) << 0);
if ((px[j * w + i] & 0x00FFFFFFu) != expect)
ok = 0;
}
}
CHECK(ok, "rgb565: pixel round-trip with 565 expansion");
free(px);
check_stream_matches_string(bmp, _len, frame, w, h, w * 2,
RBMP_SOURCE_TYPE_RGB565, "rgb565: stream == string");
free(bmp);
free(frame);
}
static void test_bgr24_roundtrip(void)
{
unsigned w = 3, h = 5; /* 9-byte rows, 3 bytes padding */
unsigned dw = 0, dh = 0;
size_t _len = 0;
size_t i, j;
int ok;
/* Tightly-sized source: exactly w*3 per row, no slack, so any
* stride overread trips ASan. */
uint8_t *frame = (uint8_t*)malloc((size_t)w * h * 3);
uint8_t *bmp = NULL;
uint32_t *px = NULL;
if (!frame)
return;
for (i = 0; i < (size_t)w * h * 3; i++)
frame[i] = (uint8_t)prng();
bmp = rbmp_save_image_string(frame, w, h, w * 3,
RBMP_SOURCE_TYPE_BGR24, &_len);
CHECK(bmp != NULL && _len == 54 + 12 * 5, "bgr24: unaligned encode");
if (!bmp)
{
free(frame);
return;
}
CHECK(bmp[54 + 9] == 0 && bmp[54 + 10] == 0 && bmp[54 + 11] == 0,
"bgr24: padding zeroed");
px = decode_bmp(bmp, _len, &dw, &dh);
ok = (px != NULL && dw == w && dh == h);
for (j = 0; ok && j < h; j++)
{
for (i = 0; ok && i < w; i++)
{
const uint8_t *s = frame + ((h - 1 - j) * w + i) * 3;
uint32_t expect = ((uint32_t)s[2] << 16)
| ((uint32_t)s[1] << 8) | s[0];
if ((px[j * w + i] & 0x00FFFFFFu) != expect)
ok = 0;
}
}
CHECK(ok, "bgr24: pixel round-trip");
free(px);
check_stream_matches_string(bmp, _len, frame, w, h, w * 3,
RBMP_SOURCE_TYPE_BGR24, "bgr24: stream == string");
free(bmp);
free(frame);
/* 4-byte-aligned width: stride == padded row size, which drives the
* stream encoder's whole-block single-write fast path. Its output
* must stay byte-identical to the string encoder. */
{
unsigned aw = 8, ah = 3;
uint8_t *aframe = (uint8_t*)malloc((size_t)aw * ah * 3);
if (aframe)
{
size_t alen = 0;
uint8_t *abmp;
for (i = 0; i < (size_t)aw * ah * 3; i++)
aframe[i] = (uint8_t)prng();
abmp = rbmp_save_image_string(aframe, aw, ah, aw * 3,
RBMP_SOURCE_TYPE_BGR24, &alen);
CHECK(abmp != NULL && alen == 54 + (size_t)aw * 3 * ah,
"bgr24 aligned: exact size, no padding");
if (abmp)
{
check_stream_matches_string(abmp, alen, aframe, aw, ah,
aw * 3, RBMP_SOURCE_TYPE_BGR24,
"bgr24 aligned: whole-block stream == string");
free(abmp);
}
free(aframe);
}
}
}
static void test_argb8888_roundtrip(void)
{
unsigned w = 8, h = 4;
unsigned dw = 0, dh = 0;
size_t _len = 0;
size_t i, j;
int ok;
uint32_t *frame = (uint32_t*)malloc((size_t)w * h * 4);
uint8_t *bmp = NULL;
uint32_t *px = NULL;
if (!frame)
return;
for (i = 0; i < (size_t)w * h; i++)
frame[i] = 0xFF000000u | (prng() & 0x00FFFFFFu);
bmp = rbmp_save_image_string(frame, w, h, w * 4,
RBMP_SOURCE_TYPE_ARGB8888, &_len);
CHECK(bmp != NULL && _len == 54 + (size_t)w * 4 * h,
"argb8888: exact 32bpp size");
if (!bmp)
{
free(frame);
return;
}
CHECK(bmp[28] == 32, "argb8888: 32bpp header");
px = decode_bmp(bmp, _len, &dw, &dh);
ok = (px != NULL && dw == w && dh == h);
for (j = 0; ok && j < h; j++)
for (i = 0; ok && i < w; i++)
if ((px[j * w + i] & 0x00FFFFFFu)
!= (frame[(h - 1 - j) * w + i] & 0x00FFFFFFu))
ok = 0;
CHECK(ok, "argb8888: pixel round-trip");
free(px);
check_stream_matches_string(bmp, _len, frame, w, h, w * 4,
RBMP_SOURCE_TYPE_ARGB8888, "argb8888: stream == string");
free(bmp);
free(frame);
}
/* The deprecated path adapter must produce a file byte-identical to
* the string encoder's buffer. */
static void test_path_adapter(void)
{
unsigned w = 16, h = 9;
size_t _len = 0;
size_t i;
int ok = 0;
const char *path = "/tmp/rbmp_encode_test.bmp";
uint8_t *frame = (uint8_t*)malloc((size_t)w * h * 3);
uint8_t *bmp = NULL;
if (!frame)
return;
for (i = 0; i < (size_t)w * h * 3; i++)
frame[i] = (uint8_t)prng();
bmp = rbmp_save_image_string(frame, w, h, w * 3,
RBMP_SOURCE_TYPE_BGR24, &_len);
if (bmp && rbmp_save_image(path, frame, w, h, w * 3,
RBMP_SOURCE_TYPE_BGR24))
{
int64_t file_len = 0;
uint8_t *file = NULL;
if (filestream_read_file(path, (void**)&file, &file_len))
{
ok = (file_len == (int64_t)_len)
&& !memcmp(file, bmp, _len);
free(file);
}
filestream_delete(path);
}
CHECK(ok, "path adapter: file == string encoder output");
free(bmp);
free(frame);
}
static void test_invalid_args(void)
{
size_t _len = 0;
uint8_t px[4] = {0, 0, 0, 0};
CHECK(!rbmp_save_image_string(NULL, 1, 1, 4,
RBMP_SOURCE_TYPE_ARGB8888, &_len),
"invalid: NULL frame rejected");
CHECK(!rbmp_save_image_string(px, 0, 1, 4,
RBMP_SOURCE_TYPE_ARGB8888, &_len),
"invalid: zero width rejected");
CHECK(!rbmp_save_image_string(px, 1, 0, 4,
RBMP_SOURCE_TYPE_ARGB8888, &_len),
"invalid: zero height rejected");
CHECK(!rbmp_save_image_string(px, 1, 1, 4,
RBMP_SOURCE_TYPE_ARGB8888, NULL),
"invalid: NULL out_len rejected");
CHECK(!rbmp_save_image_stream(NULL, px, 1, 1, 4,
RBMP_SOURCE_TYPE_ARGB8888),
"invalid: NULL stream rejected");
}
int main(void)
{
test_xrgb888_roundtrip();
test_rgb565_roundtrip();
test_bgr24_roundtrip();
test_argb8888_roundtrip();
test_path_adapter();
test_invalid_args();
if (failures)
{
printf("%d FAILURE(S)\n", failures);
return 1;
}
printf("ALL TESTS PASSED\n");
return 0;
}
+42 -4
View File
@@ -1,17 +1,52 @@
TARGET := cdfs_dir_record_test
TARGET_TEST2 := cdfs_pure_api_test
LIBRETRO_COMM_DIR := ../../..
SOURCES := cdfs_dir_record_test.c
OBJS := $(SOURCES:.c=.o)
# The pure-API test links the real cdfs.c and drives it entirely over
# memory streams, so it needs the intfstream stack (which in turn pulls
# in the file/rzip/deflate backends it can dispatch to at runtime).
TEST2_SOURCES := \
cdfs_pure_api_test.c \
$(LIBRETRO_COMM_DIR)/formats/cdfs/cdfs.c \
$(LIBRETRO_COMM_DIR)/streams/interface_stream.c \
$(LIBRETRO_COMM_DIR)/streams/memory_stream.c \
$(LIBRETRO_COMM_DIR)/streams/file_stream.c \
$(LIBRETRO_COMM_DIR)/streams/rzip_stream.c \
$(LIBRETRO_COMM_DIR)/streams/trans_stream.c \
$(LIBRETRO_COMM_DIR)/streams/trans_stream_pipe.c \
$(LIBRETRO_COMM_DIR)/streams/trans_stream_deflate.c \
$(LIBRETRO_COMM_DIR)/encodings/encoding_deflate.c \
$(LIBRETRO_COMM_DIR)/encodings/encoding_crc32.c \
$(LIBRETRO_COMM_DIR)/encodings/encoding_utf.c \
$(LIBRETRO_COMM_DIR)/vfs/vfs_implementation.c \
$(LIBRETRO_COMM_DIR)/file/file_path.c \
$(LIBRETRO_COMM_DIR)/file/file_path_io.c \
$(LIBRETRO_COMM_DIR)/file/archive_file.c \
$(LIBRETRO_COMM_DIR)/file/archive_file_zlib.c \
$(LIBRETRO_COMM_DIR)/string/stdstring.c \
$(LIBRETRO_COMM_DIR)/compat/compat_strl.c \
$(LIBRETRO_COMM_DIR)/compat/compat_posix_string.c \
$(LIBRETRO_COMM_DIR)/compat/compat_strcasestr.c \
$(LIBRETRO_COMM_DIR)/compat/fopen_utf8.c \
$(LIBRETRO_COMM_DIR)/features/features_cpu.c \
$(LIBRETRO_COMM_DIR)/time/rtime.c \
$(LIBRETRO_COMM_DIR)/lists/string_list.c
CFLAGS += -Wall -pedantic -std=gnu99 -g -O0
OBJS := $(SOURCES:.c=.o)
TEST2_OBJS := $(TEST2_SOURCES:.c=.o)
CFLAGS += -Wall -pedantic -std=gnu99 -g -O0 -I$(LIBRETRO_COMM_DIR)/include
LDFLAGS += -lz
ifneq ($(SANITIZER),)
CFLAGS := -fsanitize=$(SANITIZER) -fno-omit-frame-pointer $(CFLAGS)
LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS)
endif
all: $(TARGET)
all: $(TARGET) $(TARGET_TEST2)
%.o: %.c
$(CC) -c -o $@ $< $(CFLAGS)
@@ -19,7 +54,10 @@ all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) -o $@ $^ $(LDFLAGS)
$(TARGET_TEST2): $(TEST2_OBJS)
$(CC) -o $@ $^ $(LDFLAGS)
clean:
rm -f $(TARGET) $(OBJS)
rm -f $(TARGET) $(TARGET_TEST2) $(OBJS) $(TEST2_OBJS)
.PHONY: clean
@@ -0,0 +1,477 @@
/* Regression tests for the pure (no path I/O) cdfs API:
*
* - cdfs_parse_cue(): cue-sheet parsing from a memory buffer,
* including quoted names, CRLF endings,
* tab separators, multi-track offset
* arithmetic, audio tracks, truncated
* buffers and missing-newline final lines.
* - cdfs_track_from_stream():wrapping an in-memory raw MODE1/2352
* image, ISO-9660 directory walk, file
* read and seek - end to end without a
* single filesystem touch.
*
* The whole test runs on memory streams, so it is a pure consumer of
* the split introduced by the formats I/O removal work and runs clean
* under ASan/UBSan/LSan.
*
* Build (see Makefile):
* make -C libretro-common/samples/formats/cdfs cdfs_pure_api_test
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libretro.h>
#include <formats/cdfs.h>
#include <streams/interface_stream.h>
static int failures = 0;
#define CHECK(cond, name) \
do { \
if (cond) \
printf("[SUCCESS] %s\n", name); \
else \
{ \
printf("[ERROR] %s (line %d)\n", name, __LINE__); \
failures++; \
} \
} while (0)
/* --------------------------------------------------------------------
* cdfs_parse_cue
* ------------------------------------------------------------------ */
static void test_parse_cue_single_track(void)
{
cdfs_cue_info_t info;
static const char cue[] =
"FILE \"game.bin\" BINARY\n"
" TRACK 01 MODE1/2352\n"
" INDEX 01 00:00:00\n";
CHECK(cdfs_parse_cue(cue, sizeof(cue) - 1, 1, &info),
"parse_cue: single track found");
CHECK(!strcmp(info.bin_name, "game.bin"),
"parse_cue: bin name extracted");
CHECK(info.track_index == 1, "parse_cue: track index");
CHECK(info.sector_size == 2352, "parse_cue: sector size");
CHECK(info.first_sector_offset == 0, "parse_cue: offset 0");
CHECK(info.first_sector_index == 0, "parse_cue: sector index 0");
/* track_index = 0 selects the first data track */
CHECK(cdfs_parse_cue(cue, sizeof(cue) - 1, 0, &info)
&& info.track_index == 1,
"parse_cue: track_index 0 finds first data track");
}
static void test_parse_cue_quoted_name_with_spaces(void)
{
cdfs_cue_info_t info;
static const char cue[] =
"FILE \"my game (disc 1).bin\" BINARY\n"
"TRACK 01 MODE2/2352\n"
"INDEX 01 00:00:00\n";
CHECK(cdfs_parse_cue(cue, sizeof(cue) - 1, 0, &info)
&& !strcmp(info.bin_name, "my game (disc 1).bin"),
"parse_cue: quoted name with spaces");
CHECK(info.sector_size == 2352, "parse_cue: MODE2/2352 size");
}
static void test_parse_cue_second_data_track_offset(void)
{
cdfs_cue_info_t info;
static const char cue[] =
"FILE \"disc.bin\" BINARY\n"
" TRACK 01 MODE1/2352\n"
" INDEX 01 00:00:00\n"
" TRACK 02 MODE2/2352\n"
" INDEX 01 00:02:00\n";
CHECK(cdfs_parse_cue(cue, sizeof(cue) - 1, 2, &info),
"parse_cue: second data track found");
/* 00:02:00 = 150 sectors of 2352 bytes into the same bin. */
CHECK(info.first_sector_offset == 150 * 2352,
"parse_cue: second track byte offset");
CHECK(info.first_sector_index == 150,
"parse_cue: second track sector index");
CHECK(info.sector_size == 2352, "parse_cue: second track size");
}
static void test_parse_cue_audio_pregap_arithmetic(void)
{
cdfs_cue_info_t info;
/* Data track behind an audio track with a pregap (INDEX 00):
* the data track's INDEX 01 sits at 02:00:00 = 9000 sectors. */
static const char cue[] =
"FILE \"mixed.bin\" BINARY\n"
" TRACK 01 AUDIO\n"
" INDEX 01 00:00:00\n"
" TRACK 02 MODE1/2352\n"
" INDEX 00 01:58:00\n"
" INDEX 01 02:00:00\n";
CHECK(cdfs_parse_cue(cue, sizeof(cue) - 1, 0, &info),
"parse_cue: data track behind audio found");
CHECK(info.track_index == 2, "parse_cue: audio track skipped");
CHECK(info.first_sector_index == 9000,
"parse_cue: pregap sector arithmetic");
CHECK(info.first_sector_offset == 9000 * 2352,
"parse_cue: pregap byte arithmetic");
}
static void test_parse_cue_audio_only_not_found(void)
{
cdfs_cue_info_t info;
static const char cue[] =
"FILE \"cdda.bin\" BINARY\n"
"TRACK 01 AUDIO\n"
"INDEX 01 00:00:00\n";
CHECK(!cdfs_parse_cue(cue, sizeof(cue) - 1, 0, &info),
"parse_cue: audio-only cue yields no data track");
}
static void test_parse_cue_crlf_tabs_no_trailing_newline(void)
{
cdfs_cue_info_t info;
/* CRLF endings, tab separators, and no newline on the last line. */
static const char cue[] =
"FILE\t\"crlf.bin\"\tBINARY\r\n"
"TRACK\t01\tMODE1/2048\r\n"
"INDEX\t01\t00:00:00";
CHECK(cdfs_parse_cue(cue, sizeof(cue) - 1, 1, &info),
"parse_cue: CRLF + tabs + missing final newline");
CHECK(!strcmp(info.bin_name, "crlf.bin"),
"parse_cue: CRLF bin name");
CHECK(info.sector_size == 2048, "parse_cue: MODE1/2048 size");
}
static void test_parse_cue_hostile_inputs(void)
{
cdfs_cue_info_t info;
static const char cue[] =
"FILE \"game.bin\" BINARY\n"
"TRACK 01 MODE1/2352\n"
"INDEX 01 00:00:00\n";
size_t i;
/* Every truncation of a valid cue must parse without reading past
* the buffer (ASan-checked) and without crashing. A heap copy of
* exactly i bytes gives the sanitizer a real object boundary. */
for (i = 0; i < sizeof(cue) - 1; i++)
{
char *partial = (char*)malloc(i ? i : 1);
if (!partial)
continue;
memcpy(partial, cue, i);
(void)cdfs_parse_cue(partial, i, 1, &info);
free(partial);
}
printf("[SUCCESS] parse_cue: all truncations handled\n");
CHECK(!cdfs_parse_cue(NULL, 0, 1, &info), "parse_cue: NULL text");
CHECK(!cdfs_parse_cue(cue, sizeof(cue) - 1, 1, NULL),
"parse_cue: NULL out");
CHECK(!cdfs_parse_cue("", 0, 1, &info), "parse_cue: empty text");
/* A found track with no preceding FILE line must fail cleanly. */
{
static const char nofile[] =
"TRACK 01 MODE1/2352\nINDEX 01 00:00:00\n";
CHECK(!cdfs_parse_cue(nofile, sizeof(nofile) - 1, 1, &info),
"parse_cue: INDEX without FILE rejected");
}
}
/* --------------------------------------------------------------------
* cdfs_track_from_stream over an in-memory raw MODE1/2352 image
* ------------------------------------------------------------------ */
#define TEST_SECTOR_SIZE 2352
#define TEST_HEADER_SIZE 16
#define TEST_NUM_SECTORS 22
#define TEST_ROOT_SECTOR 17
#define TEST_FILE_SECTOR 18
#define TEST_BIG_SECTOR 19
#define TEST_BIG_SIZE 5000 /* spans three 2048-byte sectors */
static const char test_file_content[] =
"Hello from a purely in-memory ISO-9660 track!\n";
static uint8_t *build_raw_iso(size_t *out_len)
{
size_t _len = (size_t)TEST_NUM_SECTORS * TEST_SECTOR_SIZE;
uint8_t *img = (uint8_t*)calloc(1, _len);
uint8_t *data;
uint8_t *rec;
size_t fn_len;
uint32_t fsize = (uint32_t)(sizeof(test_file_content) - 1);
int s;
if (!img)
return NULL;
/* 12-byte sync pattern + 3-byte address + mode byte per sector. */
for (s = 0; s < TEST_NUM_SECTORS; s++)
{
uint8_t *sec = img + (size_t)s * TEST_SECTOR_SIZE;
sec[0] = 0;
memset(sec + 1, 0xFF, 10);
sec[11] = 0;
sec[15] = 1; /* MODE1 */
}
/* Sector 16: primary volume descriptor. "CD001" lands 17 bytes into
* the raw sector (1 byte into the data area), which is the layout
* cdfs_determine_sector_size probes for a 16-byte-header source. */
data = img + 16 * TEST_SECTOR_SIZE + TEST_HEADER_SIZE;
data[0] = 1;
memcpy(data + 1, "CD001", 5);
/* Root directory record at data offset 156; bytes 2..4 hold the
* root directory's extent sector (little-endian 24-bit). */
data[156 + 2] = TEST_ROOT_SECTOR & 0xFF;
data[156 + 3] = (TEST_ROOT_SECTOR >> 8) & 0xFF;
data[156 + 4] = (TEST_ROOT_SECTOR >> 16) & 0xFF;
/* Sector 17: root directory with records for README.TXT;1 and the
* multi-sector BIG.DAT;1 */
rec = img + TEST_ROOT_SECTOR * TEST_SECTOR_SIZE + TEST_HEADER_SIZE;
fn_len = strlen("README.TXT;1");
rec[0] = (uint8_t)(33 + fn_len + 1); /* record length */
rec[2] = TEST_FILE_SECTOR & 0xFF; /* extent sector, LE24 */
rec[3] = (TEST_FILE_SECTOR >> 8) & 0xFF;
rec[4] = (TEST_FILE_SECTOR >> 16) & 0xFF;
rec[10] = (uint8_t)(fsize & 0xFF); /* data length, LE32 */
rec[11] = (uint8_t)((fsize >> 8) & 0xFF);
rec[12] = (uint8_t)((fsize >> 16) & 0xFF);
rec[13] = (uint8_t)((fsize >> 24) & 0xFF);
rec[32] = (uint8_t)fn_len;
memcpy(rec + 33, "README.TXT;1", fn_len);
rec = rec + rec[0];
fn_len = strlen("BIG.DAT;1");
rec[0] = (uint8_t)(33 + fn_len + 1);
rec[2] = TEST_BIG_SECTOR & 0xFF;
rec[3] = (TEST_BIG_SECTOR >> 8) & 0xFF;
rec[4] = (TEST_BIG_SECTOR >> 16) & 0xFF;
rec[10] = (uint8_t)(TEST_BIG_SIZE & 0xFF);
rec[11] = (uint8_t)((TEST_BIG_SIZE >> 8) & 0xFF);
rec[12] = (uint8_t)((TEST_BIG_SIZE >> 16) & 0xFF);
rec[13] = (uint8_t)((TEST_BIG_SIZE >> 24) & 0xFF);
rec[32] = (uint8_t)fn_len;
memcpy(rec + 33, "BIG.DAT;1", fn_len);
/* Sector 18: README.TXT contents. */
memcpy(img + TEST_FILE_SECTOR * TEST_SECTOR_SIZE + TEST_HEADER_SIZE,
test_file_content, fsize);
/* Sectors 19-21: BIG.DAT, a deterministic byte pattern spanning
* three cooked sectors so sector-crossing reads and seeks are
* exercised. */
for (s = 0; s < 3; s++)
{
uint8_t *dst = img
+ (size_t)(TEST_BIG_SECTOR + s) * TEST_SECTOR_SIZE
+ TEST_HEADER_SIZE;
size_t remain = TEST_BIG_SIZE - (size_t)s * 2048;
size_t chunk = (remain < 2048) ? remain : 2048;
size_t i;
for (i = 0; i < chunk; i++)
dst[i] = (uint8_t)(((size_t)s * 2048 + i) * 31 + 7);
}
*out_len = _len;
return img;
}
static void test_track_from_stream_iso(void)
{
size_t img_len = 0;
uint8_t *img = build_raw_iso(&img_len);
intfstream_t *stream = NULL;
cdfs_track_t *track = NULL;
cdfs_file_t file;
char buf[128];
int64_t n;
if (!img)
{
printf("[ERROR] ISO image allocation failed\n");
failures++;
return;
}
stream = intfstream_open_memory(img,
RETRO_VFS_FILE_ACCESS_READ,
RETRO_VFS_FILE_ACCESS_HINT_NONE,
img_len);
track = cdfs_track_from_stream(stream, 0, 0);
CHECK(track != NULL, "track_from_stream: wrap succeeds");
if (!track)
{
free(img);
return;
}
CHECK(track->stream_sector_size == TEST_SECTOR_SIZE,
"track_from_stream: sector size probed from CD001");
CHECK(track->stream_sector_header_size == TEST_HEADER_SIZE,
"track_from_stream: header size probed from CD001");
CHECK(cdfs_open_file(&file, track, "README.TXT") != 0,
"cdfs_open_file: directory walk finds file");
CHECK(cdfs_get_size(&file) ==
(int64_t)(sizeof(test_file_content) - 1),
"cdfs_get_size: matches directory record");
memset(buf, 0, sizeof(buf));
n = cdfs_read_file(&file, buf, sizeof(buf));
CHECK(n == (int64_t)(sizeof(test_file_content) - 1),
"cdfs_read_file: full read length");
CHECK(!memcmp(buf, test_file_content, (size_t)n),
"cdfs_read_file: contents round-trip");
/* Seek back into the middle and re-read. */
CHECK(cdfs_seek(&file, 6, SEEK_SET) == 0, "cdfs_seek: SEEK_SET");
memset(buf, 0, sizeof(buf));
n = cdfs_read_file(&file, buf, 4);
CHECK(n == 4 && !memcmp(buf, "from", 4),
"cdfs_read_file: read after seek");
cdfs_close_file(&file);
/* Missing file must not be found. */
CHECK(cdfs_open_file(&file, track, "MISSING.BIN") == 0,
"cdfs_open_file: absent file rejected");
/* NULL path opens the raw cooked view of the track. */
CHECK(cdfs_open_file(&file, track, NULL) == 1,
"cdfs_open_file: raw (NULL path) open");
CHECK(cdfs_get_size(&file) ==
(int64_t)TEST_NUM_SECTORS * 2048,
"cdfs_open_file: raw size = sectors * 2048");
cdfs_close_file(&file);
cdfs_close_track(track);
free(img);
printf("[SUCCESS] track_from_stream: track closed cleanly\n");
}
static void test_multi_sector_file(void)
{
size_t img_len = 0;
uint8_t *img = build_raw_iso(&img_len);
intfstream_t *stream = NULL;
cdfs_track_t *track = NULL;
cdfs_file_t file;
uint8_t *buf = NULL;
int64_t n;
size_t i;
int ok;
if (!img)
{
printf("[ERROR] ISO image allocation failed\n");
failures++;
return;
}
stream = intfstream_open_memory(img,
RETRO_VFS_FILE_ACCESS_READ,
RETRO_VFS_FILE_ACCESS_HINT_NONE,
img_len);
track = cdfs_track_from_stream(stream, 0, 0);
if (!track)
{
printf("[ERROR] track wrap failed\n");
failures++;
free(img);
return;
}
CHECK(cdfs_open_file(&file, track, "BIG.DAT") != 0,
"multi-sector: file found");
CHECK(cdfs_get_size(&file) == TEST_BIG_SIZE,
"multi-sector: size from record");
buf = (uint8_t*)malloc(TEST_BIG_SIZE);
if (buf)
{
/* Full sequential read across three sectors. */
n = cdfs_read_file(&file, buf, TEST_BIG_SIZE);
ok = (n == TEST_BIG_SIZE);
for (i = 0; ok && i < TEST_BIG_SIZE; i++)
if (buf[i] != (uint8_t)(i * 31 + 7))
ok = 0;
CHECK(ok, "multi-sector: sequential read pattern");
/* Seek into the third sector and read across nothing special;
* this drives the new_sector != current_sector branch of
* cdfs_seek with a non-zero first_sector. */
CHECK(cdfs_seek(&file, 4500, SEEK_SET) == 0,
"multi-sector: seek into third sector");
n = cdfs_read_file(&file, buf, 100);
ok = (n == 100);
for (i = 0; ok && i < 100; i++)
if (buf[i] != (uint8_t)((4500 + i) * 31 + 7))
ok = 0;
CHECK(ok, "multi-sector: read after cross-sector seek");
/* The read above is served entirely from the sector cache;
* cdfs_tell must still advance (SEEK_CUR below depends on it). */
CHECK(cdfs_tell(&file) == 4600,
"multi-sector: tell after cache-hit read");
/* Seek backwards with SEEK_CUR and read across the sector
* boundary at 2048. */
CHECK(cdfs_seek(&file, -(4600 - 2000), SEEK_CUR) == 0,
"multi-sector: relative seek back");
n = cdfs_read_file(&file, buf, 100);
ok = (n == 100);
for (i = 0; ok && i < 100; i++)
if (buf[i] != (uint8_t)((2000 + i) * 31 + 7))
ok = 0;
CHECK(ok, "multi-sector: read spanning sector boundary");
free(buf);
}
cdfs_close_file(&file);
cdfs_close_track(track);
free(img);
}
static void test_track_from_stream_null(void)
{
CHECK(cdfs_track_from_stream(NULL, 0, 0) == NULL,
"track_from_stream: NULL stream rejected");
}
int main(void)
{
test_parse_cue_single_track();
test_parse_cue_quoted_name_with_spaces();
test_parse_cue_second_data_track_offset();
test_parse_cue_audio_pregap_arithmetic();
test_parse_cue_audio_only_not_found();
test_parse_cue_crlf_tabs_no_trailing_newline();
test_parse_cue_hostile_inputs();
test_track_from_stream_iso();
test_multi_sector_file();
test_track_from_stream_null();
if (failures)
{
printf("%d FAILURE(S)\n", failures);
return 1;
}
printf("ALL TESTS PASSED\n");
return 0;
}
@@ -19,6 +19,7 @@ SOURCES_C := \
$(CORE_DIR)/rpng_test.c \
$(LIBRETRO_PNG_DIR)/rpng.c \
$(LIBRETRO_PNG_DIR)/rpng_encode.c \
$(LIBRETRO_PNG_DIR)/rpng_file.c \
$(LIBRETRO_COMM_DIR)/encodings/encoding_crc32.c \
$(LIBRETRO_COMM_DIR)/features/features_cpu.c \
$(LIBRETRO_COMM_DIR)/encodings/encoding_utf.c \
@@ -70,6 +71,7 @@ TEST2_SOURCES_C := \
$(CORE_DIR)/rpng_roundtrip_test.c \
$(LIBRETRO_PNG_DIR)/rpng.c \
$(LIBRETRO_PNG_DIR)/rpng_encode.c \
$(LIBRETRO_PNG_DIR)/rpng_file.c \
$(LIBRETRO_COMM_DIR)/encodings/encoding_crc32.c \
$(LIBRETRO_COMM_DIR)/features/features_cpu.c \
$(LIBRETRO_COMM_DIR)/encodings/encoding_utf.c \
@@ -39,7 +39,9 @@
#include <string.h>
#include <file/nbio.h>
#include <libretro.h>
#include <formats/rpng.h>
#include <streams/interface_stream.h>
#include <formats/image.h>
#include <encodings/crc32.h>
@@ -591,6 +593,137 @@ out:
free(got);
}
/* XRGB8888 / RGB565 sources: encode via rpng_save_image_stream_fmt
* (the exact call shape the screenshot task's direct fast paths use),
* load, compare. For RGB565 the expected 888 value uses the scaler's
* (c << 3) | (c >> 2) expansion, locking the encoder's conversion to
* the historical convert-then-encode output. */
static void test_fmt_roundtrip(const char *path, enum rpng_pixfmt fmt,
unsigned w, unsigned h, enum pattern pat, bool flip_bottom_up)
{
uint8_t *src = NULL;
uint32_t *got = NULL;
unsigned got_w = 0, got_h = 0;
unsigned x, y;
const unsigned src_bpp = (fmt == RPNG_PIXFMT_XRGB8888) ? 4 : 2;
const size_t stride = (size_t)w * src_bpp;
const char *fname = (fmt == RPNG_PIXFMT_XRGB8888)
? "xrgb8888" : "rgb565";
const char *pname = pattern_name(pat);
const char *orient = flip_bottom_up ? "bottom-up" : "top-down";
bool ok = false;
src = (uint8_t*)malloc(stride * h);
if (!src)
{
printf("[ERROR] %s %s %s %ux%u: OOM for source\n",
fname, pname, orient, w, h);
failures++;
return;
}
for (y = 0; y < h; y++)
{
for (x = 0; x < w; x++)
{
uint32_t px = sample_argb(pat, x, y, w, h);
if (fmt == RPNG_PIXFMT_XRGB8888)
{
uint32_t *p = (uint32_t*)(void*)(src + y * stride) + x;
*p = px & 0x00FFFFFFu;
}
else
{
/* Truncate 888 -> 565 (keep the top bits per channel). */
uint16_t *p = (uint16_t*)(void*)(src + y * stride) + x;
uint16_t r5 = (uint16_t)(((px >> 16) & 0xFF) >> 3);
uint16_t g6 = (uint16_t)(((px >> 8) & 0xFF) >> 2);
uint16_t b5 = (uint16_t)(((px >> 0) & 0xFF) >> 3);
*p = (uint16_t)((r5 << 11) | (g6 << 5) | b5);
}
}
}
{
intfstream_t *intf_s = intfstream_open_file(path,
RETRO_VFS_FILE_ACCESS_WRITE,
RETRO_VFS_FILE_ACCESS_HINT_NONE);
if (intf_s)
{
const uint8_t *base = flip_bottom_up
? src + (size_t)(h - 1) * stride
: src;
signed pitch = flip_bottom_up
? -(signed)stride
: (signed)stride;
ok = rpng_save_image_stream_fmt(base, intf_s,
w, h, pitch, fmt, NULL);
intfstream_close(intf_s);
free(intf_s);
}
}
if (!ok)
{
printf("[ERROR] %s %s %s %ux%u: save failed\n",
fname, pname, orient, w, h);
failures++;
goto out;
}
if (!structural_check_ok(path))
{
printf("[ERROR] %s %s %s %ux%u: structural check rejected output\n",
fname, pname, orient, w, h);
failures++;
}
if (!load_argb(path, &got, &got_w, &got_h)
|| got_w != w || got_h != h)
{
printf("[ERROR] %s %s %s %ux%u: load failed (%ux%u)\n",
fname, pname, orient, w, h, got_w, got_h);
failures++;
goto out;
}
for (y = 0; y < h; y++)
{
for (x = 0; x < w; x++)
{
unsigned src_y = flip_bottom_up ? (h - 1 - y) : y;
uint32_t px = sample_argb(pat, x, src_y, w, h);
uint32_t exp_rgb;
if (fmt == RPNG_PIXFMT_XRGB8888)
exp_rgb = px & 0x00FFFFFFu;
else
{
uint32_t r5 = ((px >> 16) & 0xFF) >> 3;
uint32_t g6 = ((px >> 8) & 0xFF) >> 2;
uint32_t b5 = ((px >> 0) & 0xFF) >> 3;
exp_rgb = (((r5 << 3) | (r5 >> 2)) << 16)
| (((g6 << 2) | (g6 >> 4)) << 8)
| (((b5 << 3) | (b5 >> 2)) << 0);
}
if ((got[y * w + x] & 0x00FFFFFFu) != exp_rgb)
{
printf("[ERROR] %s %s %s %ux%u: pixel (%u,%u) "
"exp=0x%06x got=0x%06x\n",
fname, pname, orient, w, h, x, y,
exp_rgb, got[y * w + x] & 0x00FFFFFFu);
failures++;
goto out;
}
}
}
printf("[SUCCESS] %s %s %s %ux%u round-trip\n",
fname, pname, orient, w, h);
out:
free(src);
free(got);
}
/* ---- harness ---- */
struct size_case { unsigned w, h; };
@@ -688,6 +821,14 @@ int main(int argc, char *argv[])
test_argb_roundtrip (path, sizes[i].w, sizes[i].h, (enum pattern)p);
test_bgr24_roundtrip(path, sizes[i].w, sizes[i].h, (enum pattern)p, false);
test_bgr24_roundtrip(path, sizes[i].w, sizes[i].h, (enum pattern)p, true);
test_fmt_roundtrip(path, RPNG_PIXFMT_XRGB8888,
sizes[i].w, sizes[i].h, (enum pattern)p, false);
test_fmt_roundtrip(path, RPNG_PIXFMT_XRGB8888,
sizes[i].w, sizes[i].h, (enum pattern)p, true);
test_fmt_roundtrip(path, RPNG_PIXFMT_RGB565,
sizes[i].w, sizes[i].h, (enum pattern)p, false);
test_fmt_roundtrip(path, RPNG_PIXFMT_RGB565,
sizes[i].w, sizes[i].h, (enum pattern)p, true);
}
}
+111 -17
View File
@@ -29,6 +29,8 @@
#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>
@@ -92,6 +94,72 @@ struct screenshot_task_state
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;
@@ -109,12 +177,13 @@ static bool screenshot_dump_direct(screenshot_task_state_t *state)
* trick as the BGR24 fast path. Never resampled. */
if (state->flags & SS_TASK_FLAG_HDR)
{
ret = rpng_save_image_rgb48_hdr(
ret = screenshot_save_png(
state->filename,
(const uint16_t*)input,
input,
state->out_width,
state->out_height,
(unsigned)(-state->pitch),
-state->pitch,
RPNG_PIXFMT_RGB48,
&state->hdr);
if (state->out_buffer)
free(state->out_buffer);
@@ -136,18 +205,43 @@ static bool screenshot_dump_direct(screenshot_task_state_t *state)
&& state->out_width == state->width
&& state->out_height == state->height)
{
ret = rpng_save_image_bgr24(
ret = screenshot_save_png(
state->filename,
input,
state->out_width,
state->out_height,
(unsigned)(-state->pitch)
);
-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)
@@ -169,13 +263,14 @@ static bool screenshot_dump_direct(screenshot_task_state_t *state)
scaler_ctx_gen_reset(&state->scaler);
ret = rpng_save_image_bgr24(
ret = screenshot_save_png(
state->filename,
state->out_buffer,
state->out_width,
state->out_height,
state->out_width * 3
);
(signed)(state->out_width * 3),
RPNG_PIXFMT_BGR24,
NULL);
free(state->out_buffer);
#elif defined(HAVE_RBMP)
@@ -186,7 +281,7 @@ static bool screenshot_dump_direct(screenshot_task_state_t *state)
else if (state->pixel_format_type == RETRO_PIXEL_FORMAT_XRGB8888)
bmp_type = RBMP_SOURCE_TYPE_XRGB888;
ret = rbmp_save_image(state->filename,
ret = screenshot_save_bmp(state->filename,
state->frame,
state->width,
state->height,
@@ -561,14 +656,13 @@ static bool screenshot_dump(
#if defined(HAVE_RPNG)
/* Only allocate the BGR24 output buffer when screenshot_dump_direct
* will actually use the scaler. When the source is already BGR24 at
* the output dimensions (typical of the viewport read-back path,
* take_screenshot_viewport), the encoder walks the source directly
* with negative pitch and no intermediate buffer is needed. The HDR
* path likewise encodes the source directly and never uses the scaler. */
* 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->flags & SS_TASK_FLAG_BGR24)
|| state->out_width != width
&& ( state->out_width != width
|| state->out_height != height))
{
if (!(buf = (uint8_t*)malloc(state->out_width * state->out_height * 3)))