mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-01 14:58:14 +00:00
Merge pull request #22385 from hrydgard/goedit-camera-hang
Fix Go!Edit camera hang, and some other related issues
This commit is contained in:
7 files changed
+202
-32
No files matched your search
+89
-1
@@ -16,7 +16,9 @@
|
||||
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#include "ppsspp_config.h"
|
||||
|
||||
@@ -27,8 +29,11 @@
|
||||
#include "Core/HLE/HLE.h"
|
||||
#include "Core/HLE/sceUsbCam.h"
|
||||
#include "Core/HLE/sceUsbMic.h"
|
||||
#include "Core/CoreTiming.h"
|
||||
#include "Core/HW/Camera.h"
|
||||
#include "Core/MemMapHelpers.h"
|
||||
#include "ext/jpge/jpgd.h"
|
||||
#include "ext/jpge/jpge.h"
|
||||
|
||||
#if defined(_WIN32) && !PPSSPP_PLATFORM(UWP) && !defined(__LIBRETRO__)
|
||||
#define HAVE_WIN32_CAMERA
|
||||
@@ -43,6 +48,8 @@ Camera::Config *config;
|
||||
|
||||
unsigned int videoBufferLength = 0;
|
||||
unsigned int nextVideoFrame = 0;
|
||||
// When video capture started, which the frame clock counts from. Not saved in states, it only sets the phase.
|
||||
static u64 videoStartUs = 0;
|
||||
uint8_t *videoBuffer;
|
||||
std::mutex videoBufferMutex;
|
||||
|
||||
@@ -206,6 +213,7 @@ static int sceUsbCamStartVideo() {
|
||||
jpegData = nullptr;
|
||||
}
|
||||
|
||||
videoStartUs = CoreTiming::GetGlobalTimeUs();
|
||||
Camera::startCapture();
|
||||
return 0;
|
||||
}
|
||||
@@ -215,13 +223,29 @@ static int sceUsbCamStopVideo() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// How often the camera delivers a frame, from the framerate in the setup params
|
||||
// (PSPSDK's PSP_USBCAM_FRAMERATE_*: 3.75, 5, 7.5, 10, 15, 20, 30 and 60 fps).
|
||||
static int getFrameIntervalUs() {
|
||||
static const int intervalsUs[] = { 266667, 200000, 133333, 100000, 66667, 50000, 33333, 16667 };
|
||||
int framerate = config->type == Camera::ConfigType::CfVideoEx ? config->videoExParam.framerate : config->videoParam.framerate;
|
||||
if (framerate < 0 || framerate >= (int)ARRAY_SIZE(intervalsUs)) {
|
||||
framerate = 6; // 30 fps
|
||||
}
|
||||
return intervalsUs[framerate];
|
||||
}
|
||||
|
||||
static int sceUsbCamReadVideoFrameBlocking(u32 bufAddr, u32 size) {
|
||||
std::lock_guard<std::mutex> lock(videoBufferMutex);
|
||||
u32 transferSize = std::min(videoBufferLength, size);
|
||||
if (Memory::IsValidRange(bufAddr, size)) {
|
||||
Memory::Memcpy(bufAddr, videoBuffer, transferSize);
|
||||
}
|
||||
return transferSize;
|
||||
// This blocks until the camera's next frame. Returning at once lets a high-priority capture thread
|
||||
// (Go!Edit's bhCameraGetJpeg) spin in its read loop and starve the rest of the game.
|
||||
const int intervalUs = getFrameIntervalUs();
|
||||
const u64 sinceStartUs = CoreTiming::GetGlobalTimeUs() - videoStartUs;
|
||||
const int waitUs = intervalUs - (int)(sinceStartUs % intervalUs);
|
||||
return hleDelayResult(hleLogDebug(Log::HLE, transferSize), "camera frame", waitUs);
|
||||
}
|
||||
|
||||
static int sceUsbCamReadVideoFrame(u32 bufAddr, u32 size) {
|
||||
@@ -398,7 +422,71 @@ void Camera::onCameraDeviceChange() {
|
||||
}
|
||||
}
|
||||
|
||||
int Camera::getMaxFrameSize() {
|
||||
int framesize = 0;
|
||||
if (config) {
|
||||
if (config->type == Camera::ConfigType::CfVideoEx) {
|
||||
framesize = config->videoExParam.framesize;
|
||||
} else if (config->type == Camera::ConfigType::CfVideo) {
|
||||
framesize = config->videoParam.framesize;
|
||||
}
|
||||
}
|
||||
if (framesize <= 0 || framesize > VIDEO_BUFFER_SIZE) {
|
||||
return VIDEO_BUFFER_SIZE;
|
||||
}
|
||||
return framesize;
|
||||
}
|
||||
|
||||
// The JPEG quality that fit the last frame. Frames of one scene are similar in size, so it's usually
|
||||
// right first time. Only the capture thread uses it, but atomic in case a platform has several.
|
||||
static std::atomic<int> g_jpegQuality{ 80 };
|
||||
|
||||
int Camera::encodeToFit(int maxSize, const std::function<int(int quality)> &encode) {
|
||||
int quality = g_jpegQuality;
|
||||
int size = encode(quality);
|
||||
while ((size < 0 || size > maxSize) && quality > 10) {
|
||||
quality = std::max(10, quality - 10);
|
||||
size = encode(quality);
|
||||
}
|
||||
// Way under the limit: try a better quality next time.
|
||||
if (size >= 0 && size < maxSize / 2 && quality < 90) {
|
||||
quality += 10;
|
||||
}
|
||||
g_jpegQuality = quality;
|
||||
return size;
|
||||
}
|
||||
|
||||
// Re-encodes a frame until it fits maxSize, like the PSP camera compresses to the game's framesize.
|
||||
// Most platforms' capture code encodes at a fixed quality, so this is the common fallback.
|
||||
static bool RecompressToFit(const unsigned char *image, long long length, int maxSize, std::vector<uint8_t> *out) {
|
||||
int width = 0, height = 0, comps = 0;
|
||||
unsigned char *rgb = jpgd::decompress_jpeg_image_from_memory(image, (int)length, &width, &height, &comps, 3);
|
||||
if (!rgb) {
|
||||
return false;
|
||||
}
|
||||
out->resize(width * height * 3 + 1024);
|
||||
int size = Camera::encodeToFit(maxSize, [&](int quality) {
|
||||
jpge::params params;
|
||||
params.m_quality = quality;
|
||||
int outSize = (int)out->size();
|
||||
return jpge::compress_image_to_jpeg_file_in_memory(out->data(), outSize, width, height, 3, rgb, params) ? outSize : -1;
|
||||
});
|
||||
free(rgb);
|
||||
if (size < 0 || size > maxSize) {
|
||||
return false;
|
||||
}
|
||||
out->resize(size);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Camera::pushCameraImage(long long length, unsigned char* image) {
|
||||
std::vector<uint8_t> recompressed;
|
||||
const int maxSize = getMaxFrameSize();
|
||||
if (length > maxSize && RecompressToFit(image, length, maxSize, &recompressed)) {
|
||||
image = recompressed.data();
|
||||
length = (long long)recompressed.size();
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(videoBufferMutex);
|
||||
if (!videoBuffer) {
|
||||
return;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "Core/HLE/FunctionWrappers.h"
|
||||
@@ -121,4 +122,10 @@ namespace Camera {
|
||||
int startCapture();
|
||||
int stopCapture();
|
||||
void pushCameraImage(long long length, unsigned char *image);
|
||||
// The largest JPEG frame the game accepts (framesize in the video setup). The PSP camera compresses
|
||||
// to fit it, so capture code should too.
|
||||
int getMaxFrameSize();
|
||||
// Calls encode(quality), which returns the JPEG size or -1, starting from the quality that fit the
|
||||
// previous frame and going down until the result fits maxSize. Returns the final size (or -1).
|
||||
int encodeToFit(int maxSize, const std::function<int(int quality)> &encode);
|
||||
}
|
||||
+17
-10
@@ -69,16 +69,22 @@ static void __MicBlockingResume(u64 userdata, int cyclesLate) {
|
||||
}
|
||||
|
||||
if (Microphone::isHaveDevice()) {
|
||||
if (Microphone::getReadMicDataLength() >= iter->needSize) {
|
||||
u32 ret = __KernelGetWaitValue(threadID, error);
|
||||
DEBUG_LOG(Log::HLE, "sceUsbMic: Waking up thread(%d)", (int)iter->threadID);
|
||||
__KernelResumeThreadFromWait(threadID, ret);
|
||||
iter = waitingThreads.erase(iter);
|
||||
} else {
|
||||
u64 waitTimeus = (iter->needSize - Microphone::getReadMicDataLength()) * 1000000 / 2 / iter->sampleRate;
|
||||
CoreTiming::ScheduleEvent(usToCycles(waitTimeus), eventMicBlockingResume, userdata);
|
||||
iter++;
|
||||
// The PSP's mic delivers in real time, so the read completes when the samples are due in
|
||||
// emulated time. Waiting for the host instead hangs the game if its mic never delivers
|
||||
// (Go!Edit's recording stalled that way), so fill what's missing with silence.
|
||||
const u32 needSize = (u32)iter->needSize;
|
||||
const u32 have = std::min((u32)Microphone::getReadMicDataLength(), needSize);
|
||||
if (have < needSize) {
|
||||
DEBUG_LOG(Log::HLE, "sceUsbMic: host mic only delivered %d of %d bytes, padding with silence", have, needSize);
|
||||
if (Memory::IsValidRange(iter->addr + have, needSize - have)) {
|
||||
Memory::Memset(iter->addr + have, 0, needSize - have, "MicSilence");
|
||||
}
|
||||
readMicDataLength = needSize;
|
||||
}
|
||||
u32 ret = __KernelGetWaitValue(threadID, error);
|
||||
DEBUG_LOG(Log::HLE, "sceUsbMic: Waking up thread(%d)", (int)iter->threadID);
|
||||
__KernelResumeThreadFromWait(threadID, ret);
|
||||
iter = waitingThreads.erase(iter);
|
||||
} else {
|
||||
for (int i = 0; i < iter->needSize; i++) {
|
||||
if (Memory::IsValidAddress(iter->addr + i)) {
|
||||
@@ -346,7 +352,8 @@ int Microphone::stopMic() {
|
||||
|
||||
bool Microphone::isHaveDevice() {
|
||||
#ifdef HAVE_WIN32_MICROPHONE
|
||||
return winMic->getDeviceCounts() >= 1;
|
||||
// Only the app creates winMic, headless doesn't.
|
||||
return winMic && winMic->getDeviceCounts() >= 1;
|
||||
#elif PPSSPP_PLATFORM(ANDROID)
|
||||
return System_AudioRecordingIsAvailable();
|
||||
#endif
|
||||
|
||||
@@ -90,8 +90,10 @@ trips would otherwise hang the script forever, so it gives up after `--sync-time
|
||||
(default 30), reports it, and makes the run exit non-zero.
|
||||
|
||||
Matching is by ticket, always - `--sync` never waits for "whatever message arrives next", which is
|
||||
what used to quietly desynchronise a script. A raw JSON line (the only way to send nested
|
||||
parameters) is sent exactly as written, so it's waited for only if *you* gave it a `ticket`;
|
||||
what used to quietly desynchronise a script. So prefer the `key=value` form for nested parameters
|
||||
too: values are parsed as JSON, and single quotes keep the inner double quotes intact, as in
|
||||
`input.buttons.send buttons='{"cross":true}'`. A raw JSON line is sent exactly as written, so it's
|
||||
waited for only if *you* gave it a `ticket`;
|
||||
without one there is nothing to match and `--sync` moves straight on to the next line. Raw lines
|
||||
are rejected up front, rather than sent and left to fail somewhere downstream, if they aren't valid
|
||||
JSON, aren't an object, have no string `event`, or carry a `ticket` that isn't an integer.
|
||||
@@ -115,6 +117,7 @@ way took minutes per run. These run inside the one session instead:
|
||||
|---|---|
|
||||
| `:sleep <seconds>` | Wall-clock pause. Keeps draining and printing messages while it waits. |
|
||||
| `:wait <event> [timeout]` | Blocks until a message with that event name arrives. Exits non-zero if it never does. |
|
||||
| `:screenshot <file.png>` | Saves `gpu.buffer.screenshot` as a PNG, without printing the image data. Needs the CPU stopped. Use a native path (`C:/...`) on Windows. With headless, use `--graphics=software`: the Vulkan backend has no output image to read back there. |
|
||||
| `:echo <text>` | Prints text, for marking up a script's output. |
|
||||
| `# comment` | Ignored. |
|
||||
|
||||
|
||||
+63
-6
@@ -528,6 +528,8 @@ fn print_help() {
|
||||
println!("wsdbg - connected. Type an event name and optional key=value params, e.g.:");
|
||||
println!(" game.status");
|
||||
println!(" cpu.setReg thread=0 name=4 value=1000");
|
||||
println!("Values are parsed as JSON where possible. Single-quote nested ones, which keeps the ticket:");
|
||||
println!(" input.buttons.send buttons='{{\"cross\":true}}'");
|
||||
println!("Or paste a full JSON message starting with '{{' to send it verbatim.");
|
||||
println!("A numeric 'ticket' is auto-assigned to shorthand commands so you can match up responses.");
|
||||
println!(":help show this message");
|
||||
@@ -535,6 +537,7 @@ fn print_help() {
|
||||
println!(":snapshot <name> <addr> <size> memory.read into a locally-named byte buffer");
|
||||
println!(":snapshots list saved snapshots");
|
||||
println!(":diff <name1> <name2> byte-compare two snapshots");
|
||||
println!(":screenshot <file.png> save gpu.buffer.screenshot to a PNG file");
|
||||
println!(":sleep <seconds> pause, still printing anything that arrives");
|
||||
println!(":wait <event> [timeout] block until that event arrives (e.g. cpu.stepping)");
|
||||
println!(":echo <text> print text, for marking up a script's output");
|
||||
@@ -678,6 +681,7 @@ fn send_and_wait(
|
||||
event: &str,
|
||||
params: &[String],
|
||||
timeout_secs: f64,
|
||||
print_response: bool,
|
||||
) -> Result<serde_json::Value> {
|
||||
let ticket = next_ticket();
|
||||
let json_text = build_event_json(event, params, Some(ticket))?;
|
||||
@@ -688,11 +692,13 @@ fn send_and_wait(
|
||||
while Instant::now() < deadline {
|
||||
match socket.read() {
|
||||
Ok(Message::Text(text)) => {
|
||||
print_incoming(&text);
|
||||
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||
if v.get("ticket").and_then(|t| t.as_u64()) == Some(ticket) {
|
||||
return Ok(v);
|
||||
}
|
||||
let v = serde_json::from_str::<serde_json::Value>(&text).ok();
|
||||
let is_ours = v.as_ref().and_then(|v| v.get("ticket")).and_then(|t| t.as_u64()) == Some(ticket);
|
||||
if !is_ours || print_response {
|
||||
print_incoming(&text);
|
||||
}
|
||||
if is_ours {
|
||||
return Ok(v.unwrap());
|
||||
}
|
||||
}
|
||||
Ok(Message::Close(frame)) => return Err(anyhow!("connection closed by PPSSPP: {frame:?}")),
|
||||
@@ -724,7 +730,7 @@ fn cmd_snapshot(socket: &mut WebSocket<TcpStream>, snapshots: &mut Snapshots, ar
|
||||
}
|
||||
let name = args[0];
|
||||
let params = vec![format!("address={}", args[1]), format!("size={}", args[2])];
|
||||
match send_and_wait(socket, "memory.read", ¶ms, timeout_secs) {
|
||||
match send_and_wait(socket, "memory.read", ¶ms, timeout_secs, true) {
|
||||
Ok(resp) => {
|
||||
if resp.get("event").and_then(|e| e.as_str()) == Some("error") {
|
||||
let msg = resp.get("message").and_then(|m| m.as_str()).unwrap_or("unknown error");
|
||||
@@ -751,6 +757,51 @@ fn cmd_snapshot(socket: &mut WebSocket<TcpStream>, snapshots: &mut Snapshots, ar
|
||||
}
|
||||
}
|
||||
|
||||
// :screenshot <file.png> - saves what gpu.buffer.screenshot returns as a PNG file. The response
|
||||
// (a data: URI of the whole image) isn't printed, it would bury everything else in the output.
|
||||
fn cmd_screenshot(socket: &mut WebSocket<TcpStream>, args: &[&str], timeout_secs: f64) -> bool {
|
||||
if args.len() != 1 {
|
||||
eprintln!("! Usage: :screenshot <file.png>");
|
||||
return false;
|
||||
}
|
||||
let path = args[0];
|
||||
let resp = match send_and_wait(socket, "gpu.buffer.screenshot", &["type=uri".to_string()], timeout_secs, false) {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
eprintln!("! {e}");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
if resp.get("event").and_then(|e| e.as_str()) == Some("error") {
|
||||
let msg = resp.get("message").and_then(|m| m.as_str()).unwrap_or("unknown error");
|
||||
eprintln!("! gpu.buffer.screenshot failed: {msg}");
|
||||
return false;
|
||||
}
|
||||
let uri = resp.get("uri").and_then(|u| u.as_str()).unwrap_or("");
|
||||
let b64 = match uri.split_once(";base64,") {
|
||||
Some((_, b)) => b,
|
||||
None => {
|
||||
eprintln!("! Response had no base64 data: URI");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let bytes = match base64::engine::general_purpose::STANDARD.decode(b64) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
eprintln!("! Could not decode the image: {e}");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
if let Err(e) = std::fs::write(path, &bytes) {
|
||||
eprintln!("! Could not write {path}: {e}");
|
||||
return false;
|
||||
}
|
||||
let w = resp.get("width").and_then(|w| w.as_u64()).unwrap_or(0);
|
||||
let h = resp.get("height").and_then(|h| h.as_u64()).unwrap_or(0);
|
||||
println!("screenshot saved: {path} ({w}x{h})");
|
||||
true
|
||||
}
|
||||
|
||||
// :snapshots - list what's been captured so far in this session.
|
||||
fn cmd_list_snapshots(snapshots: &Snapshots) {
|
||||
if snapshots.is_empty() {
|
||||
@@ -914,6 +965,12 @@ fn run_repl(mut socket: WebSocket<TcpStream>, sync: bool, sync_timeout: f64) ->
|
||||
cmd_snapshot(&mut socket, &mut snapshots, &args, sync_timeout);
|
||||
}
|
||||
Some(":snapshots") => cmd_list_snapshots(&snapshots),
|
||||
Some(":screenshot") => {
|
||||
let args: Vec<&str> = words.collect();
|
||||
if !cmd_screenshot(&mut socket, &args, sync_timeout) {
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
Some(":diff") => {
|
||||
let args: Vec<&str> = words.collect();
|
||||
cmd_diff(&snapshots, &args);
|
||||
|
||||
@@ -247,13 +247,18 @@ HRESULT ReaderCallback::OnReadSample(
|
||||
}
|
||||
}
|
||||
|
||||
// Compress image to jpeg from RGB24.
|
||||
jpge::compress_image_to_jpeg_file_in_memory(
|
||||
device->imageJpeg, imgJpegSize,
|
||||
dstW,
|
||||
dstH,
|
||||
3,
|
||||
device->imageRGB);
|
||||
// Compress image to jpeg from RGB24. Like the PSP camera, fit the size the game asked for:
|
||||
// Go!Edit stores frames in fixed 15KB slots.
|
||||
imgJpegSize = Camera::encodeToFit(Camera::getMaxFrameSize(), [&](int quality) {
|
||||
jpge::params params;
|
||||
params.m_quality = quality;
|
||||
int size = device->imgJpegSize;
|
||||
return jpge::compress_image_to_jpeg_file_in_memory(
|
||||
device->imageJpeg, size, dstW, dstH, 3, device->imageRGB, params) ? size : -1;
|
||||
});
|
||||
if (imgJpegSize < 0) {
|
||||
imgJpegSize = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Camera::pushCameraImage(imgJpegSize, device->imageJpeg);
|
||||
|
||||
+9
-6
@@ -96,16 +96,19 @@ A working invocation, and the traps around it:
|
||||
- Some events deliberately never respond while the CPU is stepping, so `--sync` will burn its full timeout on them:
|
||||
`gpu.stats.get` and `gpu.stats.feed` (documented - they answer after the next flip), `gpu.record.dump`, and
|
||||
`input.buttons.press` (waits for N frames). Resume the CPU first, or skip them in scripted runs.
|
||||
- Log broadcasts drown scripted output. Send this first:
|
||||
`{"event":"broadcast.config.set","disallowed":{"logger":true,"input":true}}`. Note `wsdbg`'s `key=value` shorthand
|
||||
can't build nested objects - paste raw JSON lines (any line starting with `{` is sent verbatim) for those.
|
||||
- Log broadcasts drown scripted output. Pass `--quiet` to wsdbg, which turns them off.
|
||||
- **Nested parameters work in wsdbg's `key=value` shorthand**: values are parsed as JSON, and single quotes keep
|
||||
the inner double quotes, e.g. `input.buttons.send buttons='{"cross":true}'`. Prefer that over a raw JSON line,
|
||||
which gets no ticket (see below).
|
||||
- Keep wsdbg scripts in files and pipe them in, rather than building JSON inline in a shell command - inline
|
||||
`{"event":...}` in a bash heredoc trips Claude Code's command analyzer ("brace with quote character") and forces a
|
||||
manual approval prompt for every single invocation.
|
||||
- **`--sync` can only match a response to a request that carries a ticket**, and wsdbg only assigns tickets to its
|
||||
`key=value` shorthand. A raw JSON line (needed for nested params) gets no ticket, so `--sync` just waits for the
|
||||
next message and treats whatever broadcast arrives first as the answer, silently desynchronising the rest of the
|
||||
script. Use the shorthand wherever the parameters are flat. Hex works there: `memory.disasm address=0x08804000`.
|
||||
`key=value` shorthand. A raw JSON line without a `ticket` isn't waited for at all, so use the shorthand (nested
|
||||
values included, see above). Hex works there: `memory.disasm address=0x08804000`.
|
||||
- **To see the screen from a script, use wsdbg's `:screenshot <file.png>`** while the CPU is stopped, e.g. after a
|
||||
`cpu.runUntilTime`. With headless, run `--graphics=software` for it: headless Vulkan has no output image to read
|
||||
back and asserts. Give a native path on Windows (`C:/...`, not `/c/...`).
|
||||
- **Headless reports `SYSPROP_HAS_DEBUGGER` as false** (only `Windows/main.cpp` implements it), so anything gated on
|
||||
it does nothing there - `LoadSymbolsIfSupported()` in `Core/System.cpp`, for instance, doesn't load `.ppmap`/`.sym`
|
||||
at all under headless. Gate new debugger-adjacent features on their own config flag, not on that property.
|
||||
|
||||
Reference in new issue
Block a user