Camera: Remember the JPEG quality that fit the last frame

Start from the quality that fit the previous frame instead of the top, so
most frames encode once. Step down while a frame is too big, and step back
up when one comes out under half the limit. Windows and the recompression
fallback share the logic.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-09-29 12:29:04 -06:00
1 parent 07556d064e
commit 9e54cebcc2
3 files changed
+44 -26

No files matched your search

+31 -12
View File
@@ -16,6 +16,7 @@
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include <algorithm>
#include <atomic>
#include <mutex>
#include <vector>
@@ -436,28 +437,46 @@ int Camera::getMaxFrameSize() {
return framesize;
}
// Re-encodes a frame at lower quality 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.
// 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;
}
bool fits = false;
out->resize(width * height * 3 + 1024);
int size = Camera::encodeToFit(maxSize, [&](int quality) {
jpge::params params;
for (int quality = 70; quality >= 10; quality -= 15) {
int size = (int)out->size();
params.m_quality = quality;
if (jpge::compress_image_to_jpeg_file_in_memory(out->data(), size, width, height, 3, rgb, params) && size <= maxSize) {
out->resize(size);
fits = true;
break;
}
}
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);
return fits;
if (size < 0 || size > maxSize) {
return false;
}
out->resize(size);
return true;
}
void Camera::pushCameraImage(long long length, unsigned char* image) {
+4
View File
@@ -18,6 +18,7 @@
#pragma once
#include <stdint.h>
#include <functional>
#include <string>
#include <vector>
#include "Core/HLE/FunctionWrappers.h"
@@ -124,4 +125,7 @@ namespace Camera {
// 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);
}
+9 -14
View File
@@ -247,22 +247,17 @@ HRESULT ReaderCallback::OnReadSample(
}
}
// Compress image to jpeg from RGB24. Like the PSP camera, lower the quality until the frame
// fits the size the game asked for: Go!Edit stores frames in fixed 15KB slots.
const int maxFrameSize = Camera::getMaxFrameSize();
// 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;
for (int quality = 85; ; quality -= 10) {
imgJpegSize = device->imgJpegSize;
params.m_quality = quality;
jpge::compress_image_to_jpeg_file_in_memory(
device->imageJpeg, imgJpegSize,
dstW,
dstH,
3,
device->imageRGB, params);
if (imgJpegSize <= maxFrameSize || quality <= 15) {
break;
}
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