diff --git a/Core/Core.cpp b/Core/Core.cpp index a041fc164c..a413703417 100644 --- a/Core/Core.cpp +++ b/Core/Core.cpp @@ -245,22 +245,30 @@ static MIPSExceptionInfo g_exceptionInfo; // This is called on EmuThread before RunLoop. static bool Core_ProcessStepping(MIPSDebugInterface *cpu); -static std::function g_debugOutputListener; +static std::function g_debugOutputListener; static std::function g_debugScreenshotListener; -void Core_RegisterDebugOutputListeners(std::function listener, std::function screenshotListener) { +void Core_RegisterDebugOutputListeners(std::function listener, std::function screenshotListener) { g_debugOutputListener = std::move(listener); g_debugScreenshotListener = std::move(screenshotListener); } void Core_SendDebugOutput(LogLevel level, std::string_view string) { if (g_debugOutputListener) { - g_debugOutputListener(string); + g_debugOutputListener(DebugOutputChannel::Debug, string); } else { GENERIC_LOG(Log::sceIo, level, "%.*s", STR_VIEW(string)); } } +bool Core_SendHostOutput(DebugOutputChannel channel, std::string_view string) { + if (!g_debugOutputListener) { + return false; + } + g_debugOutputListener(channel, string); + return true; +} + void Core_SendDebugScreenshot(const DebugScreenshotDesc &desc) { if (g_debugScreenshotListener) { g_debugScreenshotListener(desc); diff --git a/Core/Core.h b/Core/Core.h index 8054077d37..1fde9982a0 100644 --- a/Core/Core.h +++ b/Core/Core.h @@ -301,9 +301,19 @@ struct DebugScreenshotDesc { u32 height; GEBufferFormat format; }; +// Which of the host's output streams a piece of emulated output belongs on. +enum class DebugOutputChannel { + Debug, // The "emulator:" devctl channel, plus our own messages about the run. This is what pspautotests uses. + StdOut, // sceIoWrite() to fd 1, and to a tty device. + StdErr, // sceIoWrite() to fd 2. +}; + void Core_SendDebugOutput(LogLevel level, std::string_view string); +// Offers raw output from the emulated program to the listener, if any. Returns false if there was +// none, in which case the caller should log it instead - that's what the normal app does. +bool Core_SendHostOutput(DebugOutputChannel channel, std::string_view string); void Core_SendDebugScreenshot(const DebugScreenshotDesc &desc); -void Core_RegisterDebugOutputListeners(std::function listener, std::function screenshotListener); +void Core_RegisterDebugOutputListeners(std::function listener, std::function screenshotListener); class MIPSState; // Shortcut, just calls Core_MemoryException with automatically determined parameters (function name, etc). diff --git a/Core/HLE/sceIo.cpp b/Core/HLE/sceIo.cpp index 2418c5bca2..45e4733713 100644 --- a/Core/HLE/sceIo.cpp +++ b/Core/HLE/sceIo.cpp @@ -1187,6 +1187,20 @@ void SanitizeControlChars(std::string &buf) { } } +// Output the emulated program wrote to stdout, stderr or a tty. A host that wants it (headless) +// gets the bytes exactly as written, so its stdout is a faithful copy of the program's. Otherwise +// it goes in the log, sanitized and with the trailing newline trimmed, since the log adds one. +static void __IoWriteToHostOutput(DebugOutputChannel channel, const char *str, u32 validSize, int size, const char *name) { + if (Core_SendHostOutput(channel, std::string_view(str, validSize))) { + return; + } + const int str_size = size <= 0 || validSize == 0 ? 0 : (str[validSize - 1] == '\n' ? validSize - 1 : validSize); + // buffer so we can edit the string. + std::string buf(str, str_size); + SanitizeControlChars(buf); + INFO_LOG(Log::Printf, "%s: %.*s", name, (int)buf.size(), buf.data()); +} + static bool __IoWrite(int &result, int id, u32 data_addr, int size, int &us) { PROFILE_THIS_SCOPE("io_rw"); // Low estimate, may be improved later from the WriteFile result. @@ -1199,12 +1213,8 @@ static bool __IoWrite(int &result, int id, u32 data_addr, int size, int &us) { const u32 validSize = Memory::ClampValidSizeAt(data_addr, size); // Let's handle stdout/stderr specially. if (id == PSP_STDOUT || id == PSP_STDERR) { - const char *str = (const char *) data_ptr; - const int str_size = size <= 0 ? 0 : (str[validSize - 1] == '\n' ? validSize - 1 : validSize); - // buffer so we can edit the string. - std::string buf(str, str_size); - SanitizeControlChars(buf); - INFO_LOG(Log::Printf, "%s: %.*s", id == 1 ? "stdout" : "stderr", (int)buf.size(), buf.data()); + __IoWriteToHostOutput(id == PSP_STDERR ? DebugOutputChannel::StdErr : DebugOutputChannel::StdOut, + (const char *)data_ptr, validSize, size, id == PSP_STDOUT ? "stdout" : "stderr"); result = validSize; return true; } @@ -1228,11 +1238,7 @@ static bool __IoWrite(int &result, int id, u32 data_addr, int size, int &us) { NotifyMemInfo(MemBlockFlags::READ, data_addr, size, tag.c_str(), tag.size()); if (f->isTTY) { - const char *str = (const char *)data_ptr; - const int str_size = size <= 0 ? 0 : (str[validSize - 1] == '\n' ? validSize - 1 : validSize); - std::string buf(str, str_size); - SanitizeControlChars(buf); - INFO_LOG(Log::Printf, "%s: %.*s", "tty", (int)buf.size(), buf.data()); + __IoWriteToHostOutput(DebugOutputChannel::StdOut, (const char *)data_ptr, validSize, size, "tty"); result = validSize; return true; } diff --git a/headless/Headless.cpp b/headless/Headless.cpp index 0ec0da4b4f..4450558d1d 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -85,6 +85,10 @@ static bool g_screenshotFailed = false; static std::string g_debugOutputBuffer; static bool g_writeFailureScreenshot = true; static bool g_writeDebugOutput = true; +// Whether the emulated program's stdout/stderr are forwarded to ours. On by default - just running +// a homebrew and seeing what it prints is the most basic thing headless does. Off for test runs, +// where the only output that should reach the console is what the comparison produces. +static bool g_forwardHostOutput = true; // Set from the savestate callback on the emu thread, read after it has been joined. static bool g_stateLoadFailed = false; // Set by --save-state. Saving needs the game to actually be running, so it happens from the run @@ -167,7 +171,20 @@ void SetWriteFailureScreenshot(bool flag) { g_writeFailureScreenshot = flag; } -void SendDebugOutput(std::string_view output) { +void SendDebugOutput(DebugOutputChannel channel, std::string_view output) { + if (channel != DebugOutputChannel::Debug) { + if (!g_forwardHostOutput) + return; + // Straight through, unmodified - and flushed, so it interleaves with the debug channel in + // the order the program actually wrote it. + FlushDebugOutput(); + fflush(stdout); + FILE *stream = channel == DebugOutputChannel::StdErr ? stderr : stdout; + fwrite(output.data(), sizeof(char), output.length(), stream); + fflush(stream); + return; + } + if (!g_writeDebugOutput) return; #ifdef _WIN32 @@ -183,7 +200,7 @@ void SendDebugOutput(std::string_view output) { } void SendAndCollectOutput(std::string_view output) { - SendDebugOutput(output); + SendDebugOutput(DebugOutputChannel::Debug, output); if (PSP_CoreParameter().collectDebugOutput) { *PSP_CoreParameter().collectDebugOutput += output; } @@ -397,7 +414,7 @@ static bool RunAutoTest(GraphicsContext *graphicsContext, CoreParameter &corePar if (!opt.bench) { printf("%s", output.c_str()); - SendDebugOutput("TIMEOUT\n"); + SendDebugOutput(DebugOutputChannel::Debug, "TIMEOUT\n"); GitHubActionsPrint("error", "Test timeout for %s", currentTestName.c_str()); } @@ -971,6 +988,7 @@ int main(int argc, const char* argv[]) { SetWriteFailureScreenshot(!getenv("GITHUB_ACTIONS") && !testOptions.bench); g_writeDebugOutput = !testOptions.compare && !testOptions.bench; + g_forwardHostOutput = !testOptions.compare && !testOptions.bench; #if PPSSPP_PLATFORM(ANDROID) // For some reason the debugger installs it with this name? diff --git a/headless/README.md b/headless/README.md index c6a0143e22..63866f869e 100644 --- a/headless/README.md +++ b/headless/README.md @@ -1,6 +1,6 @@ # PPSSPPHeadless -Non-interactive, headless build of PPSSPP. It boots a PSP executable, PRX, or GE frame dump (`.ppdmp`) without a GUI, outputs emulated debug text to the console, optionally captures and compares text output or screenshots, and exits. +Non-interactive, headless build of PPSSPP. It boots a PSP executable, PRX, or GE frame dump (`.ppdmp`) without a GUI, forwards what the program prints to the console, optionally captures and compares text output or screenshots, and exits. Primarily intended for: - Automated regression testing (via [pspautotests](https://github.com/hrydgard/pspautotests/)) @@ -39,7 +39,7 @@ PPSSPPHeadless file.elf|file.prx|file.ppdmp [...] [options] | `@file` | Read list of test filenames from a text file (`@-` for stdin). | | `-m`, `--mount ` | Mount an ISO/CSO on `umd1:`. | | `-r`, `--root ` | Mount a path on `host0:` (ELF/PRX files must be under this). | -| `-l`, `--log` | Full emulator log output (not just emulated `printf`). | +| `-l`, `--log` | Full emulator log output, on top of the program's own output (see Program Output below). | | `-o`, `--odslog` | Write log to `OutputDebugString` (Windows only). | | `--graphics=` | GPU backend: `software`, `gles`, `directx11`, `vulkan`. | | `--screenshot=` | Compare the rendered output against a reference screenshot. | @@ -60,6 +60,21 @@ PPSSPPHeadless file.elf|file.prx|file.ppdmp [...] [options] | `--ignore ` | Skip the specified test file. | | `--help` / `-h` | Show usage information. | +## Program Output + +Running a homebrew with no options at all prints what it prints, and nothing else. Two separate +channels feed the console, both on by default: + +- **The emulated program's `stdout` and `stderr`** - `sceIoWrite()` to fd 1 and 2 (which is where a + PSPSDK `printf()` ends up), and writes to a tty device. These go to *our* `stdout` and `stderr` + respectively, byte for byte, with no prefix or sanitization. +- **The `emulator:` devctl channel** - `sceIoDevctl("emulator:", SEND_OUTPUT, ...)`, a PPSSPP + extension. This goes to `stdout`. pspautotests uses this channel exclusively, and it is what + `--compare` compares against the `.expected` file. + +`--compare` and `--bench` turn both off, so the only thing a test run prints is the comparison +result. Emulator log output is separate again, and stays off unless you pass `-l`. + ## GPU Backends The `--graphics` option selects the rendering backend: