diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d9bd04..7584efc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,6 +75,7 @@ add_library(splasheditdoc STATIC src/editor/project.cpp src/editor/serial.cpp src/editor/hardware.cpp + src/editor/psxmon.cpp src/editor/unirom.cpp) target_include_directories(splasheditdoc PUBLIC src) find_package(Threads REQUIRED) diff --git a/src/editor/hardware.cpp b/src/editor/hardware.cpp index 2542aee..920b594 100644 --- a/src/editor/hardware.cpp +++ b/src/editor/hardware.cpp @@ -3,6 +3,7 @@ #include #include +#include "editor/psxmon.hh" #include "editor/unirom.hh" namespace editor { @@ -78,8 +79,14 @@ void HardwareRun::start(Opener open, std::filesystem::path exe, std::filesystem: return; } phase_ = Phase::Uploading; - add("Uploading psxsplash..."); - if (!uniromUpload(*link, bytes, &err, [this](int p) { progress_ = p; }, &cancel_)) { + // psxmon answers a PING; anything else is taken to be the Unirom shell, + // which ignores the PING frames as unknown command text. + uint16_t caps = 0; + const bool monitor = psxmonPresent(*link, 1500, &caps); + add(monitor ? "Uploading psxsplash through psxmon..." : "Uploading psxsplash through Unirom..."); + auto progress = [this](int p) { progress_ = p; }; + if (!(monitor ? psxmonUpload(*link, bytes, &err, progress, &cancel_) + : uniromUpload(*link, bytes, &err, progress, &cancel_))) { if (cancel_) { phase_ = Phase::Stopped; return; @@ -89,8 +96,14 @@ void HardwareRun::start(Opener open, std::filesystem::path exe, std::filesystem: phase_ = Phase::Running; add("Running. Files are served from " + dir.string()); PcdrvHost host(dir); - const bool ok = host.serve(*link, cancel_, [this](const std::string& l) { add(l); }, - [this](const std::string& e) { add("[file] " + e); }, &err); + auto line = [this](const std::string& l) { add(l); }; + auto event = [this](const std::string& e) { add("[file] " + e); }; + // Under a psxmon entered from the exception handler's slot, psxsplash + // asks for files with break calls the monitor stops on; anywhere else + // it speaks its own SIO1 protocol. + const bool breaks = monitor && (caps & psxmon::kCapSlot); + const bool ok = breaks ? psxmonServe(*link, host, cancel_, line, event, &err) + : host.serve(*link, cancel_, line, event, &err); if (!ok && !cancel_) return fail(err); phase_ = Phase::Stopped; }); diff --git a/src/editor/psxmon.cpp b/src/editor/psxmon.cpp new file mode 100644 index 0000000..0a13676 --- /dev/null +++ b/src/editor/psxmon.cpp @@ -0,0 +1,394 @@ +#include "editor/psxmon.hh" + +#include +#include +#include +#include +#include + +namespace editor { + +namespace psxmon { + +uint32_t fletcher(const uint16_t* words, size_t count) { + uint32_t s1 = 0, s2 = 0; + for (size_t i = 0; i < count; ++i) { + s1 += words[i]; + s2 += s1; + } + const uint32_t ck = (s2 % 65535) << 16 | (s1 % 65535); + return ck ? ck : 0xffffffffu; +} + +std::vector encode(uint16_t type, const std::vector& payload) { + std::vector words{type, uint16_t(payload.size())}; + words.insert(words.end(), payload.begin(), payload.end()); + const uint32_t ck = fletcher(words.data(), words.size()); + words.push_back(uint16_t(ck)); + words.push_back(uint16_t(ck >> 16)); + std::vector out{0x00, 0xaa, 0x55}; + for (uint16_t w : words) { + out.push_back(uint8_t(w)); + out.push_back(uint8_t(w >> 8)); + } + return out; +} + +} // namespace psxmon + +namespace { + +using Clock = std::chrono::steady_clock; +constexpr size_t kLoadChunk = 8192; // 4 header words + 4096 data words, under the monitor's cap +constexpr size_t kMaxLen = 4096 + 16; // a longer header is noise, as on the monitor side +constexpr uint32_t kDefaultStack = 0x801ffff0; + +uint32_t readLE(const std::vector& v, size_t at) { + return uint32_t(v[at]) | uint32_t(v[at + 1]) << 8 | uint32_t(v[at + 2]) << 16 | uint32_t(v[at + 3]) << 24; +} + +void push32(std::vector& w, uint32_t v) { + w.push_back(uint16_t(v)); + w.push_back(uint16_t(v >> 16)); +} + +struct Frame { + uint16_t type = 0; + std::vector words; + bool ok = false; +}; + +// Splits what the console sends into frames, dropping console text. +struct Reader { + Link& link; + std::deque buf; + bool closed = false; + // Read one byte at a time, so nothing past the awaited frame is taken off the link. + bool exact = false; + std::string text; // console text seen between frames + bool sioEscape = false; // psxsplash's SIO1 file call (0x00 'p') seen instead of a frame + + // Next frame of a type in `types`, or false at the deadline. Frames of + // other types are dropped. + bool wait(std::initializer_list types, Clock::time_point deadline, Frame* out) { + for (;;) { + while (parse(out)) { + for (uint16_t t : types) + if (out->type == t) return true; + } + if (closed) return false; + const auto now = Clock::now(); + if (now >= deadline) return false; + uint8_t tmp[512]; + const int ms = int(std::chrono::duration_cast(deadline - now).count()); + const int n = link.read(tmp, exact ? 1 : sizeof tmp, ms < 1 ? 1 : ms > 50 ? 50 : ms); + if (n < 0) closed = true; + for (int i = 0; i < n; ++i) buf.push_back(tmp[i]); + } + } + + bool parse(Frame* out) { + for (;;) { + while (!buf.empty() && buf.front() != 0) { + text.push_back(char(buf.front())); + buf.pop_front(); + } + // psxsplash's own file call starts 0x00 'p', never a frame. + if (buf.size() >= 2 && buf[1] == 'p') { + sioEscape = true; + buf.pop_front(); + continue; + } + if (buf.size() < 7) return false; + // A run of zeros is one frame start. + size_t z = 0; + while (z < buf.size() && buf[z] == 0) ++z; + if (z > 1) { + buf.erase(buf.begin(), buf.begin() + std::ptrdiff_t(z - 1)); + continue; + } + auto word = [&](size_t at) { return uint16_t(buf[at] | buf[at + 1] << 8); }; + const uint16_t len = word(5); + if (word(1) != 0x55aa || len > kMaxLen) { + buf.pop_front(); + continue; + } + const size_t total = 7 + 2 * size_t(len) + 4; + if (buf.size() < total) return false; + std::vector all{word(3), len}; + for (size_t i = 0; i < len; ++i) all.push_back(word(7 + 2 * i)); + const uint32_t ck = uint32_t(word(7 + 2 * len)) | uint32_t(word(9 + 2 * len)) << 16; + out->type = all[0]; + out->words.assign(all.begin() + 2, all.end()); + out->ok = ck == psxmon::fletcher(all.data(), all.size()); + buf.erase(buf.begin(), buf.begin() + std::ptrdiff_t(total)); + return true; + } + } +}; + +bool send(Link& link, uint16_t type, const std::vector& payload) { + const std::vector f = psxmon::encode(type, payload); + return link.write(f.data(), f.size()); +} + +// Sends a command and waits for ACK. The monitor answers every command it +// reads, so silence means the frame (or the answer) was lost. +bool command(Reader& r, uint16_t type, const std::vector& payload, int timeoutMs, const std::string& what, + std::string* err) { + if (!send(r.link, type, payload)) { + *err = "The connection closed."; + return false; + } + Frame f; + if (!r.wait({psxmon::Ack, psxmon::Error}, Clock::now() + std::chrono::milliseconds(timeoutMs), &f)) { + *err = r.closed ? "The connection closed." : "psxmon did not answer " + what + "."; + return false; + } + if (f.type == psxmon::Error || !f.ok) { + *err = "psxmon refused " + what + " (error " + std::to_string(f.words.empty() ? -1 : int(f.words[0])) + ")."; + return false; + } + return true; +} + +} // namespace + +bool psxmonPresent(Link& link, int timeoutMs, uint16_t* caps) { + Reader r{link, {}, false, false, {}}; + const auto deadline = Clock::now() + std::chrono::milliseconds(timeoutMs); + while (Clock::now() < deadline && !r.closed) { + if (!send(link, psxmon::Ping, {})) return false; + Frame f; + const auto slice = std::min(deadline, Clock::now() + std::chrono::milliseconds(500)); + if (r.wait({psxmon::Pong}, slice, &f) && f.ok) { + if (caps) *caps = f.words.size() > 1 ? f.words[1] : 0; + return true; + } + } + return false; +} + +bool psxmonUpload(Link& link, const std::vector& exe, std::string* err, const UploadProgress& progress, + const std::atomic* cancel) { + std::string local; + if (!err) err = &local; + err->clear(); + if (exe.size() <= 0x800 || std::memcmp(exe.data(), "PS-X EXE", 8) != 0) { + *err = "Not a PS-X EXE."; + return false; + } + const uint32_t pc = readLE(exe, 0x10), gp = readLE(exe, 0x14), dest = readLE(exe, 0x18); + uint32_t size = readLE(exe, 0x1c); + if (!size || size > exe.size() - 0x800) size = uint32_t(exe.size() - 0x800); + uint32_t sp = readLE(exe, 0x30) + readLE(exe, 0x34); + if (!sp) sp = kDefaultStack; + + Reader r{link, {}, false, false, {}}; + for (uint32_t at = 0; at < size; at += uint32_t(kLoadChunk)) { + if (cancel && cancel->load()) { + *err = "Cancelled."; + return false; + } + const uint32_t n = std::min(uint32_t(kLoadChunk), size - at); + std::vector payload; + push32(payload, dest + at); + push32(payload, n); + const uint8_t* p = exe.data() + 0x800 + at; + for (uint32_t i = 0; i < n; i += 2) payload.push_back(uint16_t(p[i] | (i + 1 < n ? p[i + 1] << 8 : 0))); + char where[16]; + std::snprintf(where, sizeof where, "%08x", unsigned(dest + at)); + if (!command(r, psxmon::Load, payload, 5000, std::string("LOAD at ") + where, err)) return false; + if (progress) progress(int(uint64_t(at + n) * 100 / size)); + } + std::vector run; + push32(run, pc); + push32(run, gp); + push32(run, sp); + // The program may speak right after the ACK: leave its bytes on the link. + r.exact = true; + return command(r, psxmon::Run, run, 2000, "RUN", err); +} + +namespace { + +constexpr int kReplyMs = 3000; +constexpr uint32_t kNameMax = 256; +constexpr int kRegV0 = 2, kRegA0 = 4, kRegPc = 37; + +uint32_t word32(const std::vector& w, size_t i) { + return (i + 1 < w.size()) ? uint32_t(w[i]) | uint32_t(w[i + 1]) << 16 : 0; +} + +struct Served { + Reader& r; + std::string* err; + + bool getRegs(uint32_t* regs) { + if (!send(r.link, psxmon::GetRegs, {})) return lost(); + Frame f; + if (!r.wait({psxmon::Regs, psxmon::Error}, deadline(), &f) || f.type != psxmon::Regs || !f.ok || + f.words.size() < 76) + return fail("GET_REGS"); + for (size_t i = 0; i < 38; ++i) regs[i] = word32(f.words, 2 * i); + return true; + } + bool readMem(uint32_t addr, uint32_t len, std::vector* out) { + std::vector p; + push32(p, addr); + push32(p, len); + if (!send(r.link, psxmon::ReadMem, p)) return lost(); + out->clear(); + while (out->size() < len) { + Frame f; + if (!r.wait({psxmon::Data, psxmon::Error}, deadline(), &f) || f.type != psxmon::Data || !f.ok) + return fail("READ_MEM"); + const uint32_t n = word32(f.words, 0); + if (n == 0 || f.words.size() < 2 + (n + 1) / 2) return fail("READ_MEM"); + for (uint32_t i = 0; i < n; ++i) out->push_back(uint8_t(f.words[2 + i / 2] >> (i & 1 ? 8 : 0))); + } + return true; + } + bool writeMem(uint32_t addr, const std::vector& data) { + for (size_t at = 0; at < data.size(); at += kLoadChunk) { + const uint32_t n = uint32_t(std::min(kLoadChunk, data.size() - at)); + std::vector p; + push32(p, addr + uint32_t(at)); + push32(p, n); + for (uint32_t i = 0; i < n; i += 2) + p.push_back(uint16_t(data[at + i] | (i + 1 < n ? data[at + i + 1] << 8 : 0))); + if (!command(r, psxmon::WriteMem, p, kReplyMs, "WRITE_MEM", err)) return false; + } + return true; + } + bool setReg(int index, uint32_t value) { + std::vector p{uint16_t(index)}; + push32(p, value); + return command(r, psxmon::SetReg, p, kReplyMs, "SET_REG", err); + } + + Clock::time_point deadline() const { return Clock::now() + std::chrono::milliseconds(kReplyMs); } + bool lost() { + *err = "The connection closed."; + return false; + } + bool fail(const char* what) { + *err = r.closed ? "The connection closed." : std::string("psxmon did not answer ") + what + "."; + return false; + } +}; + +} // namespace + +bool psxmonServe(Link& link, PcdrvHost& files, const std::atomic& cancel, + const std::function& line, const PcdrvHost::Event& event, std::string* err) { + std::string local; + if (!err) err = &local; + err->clear(); + Reader r{link, {}, false, false, {}}; + Served s{r, err}; + std::string pendingLine; + bool warned = false; + // Console text goes out a line at a time; a partial line waits for its end + // or for the link to go quiet. + auto flushText = [&](bool idle) { + for (char c : r.text) { + if (c == '\n' || c == '\r') { + if (!pendingLine.empty() && line) line(pendingLine); + pendingLine.clear(); + } else if (uint8_t(c) >= 0x20 && uint8_t(c) < 0x7f) { + pendingLine.push_back(c); + if (pendingLine.size() >= 200) { + if (line) line(pendingLine); + pendingLine.clear(); + } + } + } + r.text.clear(); + if (idle && !pendingLine.empty()) { + if (line) line(pendingLine); + pendingLine.clear(); + } + }; + while (!cancel.load()) { + Frame f; + const bool got = r.wait({psxmon::Stopped}, Clock::now() + std::chrono::milliseconds(50), &f); + flushText(!got); + if (r.sioEscape && !warned) { + // It never stops for a break call, so nothing below would ever say why it waits. + warned = true; + if (line) line("This psxsplash asks for files over SIO1, which psxmon does not allow. Update psxsplash."); + } + if (r.closed) return s.lost(); + if (!got) continue; + const uint32_t reason = f.words.empty() ? 0 : f.words[0]; + const uint32_t epc = word32(f.words, 1), insn = word32(f.words, 3); + const uint32_t code1 = insn >> 16 & 0x3ff, code2 = insn >> 6 & 0x3ff; + const bool isBreak = f.ok && reason == 1 && (insn & 0x3f) == 0x0d; + uint32_t regs[38]; + if (isBreak && code1 == 4 && code2 == 0) { + if (!s.getRegs(regs)) return false; + if (line) line("The program exited with code " + std::to_string(int32_t(regs[kRegA0])) + "."); + return true; + } + if (!isBreak || code1 != 0 || code2 < 0x101 || code2 > 0x107) { + static const char* names[] = {"?", "a breakpoint", "an interrupt", "a data watch", "a fault", "exit"}; + char at[16]; + std::snprintf(at, sizeof at, "%08x", unsigned(epc)); + *err = std::string("The program stopped on ") + (reason < 6 ? names[reason] : "an unknown event") + + " at " + at + "."; + return false; + } + if (!s.getRegs(regs)) return false; + const uint32_t a0 = regs[kRegA0], a1 = regs[kRegA0 + 1], a2 = regs[kRegA0 + 2], a3 = regs[kRegA0 + 3]; + int32_t ret = -1; + switch (code2) { + case 0x101: + if (event) event("init"); + ret = 0; + break; + case 0x103: { + std::vector raw; + if (!s.readMem(a0, kNameMax, &raw)) return false; + std::string name; + for (uint8_t c : raw) { + if (!c) break; + name.push_back(char(c)); + } + ret = files.open(name, event); + break; + } + case 0x104: + ret = files.close(a0, event); + break; + case 0x105: { + std::vector data; + ret = files.read(a1, a2, &data, event); + if (ret > 0 && !s.writeMem(a3, data)) return false; + break; + } + case 0x107: + ret = files.seek(a0, int32_t(a2), a3, event); + break; + default: // creat and write: files are read-only + if (event) { + char op[16]; + std::snprintf(op, sizeof op, "%x", unsigned(code2)); + event(std::string("unsupported file call 0x") + op); + } + break; + } + // pcdrv.h: init and close return v0; the others return v1 when v0 is 0. + const bool inV0 = code2 == 0x101 || code2 == 0x104; + if (inV0) { + if (!s.setReg(kRegV0, uint32_t(ret))) return false; + } else if (!s.setReg(kRegV0, 0) || !s.setReg(kRegV0 + 1, uint32_t(ret))) { + return false; + } + if (!s.setReg(kRegPc, epc + 4) || !command(r, psxmon::Cont, {}, kReplyMs, "CONT", err)) return false; + } + flushText(true); + return true; +} + +} // namespace editor diff --git a/src/editor/psxmon.hh b/src/editor/psxmon.hh new file mode 100644 index 0000000..0c5ec56 --- /dev/null +++ b/src/editor/psxmon.hh @@ -0,0 +1,69 @@ +// Run on hardware through psxmon, the debug monitor the pcsx-redux tools ship +// (nugget monitor/PROTOCOL.md). A 0x00 byte starts a frame and every other +// byte is console text. A frame is 0x00, sync 0x55AA, type, word count, the +// payload words, then a 32-bit Fletcher checksum, all 16-bit words little +// endian. The host PINGs until PONG, LOADs the program in 8 KiB frames each +// answered ACK, then RUNs it with its entry pc, gp and sp. No SDL. +#pragma once + +#include +#include +#include +#include + +#include "editor/serial.hh" +#include "editor/unirom.hh" + +namespace editor { + +namespace psxmon { + +enum : uint16_t { + Ping = 0x01, + ReadMem = 0x02, + WriteMem = 0x03, + GetRegs = 0x04, + SetReg = 0x05, + Load = 0x08, + Run = 0x09, + Cont = 0x0a, + Ack = 0x40, + Data = 0x41, + Regs = 0x42, + Pong = 0x43, + Error = 0x4f, + Stopped = 0x81, +}; + +// PONG capability bit: the monitor is entered from slot 4 of the kernel's +// exception handler, so it still sees a program's `break` after psyqo has +// emptied the kernel's handler chains. psxsplash uses break calls for its +// files exactly when this is set (pcdrv_handler.hh), and its own SIO1 +// protocol otherwise. +constexpr uint16_t kCapSlot = 0x0004; + +uint32_t fletcher(const uint16_t* words, size_t count); +// The whole frame as it goes on the wire, leading 0x00 included. +std::vector encode(uint16_t type, const std::vector& payload); + +} // namespace psxmon + +// PINGs for up to `timeoutMs`; true once the console answers PONG, with its +// capability bits in *caps. +bool psxmonPresent(Link& link, int timeoutMs, uint16_t* caps = nullptr); + +// Loads `exe` (a whole PS-X EXE file) and runs it. The console must already +// have answered a PING. False with *err when a frame is refused or not answered. +bool psxmonUpload(Link& link, const std::vector& exe, std::string* err, const UploadProgress& progress = {}, + const std::atomic* cancel = nullptr); + +// Serves the program psxmonUpload started: its console text goes to `line`, +// and its PCDRV calls (`break 0, 0x101`..`0x107`, which stop it in the +// monitor) are answered from `files` through the monitor's register and +// memory commands, then resumed. Runs until `cancel`, the link goes away, or +// the program exits (`break 4, 0`) or stops for any other reason. True on +// cancel or exit; false with *err otherwise. +bool psxmonServe(Link& link, PcdrvHost& files, const std::atomic& cancel, + const std::function& line, const PcdrvHost::Event& event, std::string* err); + +} // namespace editor diff --git a/src/editor/tests/editor_tests.cpp b/src/editor/tests/editor_tests.cpp index 91d44ef..1c95a3d 100644 --- a/src/editor/tests/editor_tests.cpp +++ b/src/editor/tests/editor_tests.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -25,6 +26,7 @@ #include "editor/pick.hh" #include "editor/play.hh" #include "editor/project.hh" +#include "editor/psxmon.hh" #include "editor/serial.hh" #include "editor/unirom.hh" #include "budget.hh" @@ -1040,11 +1042,22 @@ struct FakeConsole { uint8_t b[4] = {uint8_t(v), uint8_t(v >> 8), uint8_t(v >> 16), uint8_t(v >> 24)}; put(b, 4); } - std::string command() { // the shell echoes what it reads + // The shell matches the last four bytes it read against its commands and + // echoes a match, so anything else in front (a psxmon PING) is skipped. + std::string command() { char c[5] = {}; - if (!get(c, 4)) return {}; - put(c, 4); - return c; + for (int have = 0;;) { + uint8_t b; + if (!get(&b, 1)) return {}; + std::memmove(c, c + 1, 3); + c[3] = char(b); + if (++have < 4) continue; + for (const char* known : {"SEXE", "UPV2", "UPV3"}) + if (!std::memcmp(c, known, 4)) { + put(c, 4); + return c; + } + } } static uint32_t sum(const uint8_t* p, size_t n) { uint32_t s = 0; @@ -1222,6 +1235,283 @@ void runUnirom(int offer) { CHECK(!fs::exists(base.parent_path() / "outside")); } +// psxmon's half (nugget monitor/PROTOCOL.md): frames in, ACK/PONG/ERROR out, +// then RUN hands the link to the program, here the same psxsplash file calls. +struct FakeMonitor { + FakeConsole con; + std::map mem{}; + uint32_t pc = 0, gp = 0, sp = 0; + int pings = 0; + bool refuseFirstLoad = false; + uint16_t caps = 0x0007; + + bool frame(uint16_t* type, std::vector* words) { + uint8_t b = 1; + while (b != 0) + if (!con.get(&b, 1)) return false; + uint8_t h[6]; + if (!con.get(h, 6)) return false; + if (h[0] != 0xaa || h[1] != 0x55) { + con.fail("monitor: bad sync"); + return false; + } + *type = uint16_t(h[2] | h[3] << 8); + const uint16_t len = uint16_t(h[4] | h[5] << 8); + std::vector raw(2 * size_t(len) + 4); + if (!con.get(raw.data(), raw.size())) return false; + std::vector all{*type, len}; + for (size_t i = 0; i < len; ++i) all.push_back(uint16_t(raw[2 * i] | raw[2 * i + 1] << 8)); + const size_t c = 2 * size_t(len); + const uint32_t ck = uint32_t(raw[c] | raw[c + 1] << 8) | uint32_t(raw[c + 2] | raw[c + 3] << 8) << 16; + if (ck != editor::psxmon::fletcher(all.data(), all.size())) con.fail("monitor: bad checksum"); + words->assign(all.begin() + 2, all.end()); + return true; + } + void reply(uint16_t type, const std::vector& words = {}) { + const std::vector f = editor::psxmon::encode(type, words); + con.put(f.data(), f.size()); + } + // Serves commands until RUN. `afterRun` is written in the same burst as RUN's ACK. + bool session(const std::string& afterRun) { + for (;;) { + uint16_t type; + std::vector w; + if (!frame(&type, &w)) return false; + auto u32 = [&](size_t i) { return uint32_t(w[i]) | uint32_t(w[i + 1]) << 16; }; + if (type == editor::psxmon::Ping) { + ++pings; + reply(editor::psxmon::Pong, {2, caps, 0x1234, 0x5678}); + } else if (type == editor::psxmon::Load) { + if (refuseFirstLoad) { + refuseFirstLoad = false; + reply(editor::psxmon::Error, {3}); + return false; + } + const uint32_t addr = u32(0), n = u32(2); + if (w.size() != 4 + (n + 1) / 2) con.fail("monitor: LOAD length"); + for (uint32_t i = 0; i < n; ++i) mem[addr + i] = uint8_t(w[4 + i / 2] >> (i & 1 ? 8 : 0)); + reply(editor::psxmon::Ack); + } else if (type == editor::psxmon::Run) { + pc = u32(0), gp = u32(2), sp = u32(4); + std::vector burst = editor::psxmon::encode(editor::psxmon::Ack, {}); + burst.insert(burst.end(), afterRun.begin(), afterRun.end()); + con.put(burst.data(), burst.size()); + return true; + } else { + reply(editor::psxmon::Error, {1}); + } + } + } + + // The running program: a `break` stops it, and the host works on it from + // HALTED until CONT. Returns false if the host never resumes it. + uint32_t regs[38] = {}; + uint32_t lastEpc = 0; + std::vector> resumedAt{}; // (epc, pc on CONT) + bool brk(uint32_t code1, uint32_t code2, bool final = false) { + lastEpc += 0x40; + const uint32_t insn = code1 << 16 | code2 << 6 | 0x0d; + std::vector st{1}; + for (uint32_t v : {lastEpc, insn, 0u}) st.push_back(uint16_t(v)), st.push_back(uint16_t(v >> 16)); + reply(editor::psxmon::Stopped, st); + if (final) { // exit: the host reads a0 and is done; nothing resumes it + uint16_t type; + std::vector w; + if (!frame(&type, &w) || type != editor::psxmon::GetRegs) return false; + std::vector r; + for (uint32_t v : regs) r.push_back(uint16_t(v)), r.push_back(uint16_t(v >> 16)); + reply(editor::psxmon::Regs, r); + return true; + } + for (;;) { + uint16_t type; + std::vector w; + if (!frame(&type, &w)) return false; + auto u32 = [&](size_t i) { return i + 1 < w.size() ? uint32_t(w[i]) | uint32_t(w[i + 1]) << 16 : 0u; }; + if (type == editor::psxmon::GetRegs) { + std::vector r; + for (uint32_t v : regs) r.push_back(uint16_t(v)), r.push_back(uint16_t(v >> 16)); + reply(editor::psxmon::Regs, r); + } else if (type == editor::psxmon::ReadMem) { + const uint32_t addr = u32(0), len = u32(2); + for (uint32_t at = 0; at < len; at += 100) { // several DATA frames + const uint32_t n = std::min(100u, len - at); + std::vector d{uint16_t(n), 0}; + for (uint32_t i = 0; i < n; i += 2) + d.push_back(uint16_t(mem[addr + at + i] | mem[addr + at + i + 1] << 8)); + reply(editor::psxmon::Data, d); + } + } else if (type == editor::psxmon::WriteMem) { + const uint32_t addr = u32(0), n = u32(2); + for (uint32_t i = 0; i < n; ++i) mem[addr + i] = uint8_t(w[4 + i / 2] >> (i & 1 ? 8 : 0)); + reply(editor::psxmon::Ack); + } else if (type == editor::psxmon::SetReg) { + if (!w.empty() && w[0] < 38) regs[w[0]] = u32(1); + reply(editor::psxmon::Ack); + } else if (type == editor::psxmon::Cont) { + resumedAt.emplace_back(lastEpc, regs[37]); + reply(editor::psxmon::Ack); + return true; + } else { + con.fail("monitor: unexpected command while halted"); + reply(editor::psxmon::Error, {1}); + } + } + } + // pcdrv.h's calls; each returns what the wrapper would. + int32_t viaV1() { return regs[2] == 0 ? int32_t(regs[3]) : -1; } + void setName(const char* name) { + for (size_t i = 0; i <= std::strlen(name); ++i) mem[0x80100000 + i] = uint8_t(name[i]); + for (size_t i = std::strlen(name) + 1; i < 300; ++i) mem[0x80100000 + i] = 0xee; // junk after the NUL + } + int32_t pcInit() { return brk(0, 0x101) ? int32_t(regs[2]) : -99; } + int32_t pcOpen(const char* name) { + setName(name); + regs[4] = regs[5] = 0x80100000, regs[6] = 0; + return brk(0, 0x103) ? viaV1() : -99; + } + int32_t pcRead(uint32_t fd, uint32_t len, uint32_t buf) { + regs[4] = 0, regs[5] = fd, regs[6] = len, regs[7] = buf; + return brk(0, 0x105) ? viaV1() : -99; + } + int32_t pcSeek(uint32_t fd, int32_t off, uint32_t whence) { + regs[4] = regs[5] = fd, regs[6] = uint32_t(off), regs[7] = whence; + return brk(0, 0x107) ? viaV1() : -99; + } + int32_t pcClose(uint32_t fd) { + regs[4] = regs[5] = fd; + return brk(0, 0x104) ? int32_t(regs[2]) : -99; + } +}; + +void testPsxmon() { + namespace fs = std::filesystem; + // A PING and a LOAD byte for byte as a separate host implementation encodes them. + const std::vector ping = editor::psxmon::encode(editor::psxmon::Ping, {}); + const std::vector load = editor::psxmon::encode(editor::psxmon::Load, {0x10, 0x8001, 3, 0, 0x0201, 0x03}); + CHECK(ping == std::vector({0x00, 0xaa, 0x55, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00})); + CHECK(load == std::vector({0x00, 0xaa, 0x55, 0x08, 0x00, 0x06, 0x00, 0x10, 0x00, 0x01, 0x80, 0x03, 0x00, + 0x00, 0x00, 0x01, 0x02, 0x03, 0x00, 0x26, 0x82, 0xe2, 0x84})); + + const fs::path base = g_outDir / "psxmon"; + std::error_code ec; + fs::remove_all(base, ec); + fs::create_directories(base, ec); + std::vector scene(2500); + for (size_t i = 0; i < scene.size(); ++i) scene[i] = uint8_t(i * 5 + 3); + std::ofstream(base / "scene_0.splashpack", std::ios::binary).write(reinterpret_cast(scene.data()), scene.size()); + // A program over two LOAD frames, odd-sized, with a stack in the header. + std::vector exe(0x800 + 9001); + std::memcpy(exe.data(), "PS-X EXE", 8); + const uint32_t pc = 0x80010000, gp = 0x8001f000, addr = 0x80010000, tsize = 9001, sbase = 0x801fff00, ssize = 0xf0; + std::memcpy(&exe[0x10], &pc, 4); + std::memcpy(&exe[0x14], &gp, 4); + std::memcpy(&exe[0x18], &addr, 4); + std::memcpy(&exe[0x1c], &tsize, 4); + std::memcpy(&exe[0x30], &sbase, 4); + std::memcpy(&exe[0x34], &ssize, 4); + for (size_t i = 0x800; i < exe.size(); ++i) exe[i] = uint8_t(i * 11 + 1); + + Pipe toConsole, toHost; + PipeEnd hostEnd(toHost, toConsole), consoleEnd(toConsole, toHost); + FakeMonitor mon{FakeConsole{consoleEnd, 1, {}, {}, 0, 0}}; + std::vector readBack; + int32_t init = -9, h = -9, got = -9, pos = -9, tailGot = -9, closed = -9, missing = -9, outside = -9; + std::thread console([&] { + // The program prints straight after RUN's ACK, in the same burst. + if (mon.session("psxsplash: boot\r\n")) { + init = mon.pcInit(); + h = mon.pcOpen("scene_0.splashpack"); + got = mon.pcRead(uint32_t(h), 4096, 0x80120000); // more than the file holds + for (int32_t i = 0; i < got; ++i) readBack.push_back(mon.mem[0x80120000 + uint32_t(i)]); + pos = mon.pcSeek(uint32_t(h), 2490, 0); + tailGot = mon.pcRead(uint32_t(h), 64, 0x80130000); + closed = mon.pcClose(uint32_t(h)); + missing = mon.pcOpen("missing.bin"); + outside = mon.pcOpen("../outside"); + mon.con.say("done\n"); + mon.regs[4] = 7; + mon.brk(4, 0, true); // exit(7): the host reads a0 and stops serving + } + consoleEnd.close(); + }); + std::string err; + std::vector progress; + uint16_t caps = 0; + CHECK(editor::psxmonPresent(hostEnd, 2000, &caps) && caps == 0x0007); + CHECK(editor::psxmonUpload(hostEnd, exe, &err, [&](int p) { progress.push_back(p); })); + if (!err.empty()) std::fprintf(stderr, "psxmon upload: %s\n", err.c_str()); + std::vector lines, events; + std::atomic cancel{false}; + editor::PcdrvHost host(base); + const bool exited = editor::psxmonServe(hostEnd, host, cancel, [&](const std::string& l) { lines.push_back(l); }, + [&](const std::string& e) { events.push_back(e); }, &err); + if (!err.empty()) std::fprintf(stderr, "psxmon serve: %s\n", err.c_str()); + consoleEnd.close(); + console.join(); + for (const std::string& p : mon.con.problems) std::fprintf(stderr, "psxmon: %s\n", p.c_str()); + CHECK(mon.con.problems.empty()); + CHECK(mon.pings >= 1); + CHECK(mon.pc == pc && mon.gp == gp && mon.sp == sbase + ssize); + bool same = mon.mem.count(addr + tsize) == 0 && mon.mem.count(addr - 1) == 0; + for (uint32_t i = 0; same && i < tsize; ++i) same = mon.mem[addr + i] == exe[0x800 + i]; + CHECK(same); + CHECK(progress.size() == 2 && progress.back() == 100); + CHECK(std::find(lines.begin(), lines.end(), "psxsplash: boot") != lines.end()); + CHECK(init == 0 && h >= 3); + CHECK(got == 2500 && readBack.size() == 2500 && std::equal(scene.begin(), scene.end(), readBack.begin())); + CHECK(pos == 2490 && tailGot == 10 && mon.mem[0x80130009] == scene[2499] && mon.mem.count(0x8013000a) == 0); + CHECK(closed == 0 && missing == -1 && outside == -1); + CHECK(std::find(lines.begin(), lines.end(), "done") != lines.end()); + CHECK(exited && std::find(lines.begin(), lines.end(), "The program exited with code 7.") != lines.end()); + // Every call resumed past its break, and the exit was not resumed at all. + bool past = mon.resumedAt.size() == 8; + for (auto& [epc, next] : mon.resumedAt) past = past && next == epc + 4; + CHECK(past); + CHECK(!fs::exists(base.parent_path() / "outside")); + + // A refused LOAD stops the upload with the monitor's error code. + { + Pipe a, b; + PipeEnd h(a, b), c(b, a); + FakeMonitor m{FakeConsole{c, 1, {}, {}, 0, 0}}; + m.refuseFirstLoad = true; + std::thread t([&] { + m.session(""); + c.close(); + }); + CHECK(!editor::psxmonUpload(h, exe, &err) && err.find("refused LOAD") != std::string::npos && + err.find("error 3") != std::string::npos); + t.join(); + } + // A psxsplash from before break calls under psxmon: it tries its SIO1 + // protocol, and the host says why nothing loads. + { + Pipe a, b; + PipeEnd h(a, b), c(b, a); + FakeMonitor m{FakeConsole{c, 1, {}, {}, 0, 0}}; + std::thread t([&] { + const uint8_t escape[] = {0, 'p', 0x01, 0x01, 0, 0}; + m.con.put(escape, sizeof escape); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + c.close(); + }); + std::vector seen; + std::atomic stop{false}; + editor::PcdrvHost files(base); + editor::psxmonServe(h, files, stop, [&](const std::string& l) { seen.push_back(l); }, {}, &err); + t.join(); + CHECK(std::count_if(seen.begin(), seen.end(), + [](const std::string& l) { return l.find("over SIO1") != std::string::npos; }) == 1); + } + // Nothing answers the PING: not present, so Unirom gets its turn. + { + Pipe a, b; + PipeEnd h(a, b); + CHECK(!editor::psxmonPresent(h, 600)); + } +} + // HardwareRun: the editor's session, over a pipe to the same fake console. class Borrowed : public editor::Link { public: @@ -1233,6 +1523,48 @@ class Borrowed : public editor::Link { editor::Link& l_; }; +// HardwareRun picks the file protocol from the monitor's caps: break calls +// with the slot, psxsplash's SIO1 protocol without it. +void runHardwarePsxmon(uint16_t caps) { + namespace fs = std::filesystem; + const fs::path dir = g_outDir / "hwrun-psxmon"; + std::error_code ec; + fs::create_directories(dir, ec); + std::ofstream(dir / "scene_0.splashpack", std::ios::binary) << "scene"; + std::vector exe(0x800 + 64); + std::memcpy(exe.data(), "PS-X EXE", 8); + const fs::path exePath = dir / "engine.ps-exe"; + std::ofstream(exePath, std::ios::binary).write(reinterpret_cast(exe.data()), exe.size()); + + Pipe toConsole, toHost; + PipeEnd hostEnd(toHost, toConsole), consoleEnd(toConsole, toHost); + FakeMonitor mon{FakeConsole{consoleEnd, 1, {}, {}, 0, 0}}; + mon.caps = caps; + int32_t h = -9; + std::thread console([&] { + if (!mon.session("")) return; + if (caps & editor::psxmon::kCapSlot) { + h = mon.pcOpen("scene_0.splashpack"); + } else { + uint32_t u = 0; + if (mon.con.open("scene_0.splashpack", &u)) h = int32_t(u); + } + }); + editor::HardwareRun run; + run.start([&](std::string*) { return std::make_unique(hostEnd); }, exePath, dir); + console.join(); + for (int i = 0; i < 300; ++i) { + auto l = run.lines(); + if (std::find(l.begin(), l.end(), "[file] open scene_0.splashpack -> 3") != l.end()) break; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + auto l = run.lines(); + CHECK(h == 3 && std::find(l.begin(), l.end(), "Uploading psxsplash through psxmon...") != l.end()); + CHECK(mon.con.problems.empty()); + run.stop(); + consoleEnd.close(); +} + void testHardwareRun() { namespace fs = std::filesystem; const fs::path dir = g_outDir / "hwrun"; @@ -1327,7 +1659,10 @@ int main(int argc, char** argv) { testProject(); testAdoptAsset(); testUnirom(); + testPsxmon(); testHardwareRun(); + runHardwarePsxmon(0x0007); + runHardwarePsxmon(0x0001); if (g_failures) { std::fprintf(stderr, "editor_tests: %d of %d checks failed\n", g_failures, g_checks); return 1; diff --git a/src/editor/unirom.cpp b/src/editor/unirom.cpp index 8e3e9ab..31f2829 100644 --- a/src/editor/unirom.cpp +++ b/src/editor/unirom.cpp @@ -366,63 +366,32 @@ bool PcdrvHost::handleCall(Link& raw, const std::function " + std::to_string(h)); - return (sendText(link, "OKAY") && sendWord(link, h)) || closed(); + const int32_t h = open(name, event); + if (h < 0) return refuse(link) || closed(); + return (sendText(link, "OKAY") && sendWord(link, uint32_t(h))) || closed(); } uint32_t a1, a2, a3; if (!in.u32(&a1) || !in.u32(&a2) || !in.u32(&a3)) return false; - auto it = files_.find(a1); - std::FILE* f = it == files_.end() ? nullptr : it->second; - const std::string h = std::to_string(a1); switch (op) { case kClose: - if (f) { - std::fclose(f); - files_.erase(it); - } - report("close " + h); + close(a1, event); return (sendText(link, "OKAY") && sendWord(link, a1)) || closed(); case kSeek: { - const int whence = a3 == 1 ? SEEK_CUR : a3 == 2 ? SEEK_END : SEEK_SET; - if (!f || std::fseek(f, long(int32_t(a2)), whence) != 0) { - report("seek " + h + ": failed"); - return refuse(link) || closed(); - } - const long pos = std::ftell(f); - report("seek " + h + " -> " + std::to_string(pos)); + const int32_t pos = seek(a1, int32_t(a2), a3, event); + if (pos < 0) return refuse(link) || closed(); return (sendText(link, "OKAY") && sendWord(link, uint32_t(pos))) || closed(); } case kRead: { - if (!f || a2 > kMaxTransfer) { - report("read " + h + ": refused"); - return refuse(link) || closed(); - } + std::vector data; + if (read(a1, a2, &data, event) < 0) return refuse(link) || closed(); // The console reads exactly the count it is told, so the count is what the file had. - std::vector data(a2); - data.resize(std::fread(data.data(), 1, a2, f)); - if (!sendText(link, "OKAY") || !sendWord(link, uint32_t(data.size())) || - !sendWord(link, byteSum(data.data(), data.size())) || !link.write(data.data(), data.size())) - return closed(); - report("read " + h + ": " + std::to_string(data.size()) + " of " + std::to_string(a2) + " bytes"); - return true; + return (sendText(link, "OKAY") && sendWord(link, uint32_t(data.size())) && + sendWord(link, byteSum(data.data(), data.size())) && link.write(data.data(), data.size())) || + closed(); } default: @@ -435,4 +404,58 @@ bool PcdrvHost::handleCall(Link& raw, const std::function " + std::to_string(h)); + return int32_t(h); +} + +int32_t PcdrvHost::close(uint32_t handle, const Event& event) { + auto it = files_.find(handle); + if (event) event("close " + std::to_string(handle)); + if (it == files_.end()) return -1; + std::fclose(it->second); + files_.erase(it); + return 0; +} + +int32_t PcdrvHost::seek(uint32_t handle, int32_t offset, uint32_t whence, const Event& event) { + auto it = files_.find(handle); + const std::string h = std::to_string(handle); + const int w = whence == 1 ? SEEK_CUR : whence == 2 ? SEEK_END : SEEK_SET; + if (it == files_.end() || std::fseek(it->second, long(offset), w) != 0) { + if (event) event("seek " + h + ": failed"); + return -1; + } + const long pos = std::ftell(it->second); + if (event) event("seek " + h + " -> " + std::to_string(pos)); + return int32_t(pos); +} + +int32_t PcdrvHost::read(uint32_t handle, uint32_t len, std::vector* data, const Event& event) { + auto it = files_.find(handle); + const std::string h = std::to_string(handle); + if (it == files_.end() || len > kMaxTransfer) { + if (event) event("read " + h + ": refused"); + return -1; + } + data->resize(len); + data->resize(std::fread(data->data(), 1, len, it->second)); + if (event) event("read " + h + ": " + std::to_string(data->size()) + " of " + std::to_string(len) + " bytes"); + return int32_t(data->size()); +} + } // namespace editor diff --git a/src/editor/unirom.hh b/src/editor/unirom.hh index de3fc7d..00abaad 100644 --- a/src/editor/unirom.hh +++ b/src/editor/unirom.hh @@ -45,6 +45,15 @@ class PcdrvHost { bool serve(Link& link, const std::atomic& cancel, const std::function& line, const std::function& event, std::string* err); + using Event = std::function; + // The file calls themselves, for a host that carries the arguments some + // other way (psxmon's break calls). Each returns what the console is told, + // a handle, a byte count or a position, or -1, and reports one line. + int32_t open(const std::string& name, const Event& event); + int32_t close(uint32_t handle, const Event& event); + int32_t read(uint32_t handle, uint32_t len, std::vector* data, const Event& event); + int32_t seek(uint32_t handle, int32_t offset, uint32_t whence, const Event& event); + private: bool handleCall(Link& link, const std::function& event, std::string* err); std::filesystem::path resolve(const std::string& name, bool* ok) const;