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/. // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include <algorithm> #include <algorithm>
#include <atomic>
#include <mutex> #include <mutex>
#include <vector> #include <vector>
@@ -436,28 +437,46 @@ int Camera::getMaxFrameSize() {
return framesize; return framesize;
} }
// Re-encodes a frame at lower quality until it fits maxSize, like the PSP camera compresses to the // The JPEG quality that fit the last frame. Frames of one scene are similar in size, so it's usually
// game's framesize. Most platforms' capture code encodes at a fixed quality, so this is the common fallback. // 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) { static bool RecompressToFit(const unsigned char *image, long long length, int maxSize, std::vector<uint8_t> *out) {
int width = 0, height = 0, comps = 0; int width = 0, height = 0, comps = 0;
unsigned char *rgb = jpgd::decompress_jpeg_image_from_memory(image, (int)length, &width, &height, &comps, 3); unsigned char *rgb = jpgd::decompress_jpeg_image_from_memory(image, (int)length, &width, &height, &comps, 3);
if (!rgb) { if (!rgb) {
return false; return false;
} }
bool fits = false;
out->resize(width * height * 3 + 1024); out->resize(width * height * 3 + 1024);
int size = Camera::encodeToFit(maxSize, [&](int quality) {
jpge::params params; jpge::params params;
for (int quality = 70; quality >= 10; quality -= 15) {
int size = (int)out->size();
params.m_quality = quality; params.m_quality = quality;
if (jpge::compress_image_to_jpeg_file_in_memory(out->data(), size, width, height, 3, rgb, params) && size <= maxSize) { int outSize = (int)out->size();
out->resize(size); return jpge::compress_image_to_jpeg_file_in_memory(out->data(), outSize, width, height, 3, rgb, params) ? outSize : -1;
fits = true; });
break;
}
}
free(rgb); 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) { void Camera::pushCameraImage(long long length, unsigned char* image) {
+4
View File
@@ -18,6 +18,7 @@
#pragma once #pragma once
#include <stdint.h> #include <stdint.h>
#include <functional>
#include <string> #include <string>
#include <vector> #include <vector>
#include "Core/HLE/FunctionWrappers.h" #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 // The largest JPEG frame the game accepts (framesize in the video setup). The PSP camera compresses
// to fit it, so capture code should too. // to fit it, so capture code should too.
int getMaxFrameSize(); 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 // Compress image to jpeg from RGB24. Like the PSP camera, fit the size the game asked for:
// fits the size the game asked for: Go!Edit stores frames in fixed 15KB slots. // Go!Edit stores frames in fixed 15KB slots.
const int maxFrameSize = Camera::getMaxFrameSize(); imgJpegSize = Camera::encodeToFit(Camera::getMaxFrameSize(), [&](int quality) {
jpge::params params; jpge::params params;
for (int quality = 85; ; quality -= 10) {
imgJpegSize = device->imgJpegSize;
params.m_quality = quality; params.m_quality = quality;
jpge::compress_image_to_jpeg_file_in_memory( int size = device->imgJpegSize;
device->imageJpeg, imgJpegSize, return jpge::compress_image_to_jpeg_file_in_memory(
dstW, device->imageJpeg, size, dstW, dstH, 3, device->imageRGB, params) ? size : -1;
dstH, });
3, if (imgJpegSize < 0) {
device->imageRGB, params); imgJpegSize = 0;
if (imgJpegSize <= maxFrameSize || quality <= 15) {
break;
}
} }
} }
#endif #endif