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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user