tools: mdec, a command-line MDEC encoder and decoder

Split out of #2101.

Co-authored-by: Rix <[email protected]>
This commit is contained in:
Nicolas 'Pixel' Noble
2026-09-27 22:16:32 -07:00
committed by Rix
co-authored by Rix
parent 6ace693b24
commit 257ffa3cc0
9 changed files with 877 additions and 1 deletions
+1
View File
@@ -44,6 +44,7 @@ bins/
/exe2elf
/exe2exe
/exe2iso
/mdec
/midi2psm
/midi2spd
/modconv
+1 -1
View File
@@ -206,7 +206,7 @@ SUPPORT_SRCS += third_party/ucl/src/n2e_99.c third_party/ucl/src/alloc.c third_p
SUPPORT_SRCS += $(wildcard third_party/iec-60908b/*.c)
LIBS := third_party/luajit/src/libluajit.a
TOOLS = authoring exe2elf exe2exe exe2iso midi2psm midi2spd modconv ps1-packer psyq-obj-parser
TOOLS = authoring exe2elf exe2exe exe2iso mdec midi2psm midi2spd modconv ps1-packer psyq-obj-parser
##############################################################################
+1
View File
@@ -91,6 +91,7 @@ steps:
!**\exe2elf.exe
!**\exe2exe.exe
!**\exe2iso.exe
!**\mdec.exe
!**\modconv.exe
!**\midi2psm.exe
!**\midi2spd.exe
+1
View File
@@ -98,6 +98,7 @@ steps:
!**\exe2elf.exe
!**\exe2exe.exe
!**\exe2iso.exe
!**\mdec.exe
!**\modconv.exe
!**\midi2psm.exe
!**\midi2spd.exe
+699
View File
@@ -0,0 +1,699 @@
/***************************************************************************
* Copyright (C) 2026 PCSX-Redux authors *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *
***************************************************************************/
// mdec - tooling for the PlayStation's Macroblock Decoder.
//
// ---------------------------------------------------------------------------
// CLI CONTRACT. Read this before adding a command.
//
// Usage is always: mdec <command> [options]
//
// 1. The command is the FIRST POSITIONAL argument, always, and never a flag.
// There is no default command: `mdec` with no command prints usage and
// exits non-zero. This is what lets commands be added without any existing
// invocation changing meaning.
// 2. `-i` is the input and `-o` is the output in EVERY command that has them.
// An option name never means two different things in two commands.
// 3. Options are scoped to their command. A command ignores nothing silently:
// an option it does not know is an error, not a no-op, because a silently
// ignored option is how a user ends up believing a table was applied.
// 4. `mdec -h` lists commands. `mdec <command> -h` describes one command.
// 5. An unknown command lists what exists and exits non-zero. It never falls
// back to a default.
//
// Commands so far: rawencode, bsencode, rawdecode. "raw" means the MDEC's own
// run-level halfword stream, the bytes DMA0 consumes, with no BS/VLC entropy
// layer and no STR framing; `bsencode` is the same stream VLC coded into Sony's
// legacy BS. STR framing gets its own command when it lands, rather than a flag
// on either of these, so that each one keeps meaning what it means today.
//
// ⛔ THERE IS NO COMPRESSING COMMAND AND THAT IS A MEASURED DECISION, not a gap.
// A ucl-nrv2e path existed here briefly and was removed 2026-09-18: measured over
// 245 real 320x240 video frames, per frame, BS is smaller on EVERY ONE of them at
// every q_scale, by 30-54%. It still wins by 4.6% against ucl given the whole
// sequence as one stream, which is not even a deployable shape. The decode-cost
// argument that might have rescued it does not survive either - Sony shipped games
// that decode BS at full rate on real hardware, so the VLC pass is affordable by
// existence proof.
//
// If compression ever comes back, it belongs on THIS side of the line and not in
// supportpsx: this tool is GPLv2, the library is MIT, and ucl's compressor is
// GPLv2. The library emits Container::Raw and stops for that reason.
// ---------------------------------------------------------------------------
#include <stdint.h>
#include <string.h>
#include <algorithm>
#include <exception>
#include <filesystem>
#include <string>
#include <vector>
#include "flags.h"
#include "fmt/format.h"
#include "json.hpp"
#include "supportpsx/dct.h"
#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb/stb_image.h"
#include "stb/stb_image_write.h"
namespace {
// Sony's own zscan[], FileFormat47 p.1-12, verified identical entry for entry.
// c_zscan[k] is the natural-order index of zigzag position k.
constexpr int c_zscan[64] = {
0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48,
41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23,
30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63,
};
// The scale matrix every game uploads via MDEC(3), Q14. This is the INVERSE
// transform; the forward one used for encoding is DCT::standardBasis().
constexpr int16_t c_standardScale[64] = {
0x5A82, 0x5A82, 0x5A82, 0x5A82, 0x5A82, 0x5A82, 0x5A82, 0x5A82,
0x7D8A, 0x6A6D, 0x471C, 0x18F8, (int16_t)0xE707, (int16_t)0xB8E3, (int16_t)0x9592, (int16_t)0x8275,
0x7641, 0x30FB, (int16_t)0xCF04, (int16_t)0x89BE, (int16_t)0x89BE, (int16_t)0xCF04, 0x30FB, 0x7641,
0x6A6D, (int16_t)0xE707, (int16_t)0x8275, (int16_t)0xB8E3, 0x471C, 0x7D8A, 0x18F8, (int16_t)0x9592,
0x5A82, (int16_t)0xA57D, (int16_t)0xA57D, 0x5A82, 0x5A82, (int16_t)0xA57D, (int16_t)0xA57D, 0x5A82,
0x471C, (int16_t)0x8275, 0x18F8, 0x6A6D, (int16_t)0x9592, (int16_t)0xE707, 0x7D8A, (int16_t)0xB8E3,
0x30FB, (int16_t)0x89BE, 0x7641, (int16_t)0xCF04, (int16_t)0xCF04, 0x7641, (int16_t)0x89BE, 0x30FB,
0x18F8, (int16_t)0xB8E3, 0x6A6D, (int16_t)0x8275, 0x7D8A, (int16_t)0x9592, 0x471C, (int16_t)0xE707,
};
// Sony's Qtab, FileFormat47 p.1-11, verified identical entry for entry. Their
// matrix is drawn "x 1/16", so the printed integers are what goes here and the
// 16 in their quantization formula is what cancels it.
//
// It is the DEFAULT here rather than an all-ones table: an
// unquantized DC for a bright 8x8 block lands around 1150, and the run-level
// field is signed TEN BITS. An identity quant table therefore clips the DC of
// any block brighter than about mid-grey, which is not a tuning problem, it is
// the reason the format carries a quant table at all.
constexpr uint8_t c_standardQuant[64] = {
2, 16, 19, 22, 26, 27, 29, 34, 16, 16, 22, 24, 27, 29, 34, 37, 19, 22, 26, 27, 29, 34,
34, 38, 22, 22, 26, 27, 29, 34, 37, 40, 22, 26, 27, 29, 32, 35, 40, 48, 26, 27, 29, 32,
35, 40, 48, 58, 26, 27, 29, 34, 38, 46, 56, 69, 27, 29, 35, 38, 46, 56, 69, 83,
};
struct Tables {
uint8_t quantY[64];
uint8_t quantUV[64];
int16_t scale[64]; // Q14 inverse matrix, what MDEC(3) receives
PCSX::DCT::Basis forward; // Q14 forward matrix, used by rawencode
bool scaleFromFile = false;
bool forwardFromFile = false;
Tables() {
memcpy(quantY, c_standardQuant, sizeof(quantY));
memcpy(quantUV, c_standardQuant, sizeof(quantUV));
memcpy(scale, c_standardScale, sizeof(scale));
forward = PCSX::DCT::standardBasis();
}
};
bool loadArray(const nlohmann::json &j, const char *key, int *out, unsigned n, std::string &err) {
if (!j.contains(key)) return false;
const auto &a = j[key];
if (!a.is_array() || a.size() != n) {
err = fmt::format("'{}' must be an array of {} numbers", key, n);
return false;
}
for (unsigned i = 0; i < n; i++) {
if (!a[i].is_number_integer()) {
err = fmt::format("'{}'[{}] is not an integer", key, i);
return false;
}
out[i] = a[i].get<int>();
}
return true;
}
// Table file layout:
// { "quant": { "y": [64], "uv": [64] }, "scale": [64], "forward": [64] }
// Every member is optional; whatever is absent keeps its default.
bool loadTables(const std::string &path, Tables &t, std::string &err) {
FILE *f = fopen(path.c_str(), "rb");
if (!f) {
err = fmt::format("cannot open {}", path);
return false;
}
std::string text;
char buf[4096];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), f)) > 0) text.append(buf, n);
fclose(f);
auto j = nlohmann::json::parse(text, nullptr, false, true);
if (j.is_discarded() || !j.is_object()) {
err = fmt::format("{} is not a JSON object", path);
return false;
}
int tmp[64];
if (j.contains("quant") && j["quant"].is_object()) {
if (loadArray(j["quant"], "y", tmp, 64, err)) {
for (int i = 0; i < 64; i++) t.quantY[i] = static_cast<uint8_t>(std::clamp(tmp[i], 0, 255));
} else if (!err.empty()) {
return false;
}
if (loadArray(j["quant"], "uv", tmp, 64, err)) {
for (int i = 0; i < 64; i++) t.quantUV[i] = static_cast<uint8_t>(std::clamp(tmp[i], 0, 255));
} else if (!err.empty()) {
return false;
}
}
if (loadArray(j, "scale", tmp, 64, err)) {
for (int i = 0; i < 64; i++) t.scale[i] = static_cast<int16_t>(tmp[i]);
t.scaleFromFile = true;
} else if (!err.empty()) {
return false;
}
if (loadArray(j, "forward", tmp, 64, err)) {
for (int i = 0; i < 64; i++) t.forward[i] = static_cast<int16_t>(tmp[i]);
t.forwardFromFile = true;
} else if (!err.empty()) {
return false;
}
return true;
}
} // namespace
namespace {
void printUsage(const char *argv0) {
fmt::print(R"(
Usage: {} <command> [options]
The command is always the first argument. Run `{} <command> -h` for its
options. Adding a command never changes what an existing one does.
commands:
rawencode a PNG into a raw MDEC run-level stream (what DMA0 consumes)
bsencode a PNG into Sony's legacy BS: the same stream, VLC/Huffman coded
bsdecode a BS stream back into a raw MDEC run-level stream
rawdecode a raw MDEC run-level stream back into a PNG
-h this help, or a command's help when given after a command
)",
argv0, argv0);
}
void usageRawEncode() {
fmt::print(R"(
Usage: mdec rawencode -i input.png -o output.bin [options]
-i file mandatory: input PNG. Dimensions are rounded UP to a multiple of
16 by edge replication; the padding is encoded and reported.
-o file mandatory: raw MDEC run-level stream, little endian halfwords.
-t file optional: JSON tables, see below.
-quality n optional: 1..100, higher is better. Default 50, which is exactly
the q_scale of 8 this defaulted to before there was a dial. The
spread is geometric, because the format's rate curve is: 100 is
q_scale 1, 50 is 8, 1 is 63.
-qscale n optional: the MDEC's own q_scale field, 1..63, for when you want to
set the format value directly. Higher is coarser, so it reads the
opposite way round to -quality. Mutually exclusive with it.
Neither one touches the quant table, and that is deliberate. qt[0]
is 2 and the DC divisor is qt[0]*2, so a finer table pins the DC
divisor at 2 and DC coefficients overrun the signed 10 bit field -
measured, on a smooth frame, mean error going 1.31 to 19.18 while
the size barely moved. The standard table is the finest DC the
format tolerates rather than a default to improve on. Supply a
genuinely different table with -t if you want one.
-order optional: raster | column. Default raster.
raster walks the image row by row. column walks 16-pixel columns
top to bottom, which is what retail STR does; it makes each column
of the decoded output one contiguous run, so a player can upload a
whole column as a single VRAM rect instead of one transfer per
macroblock. The pixels are identical either way, only the order
the macroblocks arrive in changes, and a decoder that disagrees
with the stream about it renders bands of vertically-striped
blocks.
-transform optional: exact | fast | symmetric. Default exact.
exact general basis, int32 accumulation, reference accurate
fast general basis, Q15 narrowing, quicker and coarser
symmetric general basis with the even/odd butterfly; needs a basis
with the usual symmetry and refuses one without it
JSON tables, every member optional, absent members keep their defaults:
{{ "quant": {{ "y": [64 ints], "uv": [64 ints] }},
"scale": [64 ints], "forward": [64 ints] }}
"forward" is the Q14 matrix this command transforms with. "scale" is the Q14
matrix MDEC(3) receives, used by rawdecode. They are the two halves of a pair
and THIS TOOL DOES NOT DERIVE ONE FROM THE OTHER: supply "scale" alone and
encoding still uses the default forward basis, which is almost certainly not
what you want. It says so when you do.
The default quant table is the standard one every shipping encoder uses. An
all-ones table looks appealing because it makes dequantization the identity,
but the run-level DC field is signed TEN BITS and an unquantized DC runs to
about 1150, so identity quant clips every block brighter than mid-grey.
)");
}
void usageRawDecode() {
fmt::print(R"(
Usage: mdec rawdecode -i input.bin -o output.png -width W -height H [options]
-i file mandatory: raw MDEC run-level stream.
-o file mandatory: output PNG.
-width n mandatory: width in pixels, multiple of 16.
-height n mandatory: height in pixels, multiple of 16.
-t file optional: JSON tables. Only "quant" and "scale" are used here.
The stream carries no dimensions, which is why they are mandatory rather than
guessed. This decodes with psx-spx's real_idct_core against "scale", so a
custom matrix is honoured exactly as the hardware honours it.
)");
}
int clamp10(int v, unsigned &clipped) {
if (v > 511) {
clipped++;
return 511;
}
if (v < -512) {
clipped++;
return -512;
}
return v;
}
int divRound(int num, int den) {
if (den == 0) return 0;
return (num < 0) ? -((-num + den / 2) / den) : ((num + den / 2) / den);
}
// psx-spx real_idct_core, verified against real consoles 2026-09-14 to within
// 3 LSB on a custom matrix.
void realIdct(int *block, const int16_t *scale) {
int temp[64];
int *src = block;
int *dst = temp;
for (int pass = 0; pass < 2; pass++) {
for (int x = 0; x < 8; x++) {
for (int y = 0; y < 8; y++) {
int64_t sum = 0;
for (int z = 0; z < 8; z++) sum += static_cast<int64_t>(src[y + z * 8]) * (scale[x + z * 8] / 8);
dst[x + y * 8] = static_cast<int>((sum + 0xfff) >> 13);
}
}
std::swap(src, dst);
}
if (src != block) memcpy(block, src, sizeof(temp));
}
int cmdEncode(CommandLine::args &args, bool asksForHelp, PCSX::DCT::Container container) {
if (asksForHelp) {
usageRawEncode();
return 0;
}
const auto in = args.get<std::string>("i");
const auto out = args.get<std::string>("o");
if (!in.has_value() || !out.has_value()) {
usageRawEncode();
return -1;
}
// -q is RETIRED rather than redefined. It used to mean the raw q_scale field,
// and making it mean a quality percentage would silently invert its sense -
// higher was worse, higher is now better - while every old invocation kept
// working and produced a very different image. An unknown option is already an
// error here, so the hard break is the one that cannot be misread.
if (args.get<int>("q").has_value()) {
fmt::print(stderr,
"-q is retired because it meant the raw q_scale field, which reads backwards as a quality "
"dial. Use -quality 1..100 (higher is better, 50 is the old default), or -qscale 1..63 for "
"the format field itself.\n");
return -1;
}
const auto qualityOpt = args.get<int>("quality");
const auto qscaleOpt = args.get<int>("qscale");
if (qualityOpt.has_value() && qscaleOpt.has_value()) {
fmt::print(stderr, "-quality and -qscale set the same thing. Pass one.\n");
return -1;
}
const int quality = std::clamp(qualityOpt.value_or(50), 1, 100);
const int qscale =
qscaleOpt.has_value() ? std::clamp(qscaleOpt.value(), 1, 63) : PCSX::DCT::qualityToQScale(quality);
const std::string which = args.get<std::string>("transform").value_or("exact");
PCSX::DCT::Transform transform;
if (which == "exact") {
transform = PCSX::DCT::Transform::ExactMatrix;
} else if (which == "fast") {
transform = PCSX::DCT::Transform::FastMatrix;
} else if (which == "symmetric") {
transform = PCSX::DCT::Transform::FastSymmetric;
} else {
fmt::print(stderr, "Unknown -transform '{}'; expected exact, fast or symmetric.\n", which);
return -1;
}
Tables tables;
if (auto t = args.get<std::string>("t"); t.has_value()) {
std::string err;
if (!loadTables(t.value(), tables, err)) {
fmt::print(stderr, "{}\n", err.empty() ? "cannot read tables" : err);
return -1;
}
if (tables.scaleFromFile && !tables.forwardFromFile) {
fmt::print(stderr,
"warning: {} supplies \"scale\" but no \"forward\". Encoding with the DEFAULT forward\n"
" basis, which does not match that scale matrix. Supply \"forward\" too.\n",
t.value());
}
}
int w = 0, h = 0, comp = 0;
uint8_t *pixels = stbi_load(in.value().c_str(), &w, &h, &comp, 3);
if (!pixels) {
fmt::print(stderr, "cannot read {}: {}\n", in.value(), stbi_failure_reason());
return -1;
}
const int pw = (w + 15) & ~15;
const int ph = (h + 15) & ~15;
std::vector<uint8_t> y(static_cast<size_t>(pw) * ph);
std::vector<uint8_t> cb(static_cast<size_t>(pw / 2) * (ph / 2));
std::vector<uint8_t> cr(cb.size());
// Full range JPEG YCbCr, which is what the MDEC's colour conversion inverts.
auto at = [&](int x, int yy, int c) {
const int sx = std::min(x, w - 1), sy = std::min(yy, h - 1);
return static_cast<int>(pixels[(static_cast<size_t>(sy) * w + sx) * 3 + c]);
};
for (int j = 0; j < ph; j++) {
for (int i = 0; i < pw; i++) {
const int r = at(i, j, 0), g = at(i, j, 1), b = at(i, j, 2);
y[static_cast<size_t>(j) * pw + i] =
static_cast<uint8_t>(std::clamp((299 * r + 587 * g + 114 * b) / 1000, 0, 255));
}
}
for (int j = 0; j < ph / 2; j++) {
for (int i = 0; i < pw / 2; i++) {
int rs = 0, gs = 0, bs = 0;
for (int dy = 0; dy < 2; dy++) {
for (int dx = 0; dx < 2; dx++) {
rs += at(i * 2 + dx, j * 2 + dy, 0);
gs += at(i * 2 + dx, j * 2 + dy, 1);
bs += at(i * 2 + dx, j * 2 + dy, 2);
}
}
const int r = rs / 4, g = gs / 4, b = bs / 4;
const int luma = (299 * r + 587 * g + 114 * b) / 1000;
const size_t o = static_cast<size_t>(j) * (pw / 2) + i;
cb[o] = static_cast<uint8_t>(std::clamp(128 + (b - luma) * 564 / 1000, 0, 255));
cr[o] = static_cast<uint8_t>(std::clamp(128 + (r - luma) * 713 / 1000, 0, 255));
}
}
stbi_image_free(pixels);
PCSX::DCT::Frame frame;
frame.y = y.data();
frame.cb = cb.data();
frame.cr = cr.data();
frame.width = pw;
frame.height = ph;
frame.yStride = pw;
frame.cStride = pw / 2;
// -order column walks 16-pixel columns top to bottom, the way retail STR
// does, so each decoded column is one contiguous run and a player can upload
// it as a single VRAM rect.
const std::string order = args.get<std::string>("order").value_or("raster");
if (order == "column") {
frame.order = PCSX::DCT::MacroblockOrder::Column;
} else if (order != "raster") {
fmt::print(stderr, "-order takes raster or column, got '{}'.\n", order);
return -1;
}
std::vector<int16_t> coeffs(PCSX::DCT::requiredCoefficientCount(pw, ph));
PCSX::DCT::Encoder encoder(0, transform, tables.forward);
auto promise = encoder.submit(frame, coeffs);
const auto result = promise.get();
if (result.failed) {
fmt::print(stderr, "the DCT stage refused the frame\n");
return -1;
}
// Quantization and run-level packing live in supportpsx/dct now: they are
// format knowledge, not CLI knowledge, and putting them there is what lets a
// caller drive per-macroblock rate control through a functor.
std::vector<uint16_t> stream;
stream.reserve(result.blockCount * 12);
PCSX::DCT::QuantTables qtabs;
qtabs.y = tables.quantY;
qtabs.uv = tables.quantUV;
const auto packed = PCSX::DCT::pack(coeffs, result, qtabs, qscale, stream);
if (packed.failed) {
fmt::print(stderr, "the packing stage refused the frame\n");
return -1;
}
// Two counters, two remedies. Merging them is what made the old message
// advise "raise -q" for a DC clip, which q_scale structurally cannot fix.
const unsigned clipped = packed.clippedAc + packed.clippedDc;
FILE *f = fopen(out.value().c_str(), "wb");
if (!f) {
fmt::print(stderr, "cannot write {}\n", out.value());
return -1;
}
std::vector<uint8_t> wrapped;
auto wrap = PCSX::DCT::toContainer(stream, container, wrapped);
if (wrap.failed) {
fclose(f);
fmt::print(stderr, "cannot wrap the stream: {}\n", wrap.error);
return -1;
}
fwrite(wrapped.data(), 1, wrapped.size(), f);
fclose(f);
fmt::print("{}x{}", w, h);
if (pw != w || ph != h) fmt::print(" padded to {}x{}", pw, ph);
if (!qscaleOpt.has_value()) fmt::print(", quality {}", quality);
fmt::print(", {} blocks, {} halfwords, q_scale {}, transform {}", result.blockCount, stream.size(), qscale,
which);
const double raw = static_cast<double>(stream.size() * 2);
if (container == PCSX::DCT::Container::Bs) {
fmt::print(", BS {} bytes ({:.1f}% of raw)", wrap.bytes, 100.0 * wrap.bytes / raw);
}
fmt::print("\n");
if (clipped) {
// Name the lever that can actually reach each one. q_scale is in the AC
// divisor only, so it cannot fix a clipped DC however far it is raised.
if (packed.clippedAc) {
fmt::print(stderr, "warning: {} AC coefficients clipped to the 10 bit run-level range. Raise -q.\n",
packed.clippedAc);
}
if (packed.clippedDc) {
fmt::print(stderr,
"warning: {} DC coefficients clipped to the 10 bit run-level range. Raising -q will NOT "
"help - q_scale is not in the DC divisor. Use a coarser quant table: lower -quality, or "
"raise quant[0] with -t.\n",
packed.clippedDc);
}
}
return 0;
}
int cmdBsDecode(CommandLine::args &args, bool asksForHelp) {
if (asksForHelp) {
fmt::print(R"(
Usage: mdec bsdecode -i input.bs -o output.bin
-i file mandatory: a BS stream, as bsencode writes.
-o file mandatory: the raw MDEC run-level stream it carries.
This undoes the VLC layer and nothing else, so it composes: `bsdecode` then
`rawdecode` gets you back to a PNG, and `bsencode | bsdecode` must reproduce
`rawencode` byte for byte on the same input and settings.
Dimensions are not needed and not accepted - a BS header carries the decoded
length, which is the whole reason its first word is the MDEC decode command.
)");
return 0;
}
auto in = args.get<std::string>("i");
auto out = args.get<std::string>("o");
if (!in.has_value() || !out.has_value()) {
fmt::print(stderr, "bsdecode needs -i and -o\n");
return -1;
}
FILE *f = fopen(in.value().c_str(), "rb");
if (!f) {
fmt::print(stderr, "cannot read {}\n", in.value());
return -1;
}
std::vector<uint8_t> bytes;
uint8_t buf[65536];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), f)) > 0) bytes.insert(bytes.end(), buf, buf + n);
fclose(f);
std::vector<uint16_t> rl;
auto r = PCSX::DCT::fromContainer(bytes, PCSX::DCT::Container::Bs, rl);
if (r.failed) {
fmt::print(stderr, "cannot decode {}: {}\n", in.value(), r.error);
return -1;
}
FILE *o = fopen(out.value().c_str(), "wb");
if (!o) {
fmt::print(stderr, "cannot write {}\n", out.value());
return -1;
}
for (uint16_t v : rl) {
const uint8_t b2[2] = {static_cast<uint8_t>(v & 0xff), static_cast<uint8_t>(v >> 8)};
fwrite(b2, 1, 2, o);
}
fclose(o);
fmt::print("{} blocks, q_scale {}, {} halfwords ({} words as the header declares)\n", r.blocks, r.qScale,
rl.size(), r.rlWords);
return 0;
}
int cmdRawDecode(CommandLine::args &args, bool asksForHelp) {
if (asksForHelp) {
usageRawDecode();
return 0;
}
const auto in = args.get<std::string>("i");
const auto out = args.get<std::string>("o");
const int w = args.get<int>("width").value_or(0);
const int h = args.get<int>("height").value_or(0);
if (!in.has_value() || !out.has_value() || w <= 0 || h <= 0 || (w % 16) || (h % 16)) {
usageRawDecode();
return -1;
}
Tables tables;
if (auto t = args.get<std::string>("t"); t.has_value()) {
std::string err;
if (!loadTables(t.value(), tables, err)) {
fmt::print(stderr, "{}\n", err.empty() ? "cannot read tables" : err);
return -1;
}
}
FILE *f = fopen(in.value().c_str(), "rb");
if (!f) {
fmt::print(stderr, "cannot open {}\n", in.value());
return -1;
}
std::vector<uint16_t> stream;
uint8_t bytes[2];
while (fread(bytes, 1, 2, f) == 2) stream.push_back(static_cast<uint16_t>(bytes[0] | (bytes[1] << 8)));
fclose(f);
const int mbx = w / 16, mby = h / 16;
std::vector<uint8_t> image(static_cast<size_t>(w) * h * 3);
size_t pos = 0;
for (int my = 0; my < mby; my++) {
for (int mx = 0; mx < mbx; mx++) {
int blocks[6][64];
for (int b = 0; b < 6; b++) {
memset(blocks[b], 0, sizeof(blocks[b]));
if (pos >= stream.size()) {
fmt::print(stderr, "stream ran out at macroblock {},{} block {}\n", mx, my, b);
return -1;
}
const uint8_t *qt = b < 2 ? tables.quantUV : tables.quantY;
uint16_t n = stream[pos++];
const int qscale = (n >> 10) & 0x3f;
auto sext10 = [](uint16_t v) { return static_cast<int>(static_cast<int16_t>(v << 6)) >> 6; };
blocks[b][0] = std::clamp(sext10(n & 0x3ff) * qt[0], -0x400, 0x3ff);
for (int k = 0;;) {
if (pos >= stream.size()) break;
n = stream[pos++];
if (n == 0xfe00) break;
k += ((n >> 10) & 0x3f) + 1;
if (k > 63) break;
blocks[b][c_zscan[k]] =
std::clamp((sext10(n & 0x3ff) * qt[k] * qscale + 4) / 8, -0x400, 0x3ff);
}
realIdct(blocks[b], tables.scale);
}
for (int py = 0; py < 16; py++) {
for (int px = 0; px < 16; px++) {
const int cidx = (py / 2) * 8 + (px / 2);
const int cr = blocks[0][cidx], cb = blocks[1][cidx];
const int yblk = (py / 8) * 2 + (px / 8);
const int yv = blocks[2 + yblk][(py % 8) * 8 + (px % 8)];
const int r = yv + (1402 * cr) / 1000;
const int g = yv - (344 * cb + 714 * cr) / 1000;
const int bl = yv + (1772 * cb) / 1000;
const size_t o = (static_cast<size_t>(my * 16 + py) * w + (mx * 16 + px)) * 3;
image[o + 0] = static_cast<uint8_t>(std::clamp(r + 128, 0, 255));
image[o + 1] = static_cast<uint8_t>(std::clamp(g + 128, 0, 255));
image[o + 2] = static_cast<uint8_t>(std::clamp(bl + 128, 0, 255));
}
}
}
}
if (!stbi_write_png(out.value().c_str(), w, h, 3, image.data(), w * 3)) {
fmt::print(stderr, "cannot write {}\n", out.value());
return -1;
}
fmt::print("{}x{}, {} macroblocks, {} halfwords consumed of {}\n", w, h, mbx * mby, pos, stream.size());
return 0;
}
} // namespace
int main(int argc, char **argv) {
CommandLine::args args(argc, argv);
const auto positional = args.positional();
const bool asksForHelp = args.get<bool>("h").value_or(false);
if (positional.empty()) {
printUsage(argv[0]);
return asksForHelp ? 0 : -1;
}
const std::string command(positional[0]);
// DCT::Transform::FastSymmetric throws std::invalid_argument on a basis
// without the even/odd symmetry, which is a documented and reachable input
// here - `-transform symmetric` with a `-t` table that lacks it. Without this
// the exception escapes main and the tool SIGABRTs on an input its own -h text
// says it refuses, while every other bad input prints a line and returns
// non-zero. Measured with `-transform exact` on the same table as the control:
// exit 0, encodes fine, so the abort was the symmetry check and not the table
// loader.
try {
if (command == "rawencode") return cmdEncode(args, asksForHelp, PCSX::DCT::Container::Raw);
if (command == "bsencode") return cmdEncode(args, asksForHelp, PCSX::DCT::Container::Bs);
if (command == "bsdecode") return cmdBsDecode(args, asksForHelp);
if (command == "rawdecode") return cmdRawDecode(args, asksForHelp);
} catch (const std::exception &e) {
fmt::print(stderr, "{}: {}\n", command, e.what());
return -1;
}
fmt::print(stderr, "Unknown command: {}\n\n", command);
printUsage(argv[0]);
return -1;
}
+7
View File
@@ -0,0 +1,7 @@
local support_root = path.join(os.scriptdir(), "..", "..")
includes(path.join(support_root, "tools", "xmake"))
common_rules()
target("pcsx.mdec", common_tool)
+132
View File
@@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="ReleaseWithClangCL|x64">
<Configuration>ReleaseWithClangCL</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>18.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{69f506ed-2c5b-4571-b023-786edcfc0e1e}</ProjectGuid>
<RootNamespace>mdec</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v145</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v145</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseWithClangCL|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v145</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="..\common.props" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="..\common.props" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseWithClangCL|x64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="..\common.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
<Manifest>
<EnableSegmentHeap>true</EnableSegmentHeap>
</Manifest>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
<Manifest>
<EnableSegmentHeap>true</EnableSegmentHeap>
</Manifest>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseWithClangCL|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
<Manifest>
<EnableSegmentHeap>true</EnableSegmentHeap>
</Manifest>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="..\..\tools\mdec\mdec.cc" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\fmt\fmt.vcxproj">
<Project>{71772007-5110-418d-be9c-fb102b6eaabf}</Project>
</ProjectReference>
<ProjectReference Include="..\supportpsx\supportpsx.vcxproj">
<Project>{b2e2ad84-9d7f-4976-9572-e415819ffd7f}</Project>
</ProjectReference>
<ProjectReference Include="..\support\support.vcxproj">
<Project>{0e621321-093c-4d60-bd8b-027fdc2b0f63}</Project>
</ProjectReference>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\..\tools\mdec\mdec.cc">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
</Project>
+13
View File
@@ -335,6 +335,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "midi2psm", "midi2psm\midi2p
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "exe2exe", "exe2exe\exe2exe.vcxproj", "{6A476694-C1F8-4BA6-AA33-07D70FB197F4}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "mdec", "mdec\mdec.vcxproj", "{69F506ED-2C5B-4571-B023-786EDCFC0E1E}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "implot", "implot\implot.vcxproj", "{C068ED1F-1341-42B1-A429-5D5F377FD63D}"
EndProject
Global
@@ -796,6 +798,16 @@ Global
{6A476694-C1F8-4BA6-AA33-07D70FB197F4}.ReleaseWithClangCL|x64.Build.0 = ReleaseWithClangCL|x64
{6A476694-C1F8-4BA6-AA33-07D70FB197F4}.ReleaseWithTracy|x64.ActiveCfg = Release|x64
{6A476694-C1F8-4BA6-AA33-07D70FB197F4}.ReleaseWithTracy|x64.Build.0 = Release|x64
{69F506ED-2C5B-4571-B023-786EDCFC0E1E}.Debug|x64.ActiveCfg = Debug|x64
{69F506ED-2C5B-4571-B023-786EDCFC0E1E}.Debug|x64.Build.0 = Debug|x64
{69F506ED-2C5B-4571-B023-786EDCFC0E1E}.Release|x64.ActiveCfg = Release|x64
{69F506ED-2C5B-4571-B023-786EDCFC0E1E}.Release|x64.Build.0 = Release|x64
{69F506ED-2C5B-4571-B023-786EDCFC0E1E}.ReleaseCLI|x64.ActiveCfg = ReleaseWithClangCL|x64
{69F506ED-2C5B-4571-B023-786EDCFC0E1E}.ReleaseCLI|x64.Build.0 = ReleaseWithClangCL|x64
{69F506ED-2C5B-4571-B023-786EDCFC0E1E}.ReleaseWithClangCL|x64.ActiveCfg = ReleaseWithClangCL|x64
{69F506ED-2C5B-4571-B023-786EDCFC0E1E}.ReleaseWithClangCL|x64.Build.0 = ReleaseWithClangCL|x64
{69F506ED-2C5B-4571-B023-786EDCFC0E1E}.ReleaseWithTracy|x64.ActiveCfg = Release|x64
{69F506ED-2C5B-4571-B023-786EDCFC0E1E}.ReleaseWithTracy|x64.Build.0 = Release|x64
{C068ED1F-1341-42B1-A429-5D5F377FD63D}.Debug|x64.ActiveCfg = Debug|x64
{C068ED1F-1341-42B1-A429-5D5F377FD63D}.Debug|x64.Build.0 = Debug|x64
{C068ED1F-1341-42B1-A429-5D5F377FD63D}.Release|x64.ActiveCfg = Release|x64
@@ -864,6 +876,7 @@ Global
{FC0BCB98-8E1A-4304-BFF0-B22970853019} = {C6DD47BC-0C38-4AE6-B517-9675F3AC8A50}
{E1D23582-C66F-40D3-A163-B1B4470B3F15} = {C6DD47BC-0C38-4AE6-B517-9675F3AC8A50}
{6A476694-C1F8-4BA6-AA33-07D70FB197F4} = {C6DD47BC-0C38-4AE6-B517-9675F3AC8A50}
{69F506ED-2C5B-4571-B023-786EDCFC0E1E} = {C6DD47BC-0C38-4AE6-B517-9675F3AC8A50}
{C068ED1F-1341-42B1-A429-5D5F377FD63D} = {64A05F50-3203-42CC-B632-09D6EE6EA856}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution