Add display mode options: stretch, aspect ratio, and integer scaling

Renders the emulator framebuffer into a calculated sub-rect with
nearest-neighbor scaling and black bars, driven by the core's
reported aspect ratio. Adds a cycle button in the game HUD and
a display mode setting in the settings screen.
This commit is contained in:
2026-04-08 20:48:52 -04:00
parent 39e3f8b7c0
commit 14045accb5
11 changed files with 214 additions and 15 deletions

View File

@@ -102,6 +102,11 @@ Java_com_lazy_emulate_emulation_NativeLibretro_nativeSetFrameSkip(JNIEnv*, jobje
LibretroFrontend::instance().setFrameSkip(skip);
}
JNIEXPORT void JNICALL
Java_com_lazy_emulate_emulation_NativeLibretro_nativeSetDisplayMode(JNIEnv*, jobject, jint mode) {
LibretroFrontend::instance().setDisplayMode(mode);
}
JNIEXPORT void JNICALL
Java_com_lazy_emulate_emulation_NativeLibretro_nativeSetSystemDir(JNIEnv* env, jobject, jstring dir) {
const char* d = env->GetStringUTFChars(dir, nullptr);

View File

@@ -120,9 +120,16 @@ bool LibretroFrontend::loadGame(const char* romPath) {
target_fps_ = av_info.timing.fps;
double sample_rate = av_info.timing.sample_rate;
LOGI("Game loaded - %ux%u @ %.1f fps, audio %.0f Hz",
core_base_width_ = av_info.geometry.base_width;
core_base_height_ = av_info.geometry.base_height;
core_aspect_ratio_ = av_info.geometry.aspect_ratio;
if (core_aspect_ratio_ <= 0.0f && core_base_height_ > 0) {
core_aspect_ratio_ = static_cast<float>(core_base_width_) / static_cast<float>(core_base_height_);
}
LOGI("Game loaded - %ux%u @ %.1f fps, audio %.0f Hz, aspect %.3f",
av_info.geometry.base_width, av_info.geometry.base_height,
target_fps_, sample_rate);
target_fps_, sample_rate, core_aspect_ratio_);
// Start audio
audio_engine_.start(static_cast<int32_t>(sample_rate));
@@ -274,18 +281,100 @@ void LibretroFrontend::videoRefreshCallback(const void* data, unsigned width, un
break;
}
ANativeWindow_setBuffersGeometry(fe.window_, width, height, windowFormat);
DisplayMode mode = fe.display_mode_.load();
if (mode == DisplayMode::STRETCH) {
// Original behavior: set buffer geometry to core output, let surface stretch
ANativeWindow_setBuffersGeometry(fe.window_, width, height, windowFormat);
ANativeWindow_Buffer buffer;
if (ANativeWindow_lock(fe.window_, &buffer, nullptr) != 0) return;
auto* dst = static_cast<uint8_t*>(buffer.bits);
auto* src = static_cast<const uint8_t*>(data);
size_t dstStride = buffer.stride * bytesPerPixel;
size_t copyWidth = width * bytesPerPixel;
for (unsigned y = 0; y < height; y++) {
memcpy(dst + y * dstStride, src + y * pitch, copyWidth);
}
ANativeWindow_unlockAndPost(fe.window_);
return;
}
// For ASPECT_RATIO and INTEGER_SCALE, we render into a buffer matching
// the surface size and blit the frame into a calculated sub-rect.
int surfaceWidth = ANativeWindow_getWidth(fe.window_);
int surfaceHeight = ANativeWindow_getHeight(fe.window_);
if (surfaceWidth <= 0 || surfaceHeight <= 0) return;
// Set buffer geometry to match the full surface
ANativeWindow_setBuffersGeometry(fe.window_, surfaceWidth, surfaceHeight, windowFormat);
ANativeWindow_Buffer buffer;
if (ANativeWindow_lock(fe.window_, &buffer, nullptr) != 0) return;
auto* dst = static_cast<uint8_t*>(buffer.bits);
auto* src = static_cast<const uint8_t*>(data);
size_t dstStride = buffer.stride * bytesPerPixel;
size_t copyWidth = width * bytesPerPixel;
for (unsigned y = 0; y < height; y++) {
memcpy(dst + y * dstStride, src + y * pitch, copyWidth);
// Clear entire buffer to black
for (int y = 0; y < buffer.height; y++) {
memset(dst + y * dstStride, 0, buffer.width * bytesPerPixel);
}
// Calculate destination rect
int destX = 0, destY = 0, destW = surfaceWidth, destH = surfaceHeight;
if (mode == DisplayMode::ASPECT_RATIO) {
float aspect = fe.core_aspect_ratio_;
if (aspect <= 0.0f) {
aspect = (height > 0) ? static_cast<float>(width) / static_cast<float>(height) : 1.0f;
}
float surfaceAspect = static_cast<float>(surfaceWidth) / static_cast<float>(surfaceHeight);
if (surfaceAspect > aspect) {
// Pillarbox: surface is wider than content
destH = surfaceHeight;
destW = static_cast<int>(surfaceHeight * aspect + 0.5f);
} else {
// Letterbox: surface is taller than content
destW = surfaceWidth;
destH = static_cast<int>(surfaceWidth / aspect + 0.5f);
}
destX = (surfaceWidth - destW) / 2;
destY = (surfaceHeight - destH) / 2;
} else if (mode == DisplayMode::INTEGER_SCALE) {
int scaleX = surfaceWidth / static_cast<int>(width);
int scaleY = surfaceHeight / static_cast<int>(height);
int scale = (scaleX < scaleY) ? scaleX : scaleY;
if (scale < 1) scale = 1;
destW = width * scale;
destH = height * scale;
destX = (surfaceWidth - destW) / 2;
destY = (surfaceHeight - destH) / 2;
}
// Clamp destination rect to buffer bounds
if (destX < 0) destX = 0;
if (destY < 0) destY = 0;
if (destX + destW > buffer.width) destW = buffer.width - destX;
if (destY + destH > buffer.height) destH = buffer.height - destY;
// Nearest-neighbor scale blit
auto* src = static_cast<const uint8_t*>(data);
for (int dy = 0; dy < destH; dy++) {
unsigned srcY = dy * height / destH;
if (srcY >= height) srcY = height - 1;
const uint8_t* srcRow = src + srcY * pitch;
uint8_t* dstRow = dst + (destY + dy) * dstStride + destX * bytesPerPixel;
for (int dx = 0; dx < destW; dx++) {
unsigned srcX = dx * width / destW;
if (srcX >= width) srcX = width - 1;
memcpy(dstRow + dx * bytesPerPixel, srcRow + srcX * bytesPerPixel, bytesPerPixel);
}
}
ANativeWindow_unlockAndPost(fe.window_);
@@ -451,7 +540,14 @@ bool LibretroFrontend::environmentCallback(unsigned cmd, void* data) {
}
case RETRO_ENVIRONMENT_SET_GEOMETRY: {
auto* geom = static_cast<const retro_game_geometry*>(data);
LOGD("Geometry changed: %ux%u", geom->base_width, geom->base_height);
fe.core_base_width_ = geom->base_width;
fe.core_base_height_ = geom->base_height;
if (geom->aspect_ratio > 0.0f) {
fe.core_aspect_ratio_ = geom->aspect_ratio;
} else if (geom->base_height > 0) {
fe.core_aspect_ratio_ = static_cast<float>(geom->base_width) / static_cast<float>(geom->base_height);
}
LOGD("Geometry changed: %ux%u, aspect %.3f", geom->base_width, geom->base_height, fe.core_aspect_ratio_);
return true;
}
case RETRO_ENVIRONMENT_GET_LANGUAGE: {
@@ -536,6 +632,13 @@ void LibretroFrontend::setAudioEnabled(bool enabled) {
audio_engine_.setEnabled(enabled);
}
void LibretroFrontend::setDisplayMode(int mode) {
if (mode >= 0 && mode <= 2) {
display_mode_.store(static_cast<DisplayMode>(mode));
LOGI("Display mode set to: %d", mode);
}
}
void LibretroFrontend::setFrameSkip(int skip) {
frame_skip_ = skip;
if (skip > 0) {

View File

@@ -10,6 +10,12 @@
#include <thread>
#include <unordered_map>
enum class DisplayMode : int {
STRETCH = 0,
ASPECT_RATIO = 1,
INTEGER_SCALE = 2
};
class LibretroFrontend {
public:
static LibretroFrontend& instance();
@@ -33,6 +39,7 @@ public:
void setAudioEnabled(bool enabled);
void setFrameSkip(int skip);
void setDisplayMode(int mode);
void setSystemDir(const char* dir);
void setSaveDir(const char* dir);
@@ -90,6 +97,10 @@ private:
ANativeWindow* window_ = nullptr;
std::mutex window_mutex_;
unsigned pixel_format_ = RETRO_PIXEL_FORMAT_RGB565;
std::atomic<DisplayMode> display_mode_{DisplayMode::STRETCH};
float core_aspect_ratio_ = 0.0f;
unsigned core_base_width_ = 0;
unsigned core_base_height_ = 0;
// Audio
AudioEngine audio_engine_;