mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-08 08:10:30 -10:00
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:
1 parent
6c38313bdd
commit
253c16e2b5
19 files changed
+2282
-292
No files matched your search
+3
-1
@@ -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 \
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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)))
|
||||
|
||||
Reference in new issue
Block a user