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:
@@ -102,6 +102,11 @@ Java_com_lazy_emulate_emulation_NativeLibretro_nativeSetFrameSkip(JNIEnv*, jobje
|
|||||||
LibretroFrontend::instance().setFrameSkip(skip);
|
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
|
JNIEXPORT void JNICALL
|
||||||
Java_com_lazy_emulate_emulation_NativeLibretro_nativeSetSystemDir(JNIEnv* env, jobject, jstring dir) {
|
Java_com_lazy_emulate_emulation_NativeLibretro_nativeSetSystemDir(JNIEnv* env, jobject, jstring dir) {
|
||||||
const char* d = env->GetStringUTFChars(dir, nullptr);
|
const char* d = env->GetStringUTFChars(dir, nullptr);
|
||||||
|
|||||||
@@ -120,9 +120,16 @@ bool LibretroFrontend::loadGame(const char* romPath) {
|
|||||||
target_fps_ = av_info.timing.fps;
|
target_fps_ = av_info.timing.fps;
|
||||||
double sample_rate = av_info.timing.sample_rate;
|
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,
|
av_info.geometry.base_width, av_info.geometry.base_height,
|
||||||
target_fps_, sample_rate);
|
target_fps_, sample_rate, core_aspect_ratio_);
|
||||||
|
|
||||||
// Start audio
|
// Start audio
|
||||||
audio_engine_.start(static_cast<int32_t>(sample_rate));
|
audio_engine_.start(static_cast<int32_t>(sample_rate));
|
||||||
@@ -274,18 +281,100 @@ void LibretroFrontend::videoRefreshCallback(const void* data, unsigned width, un
|
|||||||
break;
|
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;
|
ANativeWindow_Buffer buffer;
|
||||||
if (ANativeWindow_lock(fe.window_, &buffer, nullptr) != 0) return;
|
if (ANativeWindow_lock(fe.window_, &buffer, nullptr) != 0) return;
|
||||||
|
|
||||||
auto* dst = static_cast<uint8_t*>(buffer.bits);
|
auto* dst = static_cast<uint8_t*>(buffer.bits);
|
||||||
auto* src = static_cast<const uint8_t*>(data);
|
|
||||||
size_t dstStride = buffer.stride * bytesPerPixel;
|
size_t dstStride = buffer.stride * bytesPerPixel;
|
||||||
size_t copyWidth = width * bytesPerPixel;
|
|
||||||
|
|
||||||
for (unsigned y = 0; y < height; y++) {
|
// Clear entire buffer to black
|
||||||
memcpy(dst + y * dstStride, src + y * pitch, copyWidth);
|
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_);
|
ANativeWindow_unlockAndPost(fe.window_);
|
||||||
@@ -451,7 +540,14 @@ bool LibretroFrontend::environmentCallback(unsigned cmd, void* data) {
|
|||||||
}
|
}
|
||||||
case RETRO_ENVIRONMENT_SET_GEOMETRY: {
|
case RETRO_ENVIRONMENT_SET_GEOMETRY: {
|
||||||
auto* geom = static_cast<const retro_game_geometry*>(data);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
case RETRO_ENVIRONMENT_GET_LANGUAGE: {
|
case RETRO_ENVIRONMENT_GET_LANGUAGE: {
|
||||||
@@ -536,6 +632,13 @@ void LibretroFrontend::setAudioEnabled(bool enabled) {
|
|||||||
audio_engine_.setEnabled(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) {
|
void LibretroFrontend::setFrameSkip(int skip) {
|
||||||
frame_skip_ = skip;
|
frame_skip_ = skip;
|
||||||
if (skip > 0) {
|
if (skip > 0) {
|
||||||
|
|||||||
@@ -10,6 +10,12 @@
|
|||||||
#include <thread>
|
#include <thread>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
|
enum class DisplayMode : int {
|
||||||
|
STRETCH = 0,
|
||||||
|
ASPECT_RATIO = 1,
|
||||||
|
INTEGER_SCALE = 2
|
||||||
|
};
|
||||||
|
|
||||||
class LibretroFrontend {
|
class LibretroFrontend {
|
||||||
public:
|
public:
|
||||||
static LibretroFrontend& instance();
|
static LibretroFrontend& instance();
|
||||||
@@ -33,6 +39,7 @@ public:
|
|||||||
|
|
||||||
void setAudioEnabled(bool enabled);
|
void setAudioEnabled(bool enabled);
|
||||||
void setFrameSkip(int skip);
|
void setFrameSkip(int skip);
|
||||||
|
void setDisplayMode(int mode);
|
||||||
|
|
||||||
void setSystemDir(const char* dir);
|
void setSystemDir(const char* dir);
|
||||||
void setSaveDir(const char* dir);
|
void setSaveDir(const char* dir);
|
||||||
@@ -90,6 +97,10 @@ private:
|
|||||||
ANativeWindow* window_ = nullptr;
|
ANativeWindow* window_ = nullptr;
|
||||||
std::mutex window_mutex_;
|
std::mutex window_mutex_;
|
||||||
unsigned pixel_format_ = RETRO_PIXEL_FORMAT_RGB565;
|
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
|
// Audio
|
||||||
AudioEngine audio_engine_;
|
AudioEngine audio_engine_;
|
||||||
|
|||||||
@@ -4,38 +4,45 @@ enum class ConsoleType(
|
|||||||
val displayName: String,
|
val displayName: String,
|
||||||
val manufacturer: String,
|
val manufacturer: String,
|
||||||
val fileExtensions: List<String>,
|
val fileExtensions: List<String>,
|
||||||
val isImplemented: Boolean = false
|
val isImplemented: Boolean = false,
|
||||||
|
val fallbackAspectRatio: Float = 4f / 3f
|
||||||
) {
|
) {
|
||||||
PS1(
|
PS1(
|
||||||
displayName = "PlayStation",
|
displayName = "PlayStation",
|
||||||
manufacturer = "Sony",
|
manufacturer = "Sony",
|
||||||
fileExtensions = listOf("bin", "cue", "iso", "img", "pbp", "chd"),
|
fileExtensions = listOf("bin", "cue", "iso", "img", "pbp", "chd"),
|
||||||
isImplemented = true
|
isImplemented = true,
|
||||||
|
fallbackAspectRatio = 4f / 3f
|
||||||
),
|
),
|
||||||
NES(
|
NES(
|
||||||
displayName = "Nintendo Entertainment System",
|
displayName = "Nintendo Entertainment System",
|
||||||
manufacturer = "Nintendo",
|
manufacturer = "Nintendo",
|
||||||
fileExtensions = listOf("nes", "unf", "unif")
|
fileExtensions = listOf("nes", "unf", "unif"),
|
||||||
|
fallbackAspectRatio = 256f / 240f
|
||||||
),
|
),
|
||||||
SNES(
|
SNES(
|
||||||
displayName = "Super Nintendo",
|
displayName = "Super Nintendo",
|
||||||
manufacturer = "Nintendo",
|
manufacturer = "Nintendo",
|
||||||
fileExtensions = listOf("sfc", "smc", "fig", "swc")
|
fileExtensions = listOf("sfc", "smc", "fig", "swc"),
|
||||||
|
fallbackAspectRatio = 4f / 3f
|
||||||
),
|
),
|
||||||
N64(
|
N64(
|
||||||
displayName = "Nintendo 64",
|
displayName = "Nintendo 64",
|
||||||
manufacturer = "Nintendo",
|
manufacturer = "Nintendo",
|
||||||
fileExtensions = listOf("n64", "z64", "v64", "ndd")
|
fileExtensions = listOf("n64", "z64", "v64", "ndd"),
|
||||||
|
fallbackAspectRatio = 4f / 3f
|
||||||
),
|
),
|
||||||
GENESIS(
|
GENESIS(
|
||||||
displayName = "Sega Genesis",
|
displayName = "Sega Genesis",
|
||||||
manufacturer = "Sega",
|
manufacturer = "Sega",
|
||||||
fileExtensions = listOf("gen", "md", "smd", "bin")
|
fileExtensions = listOf("gen", "md", "smd", "bin"),
|
||||||
|
fallbackAspectRatio = 4f / 3f
|
||||||
),
|
),
|
||||||
PS2(
|
PS2(
|
||||||
displayName = "PlayStation 2",
|
displayName = "PlayStation 2",
|
||||||
manufacturer = "Sony",
|
manufacturer = "Sony",
|
||||||
fileExtensions = listOf("iso", "bin", "chd", "cso")
|
fileExtensions = listOf("iso", "bin", "chd", "cso"),
|
||||||
|
fallbackAspectRatio = 4f / 3f
|
||||||
);
|
);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
12
app/src/main/java/com/lazy/emulate/emulation/DisplayMode.kt
Normal file
12
app/src/main/java/com/lazy/emulate/emulation/DisplayMode.kt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package com.lazy.emulate.emulation
|
||||||
|
|
||||||
|
enum class DisplayMode(val nativeValue: Int, val label: String) {
|
||||||
|
STRETCH(0, "Stretch"),
|
||||||
|
ASPECT_RATIO(1, "Aspect Ratio"),
|
||||||
|
INTEGER_SCALE(2, "Integer Scale");
|
||||||
|
|
||||||
|
fun next(): DisplayMode {
|
||||||
|
val values = entries
|
||||||
|
return values[(ordinal + 1) % values.size]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,4 +36,6 @@ interface EmulatorCore {
|
|||||||
fun setAudioEnabled(enabled: Boolean)
|
fun setAudioEnabled(enabled: Boolean)
|
||||||
|
|
||||||
fun setFrameSkip(skip: Int)
|
fun setFrameSkip(skip: Int)
|
||||||
|
|
||||||
|
fun setDisplayMode(mode: DisplayMode)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ object NativeLibretro {
|
|||||||
external fun nativeLoadState(path: String): Boolean
|
external fun nativeLoadState(path: String): Boolean
|
||||||
external fun nativeSetAudioEnabled(enabled: Boolean)
|
external fun nativeSetAudioEnabled(enabled: Boolean)
|
||||||
external fun nativeSetFrameSkip(skip: Int)
|
external fun nativeSetFrameSkip(skip: Int)
|
||||||
|
external fun nativeSetDisplayMode(mode: Int)
|
||||||
external fun nativeSetSystemDir(dir: String)
|
external fun nativeSetSystemDir(dir: String)
|
||||||
external fun nativeSetSaveDir(dir: String)
|
external fun nativeSetSaveDir(dir: String)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.lazy.emulate.emulation.cores
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Surface
|
import android.view.Surface
|
||||||
import com.lazy.emulate.emulation.ConsoleType
|
import com.lazy.emulate.emulation.ConsoleType
|
||||||
|
import com.lazy.emulate.emulation.DisplayMode
|
||||||
import com.lazy.emulate.emulation.EmulatorCore
|
import com.lazy.emulate.emulation.EmulatorCore
|
||||||
import com.lazy.emulate.input.GamepadButton
|
import com.lazy.emulate.input.GamepadButton
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ class PlaceholderCore(override val consoleType: ConsoleType) : EmulatorCore {
|
|||||||
override fun loadState(slot: Int): Boolean = false
|
override fun loadState(slot: Int): Boolean = false
|
||||||
override fun setAudioEnabled(enabled: Boolean) {}
|
override fun setAudioEnabled(enabled: Boolean) {}
|
||||||
override fun setFrameSkip(skip: Int) {}
|
override fun setFrameSkip(skip: Int) {}
|
||||||
|
override fun setDisplayMode(mode: DisplayMode) {}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "PlaceholderCore"
|
private const val TAG = "PlaceholderCore"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Surface
|
import android.view.Surface
|
||||||
import com.lazy.emulate.emulation.ConsoleType
|
import com.lazy.emulate.emulation.ConsoleType
|
||||||
|
import com.lazy.emulate.emulation.DisplayMode
|
||||||
import com.lazy.emulate.emulation.EmulatorCore
|
import com.lazy.emulate.emulation.EmulatorCore
|
||||||
import com.lazy.emulate.emulation.NativeLibretro
|
import com.lazy.emulate.emulation.NativeLibretro
|
||||||
import com.lazy.emulate.input.GamepadButton
|
import com.lazy.emulate.input.GamepadButton
|
||||||
@@ -126,6 +127,10 @@ class Ps1Core(private val context: Context) : EmulatorCore {
|
|||||||
NativeLibretro.nativeSetFrameSkip(skip)
|
NativeLibretro.nativeSetFrameSkip(skip)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setDisplayMode(mode: DisplayMode) {
|
||||||
|
NativeLibretro.nativeSetDisplayMode(mode.nativeValue)
|
||||||
|
}
|
||||||
|
|
||||||
fun releaseSurface() {
|
fun releaseSurface() {
|
||||||
NativeLibretro.nativeReleaseSurface()
|
NativeLibretro.nativeReleaseSurface()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Pause
|
import androidx.compose.material.icons.filled.Pause
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material.icons.filled.AspectRatio
|
||||||
import androidx.compose.material.icons.filled.Save
|
import androidx.compose.material.icons.filled.Save
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -29,6 +30,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import com.lazy.emulate.data.model.Game
|
import com.lazy.emulate.data.model.Game
|
||||||
|
import com.lazy.emulate.emulation.DisplayMode
|
||||||
import com.lazy.emulate.emulation.EmulationEngine
|
import com.lazy.emulate.emulation.EmulationEngine
|
||||||
import com.lazy.emulate.emulation.EmulatorCore
|
import com.lazy.emulate.emulation.EmulatorCore
|
||||||
import com.lazy.emulate.emulation.cores.Ps1Core
|
import com.lazy.emulate.emulation.cores.Ps1Core
|
||||||
@@ -45,6 +47,7 @@ fun GameScreen(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var core by remember { mutableStateOf<EmulatorCore?>(null) }
|
var core by remember { mutableStateOf<EmulatorCore?>(null) }
|
||||||
var isPaused by remember { mutableStateOf(false) }
|
var isPaused by remember { mutableStateOf(false) }
|
||||||
|
var displayMode by remember { mutableStateOf(DisplayMode.STRETCH) }
|
||||||
var loadError by remember { mutableStateOf<String?>(null) }
|
var loadError by remember { mutableStateOf<String?>(null) }
|
||||||
val activeController by controllerManager.activeController.collectAsState()
|
val activeController by controllerManager.activeController.collectAsState()
|
||||||
val showTouchControls = activeController == null
|
val showTouchControls = activeController == null
|
||||||
@@ -177,5 +180,21 @@ fun GameScreen(
|
|||||||
tint = Color.White.copy(alpha = 0.7f)
|
tint = Color.White.copy(alpha = 0.7f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
displayMode = displayMode.next()
|
||||||
|
core?.setDisplayMode(displayMode)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(top = 8.dp, end = 88.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.AspectRatio,
|
||||||
|
contentDescription = "Display: ${displayMode.label}",
|
||||||
|
tint = Color.White.copy(alpha = 0.7f)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.lazy.emulate.ui.screens.settings
|
package com.lazy.emulate.ui.screens.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -27,9 +28,13 @@ import androidx.compose.material3.TopAppBar
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.lazy.emulate.emulation.DisplayMode
|
||||||
import com.lazy.emulate.input.ControllerManager
|
import com.lazy.emulate.input.ControllerManager
|
||||||
import com.lazy.emulate.input.GameController
|
import com.lazy.emulate.input.GameController
|
||||||
|
|
||||||
@@ -141,6 +146,33 @@ fun SettingsScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Display section
|
||||||
|
Text(
|
||||||
|
text = "Display",
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
var displayMode by remember { mutableStateOf(DisplayMode.STRETCH) }
|
||||||
|
|
||||||
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { displayMode = displayMode.next() }
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text("Display Mode", style = MaterialTheme.typography.bodyLarge)
|
||||||
|
Text(
|
||||||
|
displayMode.label,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
// About
|
// About
|
||||||
Text(
|
Text(
|
||||||
text = "About",
|
text = "About",
|
||||||
|
|||||||
Reference in New Issue
Block a user