diff --git a/app/src/legacy/jniLibs/arm64-v8a/libvulkan_renderer.so b/app/src/legacy/jniLibs/arm64-v8a/libvulkan_renderer.so index 429b4019c9..15d8fdfd38 100755 Binary files a/app/src/legacy/jniLibs/arm64-v8a/libvulkan_renderer.so and b/app/src/legacy/jniLibs/arm64-v8a/libvulkan_renderer.so differ diff --git a/app/src/main/cpp/winlator/VulkanRendererContext.cpp b/app/src/main/cpp/winlator/VulkanRendererContext.cpp index e6821bee20..8058fe3f81 100644 --- a/app/src/main/cpp/winlator/VulkanRendererContext.cpp +++ b/app/src/main/cpp/winlator/VulkanRendererContext.cpp @@ -775,6 +775,9 @@ void VulkanRendererContext::recordCmdBuf(VkCommandBuffer cb, uint32_t imgIdx, pc.gamma = activeGamma; pc.outW = std::max(1.0f, (float)d.w * sx / cw * (float)swapchainExt.width); pc.outH = std::max(1.0f, (float)d.h * sy / ch * (float)swapchainExt.height); + pc.barrelStrength = activeBarrelStrength; + pc.barrelHeight = activeBarrelHeight; + pc.barrelCylindricalRatio = activeBarrelCylindricalRatio; vk_.CmdPushConstants(cb, pipeLayout, VK_SHADER_STAGE_VERTEX_BIT|VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(pc), &pc); vk_.CmdDraw(cb, 4, 1, 0, 0); } @@ -797,6 +800,9 @@ void VulkanRendererContext::recordCmdBuf(VkCommandBuffer cb, uint32_t imgIdx, cpc.gamma = 1.0f; cpc.outW = (float)std::max(1, (int)curW); cpc.outH = (float)std::max(1, (int)curH); + cpc.barrelStrength = 0.0f; + cpc.barrelHeight = 0.176327f; + cpc.barrelCylindricalRatio = 0.5f; vk_.CmdPushConstants(cb, pipeLayout, VK_SHADER_STAGE_VERTEX_BIT|VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(cpc), &cpc); vk_.CmdDraw(cb, 4, 1, 0, 0); } @@ -1245,15 +1251,22 @@ void VulkanRendererContext::setSwapRB(bool enabled) { } -void VulkanRendererContext::setEffect(int effectId, float sharpness, int effectMask, float brightness, float contrast, float gamma) { +void VulkanRendererContext::setEffect(int effectId, float sharpness, int effectMask, + float brightness, float contrast, float gamma, + float barrelStrength, float barrelHeight, + float barrelCylindricalRatio) { activeEffectId = effectId; activeSharpness = std::max(0.0f, std::min(1.0f, sharpness)); activeEffectMask = effectMask; activeBrightness = std::max(-1.0f, std::min(1.0f, brightness)); activeContrast = std::max(-1.0f, std::min(1.0f, contrast)); activeGamma = std::max(0.1f, std::min(4.0f, gamma)); - RLOG("setEffect: id=%d sharpness=%.3f mask=%d brightness=%.3f contrast=%.3f gamma=%.3f", - activeEffectId, activeSharpness, activeEffectMask, activeBrightness, activeContrast, activeGamma); + activeBarrelStrength = std::max(0.0f, std::min(1.0f, barrelStrength)); + activeBarrelHeight = std::max(0.05f, barrelHeight); + activeBarrelCylindricalRatio = std::max(0.5f, std::min(1.5f, barrelCylindricalRatio)); + RLOG("setEffect: id=%d sharpness=%.3f mask=%d brightness=%.3f contrast=%.3f gamma=%.3f barrelStrength=%.3f barrelHeight=%.4f barrelCylindricalRatio=%.3f", + activeEffectId, activeSharpness, activeEffectMask, activeBrightness, activeContrast, activeGamma, + activeBarrelStrength, activeBarrelHeight, activeBarrelCylindricalRatio); needsRender.store(true); dirtyCV.notify_one(); } diff --git a/app/src/main/cpp/winlator/VulkanRendererContext.h b/app/src/main/cpp/winlator/VulkanRendererContext.h index c1b13c8973..95d5ec3827 100644 --- a/app/src/main/cpp/winlator/VulkanRendererContext.h +++ b/app/src/main/cpp/winlator/VulkanRendererContext.h @@ -124,6 +124,9 @@ struct WindowPushConstants { float gamma; float outW; // on-screen quad width in pixels (for FSR/EASU upscale ratio) float outH; // on-screen quad height in pixels + float barrelStrength; + float barrelHeight; + float barrelCylindricalRatio; }; class VulkanRendererContext { @@ -175,7 +178,10 @@ class VulkanRendererContext { void setFilterMode(int mode); void setSwapRB(bool enabled); - void setEffect(int effectId, float sharpness, int effectMask, float brightness, float contrast, float gamma); + void setEffect(int effectId, float sharpness, int effectMask, float brightness, float contrast, float gamma, + float barrelStrength = 0.0f, + float barrelHeight = 0.176327f, + float barrelCylindricalRatio = 0.5f); void setPresentMode(VkPresentModeKHR mode); std::vector getSupportedPresentModes() const; @@ -218,6 +224,9 @@ class VulkanRendererContext { float activeBrightness = 0.0f; float activeContrast = 0.0f; float activeGamma = 1.0f; + float activeBarrelStrength = 0.0f; + float activeBarrelHeight = 0.176327f; + float activeBarrelCylindricalRatio = 0.5f; float maxAnisotropy = 1.0f; bool cubicSupported = false; VkPhysicalDeviceMemoryProperties memProperties{}; diff --git a/app/src/main/cpp/winlator/vulkan_jni.cpp b/app/src/main/cpp/winlator/vulkan_jni.cpp index bb1fb111b1..f7964fedc3 100644 --- a/app/src/main/cpp/winlator/vulkan_jni.cpp +++ b/app/src/main/cpp/winlator/vulkan_jni.cpp @@ -252,7 +252,8 @@ Java_com_winlator_renderer_VulkanRenderer_nativeSetPresentMode(JNIEnv*, jobject, extern "C" JNIEXPORT void JNICALL Java_com_winlator_renderer_VulkanRenderer_nativeSetEffect( JNIEnv*, jobject, jlong handle, jint effectId, jfloat sharpness, - jint effectMask, jfloat brightness, jfloat contrast, jfloat gamma) { + jint effectMask, jfloat brightness, jfloat contrast, jfloat gamma, + jfloat barrelStrength, jfloat barrelHeight, jfloat barrelCylindricalRatio) { auto* r = reinterpret_cast(handle); if (r) r->setEffect( (int)effectId, @@ -260,7 +261,10 @@ Java_com_winlator_renderer_VulkanRenderer_nativeSetEffect( (int)effectMask, (float)brightness, (float)contrast, - (float)gamma + (float)gamma, + (float)barrelStrength, + (float)barrelHeight, + (float)barrelCylindricalRatio ); } diff --git a/app/src/main/cpp/winlator/window.frag b/app/src/main/cpp/winlator/window.frag index 7e83b510cd..e2225c24a7 100644 --- a/app/src/main/cpp/winlator/window.frag +++ b/app/src/main/cpp/winlator/window.frag @@ -16,9 +16,15 @@ layout(push_constant) uniform PC { float gamma; float outW; float outH; + float barrelStrength; + float barrelHeight; + float barrelCylindricalRatio; } pc; -layout(location = 0) in vec2 fragTexCoord; +layout(location = 0) in vec2 fragTexCoord; +layout(location = 1) in vec3 vUVBarrel; +layout(location = 2) in vec2 vUVDotBarrel; + layout(location = 0) out vec4 outColor; const int EFFECT_MASK_TOON = 1; @@ -26,6 +32,7 @@ const int EFFECT_MASK_FXAA = 2; const int EFFECT_MASK_VIVID = 4; const int EFFECT_MASK_CRT = 8; const int EFFECT_MASK_NTSC = 16; +const int EFFECT_MASK_BARREL_DISTORTION = 32; bool hasEffect(int mask) { return (pc.effectMask & mask) != 0; @@ -264,6 +271,8 @@ vec3 applyNTSC(vec2 uv, vec3 color) { return clamp(mix(color, shifted + vec3(bleed), 0.65), 0.0, 1.0); } + + void main() { vec2 uv = fragTexCoord; vec4 src = texture(texSampler, uv); @@ -284,7 +293,12 @@ void main() { if (hasEffect(EFFECT_MASK_VIVID)) rgb = applyVivid(rgb); if (hasEffect(EFFECT_MASK_CRT)) rgb = applyCRTOverlay(uv, rgb); if (hasEffect(EFFECT_MASK_NTSC)) rgb = applyNTSC(uv, rgb); + if (hasEffect(EFFECT_MASK_BARREL_DISTORTION)) { + vec3 puv = dot(vUVDotBarrel, vUVDotBarrel) * vec3(-0.5, -0.5, -1.0) + vUVBarrel; + rgb = texture(texSampler, puv.xy / puv.z).rgb; + } } + outColor = vec4(rgb, pc.useTexAlpha != 0 ? src.a : 1.0); } diff --git a/app/src/main/cpp/winlator/window.vert b/app/src/main/cpp/winlator/window.vert index f85fbad65a..3184e4e67d 100644 --- a/app/src/main/cpp/winlator/window.vert +++ b/app/src/main/cpp/winlator/window.vert @@ -5,9 +5,25 @@ layout(push_constant) uniform PC { float ndcY0; float ndcX1; float ndcY1; + int useTexAlpha; + int effectId; + float sharpness; + float resW; + float resH; + int effectMask; + float brightness; + float contrast; + float gamma; + float outW; + float outH; + float barrelStrength; + float barrelHeight; + float barrelCylindricalRatio; } pc; layout(location = 0) out vec2 fragTexCoord; +layout(location = 1) out vec3 vUVBarrel; +layout(location = 2) out vec2 vUVDotBarrel; void main() { int xi = (gl_VertexIndex >> 1) & 1; @@ -16,4 +32,18 @@ void main() { float y = yi == 1 ? pc.ndcY1 : pc.ndcY0; gl_Position = vec4(x, y, 0.0, 1.0); fragTexCoord = vec2(float(xi), float(yi)); + + float scaledHeight = pc.barrelStrength * pc.barrelHeight; + float aspectRatio = max(pc.resW / max(pc.resH, 1e-6), 1e-6); + float cylAspectRatio = aspectRatio * pc.barrelCylindricalRatio; + float aspectDiagSq = aspectRatio * aspectRatio + 1.0; + float diagSq = scaledHeight * scaledHeight * aspectDiagSq; + vec2 signedUV = fragTexCoord * 2.0 - 1.0; + + float z = 0.5 * sqrt(diagSq + 1.0) + 0.5; + float ny = (z - 1.0) / (cylAspectRatio * cylAspectRatio + 1.0); + + vUVDotBarrel = sqrt(max(ny, 0.0)) * vec2(cylAspectRatio, 1.0) * signedUV; + vUVBarrel = vec3(0.5, 0.5, 1.0) * z + vec3(-0.5, -0.5, 0.0); + vUVBarrel.xy += fragTexCoord; } diff --git a/app/src/main/java/app/gamenative/ui/component/ScreenEffectsPanel.kt b/app/src/main/java/app/gamenative/ui/component/ScreenEffectsPanel.kt index 6389ad6429..d1129968a4 100644 --- a/app/src/main/java/app/gamenative/ui/component/ScreenEffectsPanel.kt +++ b/app/src/main/java/app/gamenative/ui/component/ScreenEffectsPanel.kt @@ -236,6 +236,15 @@ fun GLScreenEffectsTabContent( var gamma by remember(renderer, container) { mutableFloatStateOf(initialConfig.gamma) } + var barrelDistortionStrength by remember(renderer, container) { + mutableFloatStateOf(initialConfig.barrelDistortionStrength) + } + var barrelDistortionFov by remember(renderer, container) { + mutableFloatStateOf(initialConfig.barrelDistortionFov) + } + var barrelDistortionCylindricalRatio by remember(renderer, container) { + mutableFloatStateOf(initialConfig.barrelDistortionCylindricalRatio) + } var scalingMode by remember(renderer, container) { mutableIntStateOf(initialConfig.scalingMode) } @@ -257,6 +266,9 @@ fun GLScreenEffectsTabContent( var enableNTSC by remember(renderer, container) { mutableStateOf(initialConfig.enableNTSC) } + var enableBarrelDistortion by remember(renderer, container) { + mutableStateOf(initialConfig.enableBarrelDistortion) + } LaunchedEffect( brightness, @@ -264,11 +276,15 @@ fun GLScreenEffectsTabContent( gamma, scalingMode, fsrSharpnessLevel, + barrelDistortionStrength, + barrelDistortionFov, + barrelDistortionCylindricalRatio, enableToon, enableFXAA, enableVivid, enableCRT, enableNTSC, + enableBarrelDistortion, ) { val config = ScreenEffectsConfig( brightness = brightness, @@ -276,11 +292,15 @@ fun GLScreenEffectsTabContent( gamma = gamma, scalingMode = scalingMode, fsrSharpnessLevel = fsrSharpnessLevel, + barrelDistortionStrength = barrelDistortionStrength, + barrelDistortionFov = barrelDistortionFov, + barrelDistortionCylindricalRatio = barrelDistortionCylindricalRatio, enableToon = enableToon, enableFXAA = enableFXAA, enableVivid = enableVivid, enableCRT = enableCRT, enableNTSC = enableNTSC, + enableBarrelDistortion = enableBarrelDistortion, ) applyScreenEffectsConfig(renderer, config) delay(300) @@ -294,11 +314,15 @@ fun GLScreenEffectsTabContent( gamma = 1.0f scalingMode = ScreenEffectsConfig.SCALING_MODE_NONE fsrSharpnessLevel = ScreenEffectsConfig.FSR_DEFAULT_LEVEL + barrelDistortionStrength = 50f + barrelDistortionFov = 90f + barrelDistortionCylindricalRatio = 1f enableToon = false enableFXAA = false enableVivid = false enableCRT = false enableNTSC = false + enableBarrelDistortion = false } Column( @@ -433,7 +457,49 @@ fun GLScreenEffectsTabContent( enabled = enableNTSC, onToggle = { enableNTSC = !enableNTSC }, ) - + ScreenEffectToggleRow( + title = stringResource(R.string.screen_effects_barrel_distortion), + subtitle = stringResource(R.string.screen_effects_barrel_distortion_description), + enabled = enableBarrelDistortion, + onToggle = { enableBarrelDistortion = !enableBarrelDistortion }, + ) + AnimatedVisibility(visible = enableBarrelDistortion) { + Column { + ScreenEffectAdjustmentRow( + title = stringResource(R.string.screen_effects_barrel_distortion_strength), + valueText = formatPercent(barrelDistortionStrength), + progress = normalizedProgress(barrelDistortionStrength, 0f, 100f), + onDecrease = { + barrelDistortionStrength = (barrelDistortionStrength - SCREEN_EFFECT_PERCENT_STEP).coerceIn(0f, 100f) + }, + onIncrease = { + barrelDistortionStrength = (barrelDistortionStrength + SCREEN_EFFECT_PERCENT_STEP).coerceIn(0f, 100f) + }, + ) + ScreenEffectAdjustmentRow( + title = stringResource(R.string.screen_effects_barrel_distortion_fov), + valueText = String.format("%.0f°", barrelDistortionFov), + progress = normalizedProgress(barrelDistortionFov, 40f, 120f), + onDecrease = { + barrelDistortionFov = (barrelDistortionFov - SCREEN_EFFECT_PERCENT_STEP).coerceIn(40f, 120f) + }, + onIncrease = { + barrelDistortionFov = (barrelDistortionFov + SCREEN_EFFECT_PERCENT_STEP).coerceIn(40f, 120f) + }, + ) + ScreenEffectAdjustmentRow( + title = stringResource(R.string.screen_effects_barrel_distortion_cylindrical_ratio), + valueText = String.format("%.1fx", barrelDistortionCylindricalRatio), + progress = normalizedProgress(barrelDistortionCylindricalRatio, 0.5f, 1.5f), + onDecrease = { + barrelDistortionCylindricalRatio = (barrelDistortionCylindricalRatio - 0.1f).coerceIn(0.5f, 1.5f) + }, + onIncrease = { + barrelDistortionCylindricalRatio = (barrelDistortionCylindricalRatio + 0.1f).coerceIn(0.5f, 1.5f) + }, + ) + } + } Spacer(modifier = Modifier.height(20.dp)) AccentActionRow( @@ -478,6 +544,15 @@ fun ScreenEffectsTabContent( var gamma by remember(renderer, container) { mutableFloatStateOf(initialConfig.gamma) } + var barrelDistortionStrength by remember(renderer, container) { + mutableFloatStateOf(initialConfig.barrelDistortionStrength) + } + var barrelDistortionFov by remember(renderer, container) { + mutableFloatStateOf(initialConfig.barrelDistortionFov) + } + var barrelDistortionCylindricalRatio by remember(renderer, container) { + mutableFloatStateOf(initialConfig.barrelDistortionCylindricalRatio) + } var enableToon by remember(renderer, container) { mutableStateOf(initialConfig.enableToon) } @@ -493,6 +568,9 @@ fun ScreenEffectsTabContent( var enableNTSC by remember(renderer, container) { mutableStateOf(initialConfig.enableNTSC) } + var enableBarrelDistortion by remember(renderer, container) { + mutableStateOf(initialConfig.enableBarrelDistortion) + } LaunchedEffect( scalingMode, @@ -500,11 +578,15 @@ fun ScreenEffectsTabContent( brightness, contrast, gamma, + barrelDistortionStrength, + barrelDistortionFov, + barrelDistortionCylindricalRatio, enableToon, enableFXAA, enableVivid, enableCRT, enableNTSC, + enableBarrelDistortion, ) { val config = initialConfig.copy( scalingMode = scalingMode, @@ -512,11 +594,15 @@ fun ScreenEffectsTabContent( brightness = brightness, contrast = contrast, gamma = gamma, + barrelDistortionStrength = barrelDistortionStrength, + barrelDistortionFov = barrelDistortionFov, + barrelDistortionCylindricalRatio = barrelDistortionCylindricalRatio, enableToon = enableToon, enableFXAA = enableFXAA, enableVivid = enableVivid, enableCRT = enableCRT, enableNTSC = enableNTSC, + enableBarrelDistortion = enableBarrelDistortion, ) // Apply immediately for live preview applyScreenEffectsConfig(renderer, config) @@ -532,11 +618,15 @@ fun ScreenEffectsTabContent( brightness = 0f contrast = 0f gamma = 1.0f + barrelDistortionStrength = 50f + barrelDistortionFov = 90.0f + barrelDistortionCylindricalRatio = 1.0f enableToon = false enableFXAA = false enableVivid = false enableCRT = false enableNTSC = false + enableBarrelDistortion = false } Column( @@ -672,6 +762,49 @@ fun ScreenEffectsTabContent( enabled = enableNTSC, onToggle = { enableNTSC = !enableNTSC }, ) + ScreenEffectToggleRow( + title = stringResource(R.string.screen_effects_barrel_distortion), + subtitle = stringResource(R.string.screen_effects_barrel_distortion_description), + enabled = enableBarrelDistortion, + onToggle = { enableBarrelDistortion = !enableBarrelDistortion }, + ) + AnimatedVisibility(visible = enableBarrelDistortion) { + Column { + ScreenEffectAdjustmentRow( + title = stringResource(R.string.screen_effects_barrel_distortion_strength), + valueText = formatPercent(barrelDistortionStrength), + progress = normalizedProgress(barrelDistortionStrength, 0f, 100f), + onDecrease = { + barrelDistortionStrength = (barrelDistortionStrength - SCREEN_EFFECT_PERCENT_STEP).coerceIn(0f, 100f) + }, + onIncrease = { + barrelDistortionStrength = (barrelDistortionStrength + SCREEN_EFFECT_PERCENT_STEP).coerceIn(0f, 100f) + }, + ) + ScreenEffectAdjustmentRow( + title = stringResource(R.string.screen_effects_barrel_distortion_fov), + valueText = String.format("%.0f°", barrelDistortionFov), + progress = normalizedProgress(barrelDistortionFov, 40f, 120f), + onDecrease = { + barrelDistortionFov = (barrelDistortionFov - SCREEN_EFFECT_PERCENT_STEP).coerceIn(40f, 120f) + }, + onIncrease = { + barrelDistortionFov = (barrelDistortionFov + SCREEN_EFFECT_PERCENT_STEP).coerceIn(40f, 120f) + }, + ) + ScreenEffectAdjustmentRow( + title = stringResource(R.string.screen_effects_barrel_distortion_cylindrical_ratio), + valueText = String.format("%.1fx", barrelDistortionCylindricalRatio), + progress = normalizedProgress(barrelDistortionCylindricalRatio, 0.5f, 1.5f), + onDecrease = { + barrelDistortionCylindricalRatio = (barrelDistortionCylindricalRatio - 0.1f).coerceIn(0.5f, 1.5f) + }, + onIncrease = { + barrelDistortionCylindricalRatio = (barrelDistortionCylindricalRatio + 0.1f).coerceIn(0.5f, 1.5f) + }, + ) + } + } Spacer(modifier = Modifier.height(20.dp)) @@ -706,6 +839,15 @@ fun ScreenEffectsPanel( var gamma by remember(renderer, container) { mutableFloatStateOf(initialConfig.gamma) } + var barrelDistortionStrength by remember(renderer, container) { + mutableFloatStateOf(initialConfig.barrelDistortionStrength) + } + var barrelDistortionFov by remember(renderer, container) { + mutableFloatStateOf(initialConfig.barrelDistortionFov) + } + var barrelDistortionCylindricalRatio by remember(renderer, container) { + mutableFloatStateOf(initialConfig.barrelDistortionCylindricalRatio) + } var enableToon by remember(renderer, container) { mutableStateOf(initialConfig.enableToon) } @@ -721,17 +863,24 @@ fun ScreenEffectsPanel( var enableNTSC by remember(renderer, container) { mutableStateOf(initialConfig.enableNTSC) } + var enableBarrelDistortion by remember(renderer, container) { + mutableStateOf(initialConfig.enableBarrelDistortion) + } - LaunchedEffect(brightness, contrast, gamma, enableToon, enableFXAA, enableVivid, enableCRT, enableNTSC) { + LaunchedEffect(brightness, contrast, gamma, barrelDistortionStrength, barrelDistortionFov, barrelDistortionCylindricalRatio, enableToon, enableFXAA, enableVivid, enableCRT, enableNTSC, enableBarrelDistortion) { val config = initialConfig.copy( brightness = brightness, contrast = contrast, gamma = gamma, + barrelDistortionStrength = barrelDistortionStrength, + barrelDistortionFov = barrelDistortionFov, + barrelDistortionCylindricalRatio = barrelDistortionCylindricalRatio, enableToon = enableToon, enableFXAA = enableFXAA, enableVivid = enableVivid, enableCRT = enableCRT, enableNTSC = enableNTSC, + enableBarrelDistortion = enableBarrelDistortion, ) applyScreenEffectsConfig(renderer, config) delay(300) @@ -743,6 +892,10 @@ fun ScreenEffectsPanel( brightness = 0f contrast = 0f gamma = 1.0f + barrelDistortionStrength = 50f + barrelDistortionFov = 90.0f + barrelDistortionCylindricalRatio = 1.0f + enableBarrelDistortion = false enableToon = false enableFXAA = false enableVivid = false diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/ScreenEffectDialog.kt b/app/src/main/java/app/gamenative/ui/component/dialog/ScreenEffectDialog.kt index d7c1e3b64e..f5c9acd55b 100644 --- a/app/src/main/java/app/gamenative/ui/component/dialog/ScreenEffectDialog.kt +++ b/app/src/main/java/app/gamenative/ui/component/dialog/ScreenEffectDialog.kt @@ -1,5 +1,6 @@ package app.gamenative.ui.component.dialog +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -83,7 +84,22 @@ fun ScreenEffectDialog( mutableStateOf(initialConfig.enableNTSC) } - LaunchedEffect(brightness, contrast, gamma, enableToon, enableFXAA, enableVivid, enableCRT, enableNTSC) { + var barrelDistortionStrength by remember(renderer, container) { + mutableFloatStateOf(initialConfig.barrelDistortionStrength) + } + var barrelDistortionFov by remember(renderer, container) { + mutableFloatStateOf(initialConfig.barrelDistortionFov) + } + var barrelDistortionCylindricalRatio by remember(renderer, container) { + mutableFloatStateOf(initialConfig.barrelDistortionCylindricalRatio) + } + var enableBarrelDistortion by remember(renderer, container) { + mutableStateOf(initialConfig.enableBarrelDistortion) + } + + LaunchedEffect(brightness, contrast, gamma, enableToon, enableFXAA, enableVivid, enableCRT, + enableNTSC, barrelDistortionStrength, barrelDistortionFov, + barrelDistortionCylindricalRatio, enableBarrelDistortion) { val config = ScreenEffectsConfig( brightness = brightness, contrast = contrast, @@ -93,6 +109,10 @@ fun ScreenEffectDialog( enableVivid = enableVivid, enableCRT = enableCRT, enableNTSC = enableNTSC, + barrelDistortionStrength = barrelDistortionStrength, + barrelDistortionFov = barrelDistortionFov, + barrelDistortionCylindricalRatio = barrelDistortionCylindricalRatio, + enableBarrelDistortion = enableBarrelDistortion, ) applyScreenEffectsConfig(renderer, config) delay(300) @@ -109,6 +129,10 @@ fun ScreenEffectDialog( enableVivid = false enableCRT = false enableNTSC = false + barrelDistortionStrength = 50f + barrelDistortionFov = 90f + barrelDistortionCylindricalRatio = 1f + enableBarrelDistortion = false } Dialog( @@ -247,6 +271,45 @@ fun ScreenEffectDialog( } } + Spacer(modifier = Modifier.height(12.dp)) + + OutlinedCard(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(vertical = 8.dp)) { + SettingsSwitch( + colors = settingsTileColors(), + title = { Text(stringResource(R.string.screen_effects_barrel_distortion)) }, + subtitle = { Text(stringResource(R.string.screen_effects_barrel_distortion_description)) }, + state = enableBarrelDistortion, + onCheckedChange = { enableBarrelDistortion = it }, + ) + AnimatedVisibility(visible = enableBarrelDistortion) { + Column { + ScreenEffectSlider( + label = stringResource(R.string.screen_effects_barrel_distortion_strength), + valueText = formatPercent(barrelDistortionStrength), + value = barrelDistortionStrength, + valueRange = 0f..100f, + onValueChange = { barrelDistortionStrength = it }, + ) + ScreenEffectSlider( + label = stringResource(R.string.screen_effects_barrel_distortion_fov), + valueText = String.format("%.0f°", barrelDistortionFov), + value = barrelDistortionFov, + valueRange = 40f..120f, + onValueChange = { barrelDistortionFov = it }, + ) + ScreenEffectSlider( + label = stringResource(R.string.screen_effects_barrel_distortion_cylindrical_ratio), + valueText = String.format("%.1fx", barrelDistortionCylindricalRatio), + value = barrelDistortionCylindricalRatio, + valueRange = 0.5f..1.5f, + onValueChange = { barrelDistortionCylindricalRatio = it }, + ) + } + } + } + } + Spacer(modifier = Modifier.height(16.dp)) Row( diff --git a/app/src/main/java/app/gamenative/ui/util/ScreenEffectsConfig.kt b/app/src/main/java/app/gamenative/ui/util/ScreenEffectsConfig.kt index cd8b6e4d0a..495042026f 100644 --- a/app/src/main/java/app/gamenative/ui/util/ScreenEffectsConfig.kt +++ b/app/src/main/java/app/gamenative/ui/util/ScreenEffectsConfig.kt @@ -3,6 +3,7 @@ package app.gamenative.ui.util import com.winlator.container.Container import com.winlator.renderer.GLRenderer import com.winlator.renderer.VulkanRenderer +import com.winlator.renderer.effects.BarrelDistortionEffect import com.winlator.renderer.effects.ColorEffect import com.winlator.renderer.effects.CRTEffect import com.winlator.renderer.effects.Effect @@ -14,6 +15,7 @@ import com.winlator.renderer.effects.ScalingModeEffect import com.winlator.renderer.effects.ToonEffect import com.winlator.renderer.effects.VividEffect import kotlin.math.abs +import kotlin.math.tan data class ScreenEffectsConfig( val brightness: Float = 0f, @@ -26,6 +28,10 @@ data class ScreenEffectsConfig( val enableVivid: Boolean = false, val enableCRT: Boolean = false, val enableNTSC: Boolean = false, + val enableBarrelDistortion: Boolean = false, + val barrelDistortionStrength: Float = 50f, + val barrelDistortionFov: Float = 90f, + val barrelDistortionCylindricalRatio: Float = 1f, ) { companion object { const val SCALING_MODE_NONE = 0 @@ -47,11 +53,17 @@ data class ScreenEffectsConfig( const val KEY_GAMMA = "screenEffectsGamma" const val KEY_SCALING_MODE = "screenEffectsScalingMode" const val KEY_FSR_SHARPNESS = "screenEffectsFsrSharpness" + const val KEY_BARREL_DISTORTION_STRENGTH = "screenEffectsBarrelDistortionStrength" + const val KEY_BARREL_DISTORTION_FOV = "screenEffectsBarrelDistortionFov" + const val KEY_BARREL_DISTORTION_CYLINDRICAL_RATIO = "screenEffectsBarrelDistortionCylindricalRatio" const val KEY_ENABLE_TOON = "screenEffectsEnableToon" const val KEY_ENABLE_FXAA = "screenEffectsEnableFXAA" const val KEY_ENABLE_VIVID = "screenEffectsEnableVivid" const val KEY_ENABLE_CRT = "screenEffectsEnableCRT" const val KEY_ENABLE_NTSC = "screenEffectsEnableNTSC" + const val KEY_ENABLE_BARREL_DISTORTION = "screenEffectsEnableBarrelDistortion" + + const val DEGREES_TO_RADIANS: Float = 0.01745329251994329577f } } @@ -64,11 +76,15 @@ fun loadScreenEffectsConfig(container: Container?): ScreenEffectsConfig { gamma = container.getExtra(ScreenEffectsConfig.KEY_GAMMA)?.toFloatOrNull() ?: 1f, scalingMode = container.getExtra(ScreenEffectsConfig.KEY_SCALING_MODE)?.toIntOrNull() ?: ScreenEffectsConfig.SCALING_MODE_NONE, fsrSharpnessLevel = container.getExtra(ScreenEffectsConfig.KEY_FSR_SHARPNESS)?.toIntOrNull() ?: ScreenEffectsConfig.FSR_DEFAULT_LEVEL, + barrelDistortionStrength = container.getExtra(ScreenEffectsConfig.KEY_BARREL_DISTORTION_STRENGTH)?.toFloatOrNull() ?: 50f, + barrelDistortionFov = container.getExtra(ScreenEffectsConfig.KEY_BARREL_DISTORTION_FOV)?.toFloatOrNull() ?: 90f, + barrelDistortionCylindricalRatio = container.getExtra(ScreenEffectsConfig.KEY_BARREL_DISTORTION_CYLINDRICAL_RATIO)?.toFloatOrNull() ?: 1f, enableToon = container.getExtra(ScreenEffectsConfig.KEY_ENABLE_TOON)?.toBooleanStrictOrNull() ?: false, enableFXAA = container.getExtra(ScreenEffectsConfig.KEY_ENABLE_FXAA)?.toBooleanStrictOrNull() ?: false, enableVivid = container.getExtra(ScreenEffectsConfig.KEY_ENABLE_VIVID)?.toBooleanStrictOrNull() ?: false, enableCRT = container.getExtra(ScreenEffectsConfig.KEY_ENABLE_CRT)?.toBooleanStrictOrNull() ?: false, enableNTSC = container.getExtra(ScreenEffectsConfig.KEY_ENABLE_NTSC)?.toBooleanStrictOrNull() ?: false, + enableBarrelDistortion = container.getExtra(ScreenEffectsConfig.KEY_ENABLE_BARREL_DISTORTION)?.toBooleanStrictOrNull() ?: false, ) } @@ -83,11 +99,15 @@ fun persistScreenEffectsConfig(container: Container?, config: ScreenEffectsConfi container.putExtra(ScreenEffectsConfig.KEY_GAMMA, config.gamma) container.putExtra(ScreenEffectsConfig.KEY_SCALING_MODE, config.scalingMode) container.putExtra(ScreenEffectsConfig.KEY_FSR_SHARPNESS, config.fsrSharpnessLevel) + container.putExtra(ScreenEffectsConfig.KEY_BARREL_DISTORTION_STRENGTH, config.barrelDistortionStrength) + container.putExtra(ScreenEffectsConfig.KEY_BARREL_DISTORTION_FOV, config.barrelDistortionFov) + container.putExtra(ScreenEffectsConfig.KEY_BARREL_DISTORTION_CYLINDRICAL_RATIO, config.barrelDistortionCylindricalRatio) container.putExtra(ScreenEffectsConfig.KEY_ENABLE_TOON, config.enableToon) container.putExtra(ScreenEffectsConfig.KEY_ENABLE_FXAA, config.enableFXAA) container.putExtra(ScreenEffectsConfig.KEY_ENABLE_VIVID, config.enableVivid) container.putExtra(ScreenEffectsConfig.KEY_ENABLE_CRT, config.enableCRT) container.putExtra(ScreenEffectsConfig.KEY_ENABLE_NTSC, config.enableNTSC) + container.putExtra(ScreenEffectsConfig.KEY_ENABLE_BARREL_DISTORTION, config.enableBarrelDistortion) } fun fsrQuickMenuLevelToStops(level: Int): Float { @@ -153,6 +173,16 @@ fun applyScreenEffectsConfig(renderer: GLRenderer, config: ScreenEffectsConfig) effects += composer.getEffect(NTSCCombinedEffect::class.java) ?: NTSCCombinedEffect() } + + if (config.enableBarrelDistortion && abs(config.barrelDistortionStrength) > 0.001f) { + val barrelEffect = (composer.getEffect(BarrelDistortionEffect::class.java) ?: BarrelDistortionEffect()).apply { + strength = config.barrelDistortionStrength / 100f + height = tan(config.barrelDistortionFov * ScreenEffectsConfig.DEGREES_TO_RADIANS / 2f) + cylindricalRatio = config.barrelDistortionCylindricalRatio + } + effects += barrelEffect + } + composer.setEffects(effects) } @@ -188,7 +218,14 @@ fun applyScreenEffectsConfig(renderer: VulkanRenderer, config: ScreenEffectsConf (if (config.enableFXAA) VulkanRenderer.EFFECT_MASK_FXAA else 0) or (if (config.enableVivid) VulkanRenderer.EFFECT_MASK_VIVID else 0) or (if (config.enableCRT) VulkanRenderer.EFFECT_MASK_CRT else 0) or - (if (config.enableNTSC) VulkanRenderer.EFFECT_MASK_NTSC else 0) + (if (config.enableNTSC) VulkanRenderer.EFFECT_MASK_NTSC else 0) or + (if (config.enableBarrelDistortion) VulkanRenderer.EFFECT_MASK_BARREL_DISTORTION else 0) + val barrelStrength = if (config.enableBarrelDistortion) config.barrelDistortionStrength / 100f else 0f + val barrelHeight = if (config.enableBarrelDistortion) { + tan(config.barrelDistortionFov * ScreenEffectsConfig.DEGREES_TO_RADIANS / 2f) + } else 0f + val barrelCylindricalRatio = if (config.enableBarrelDistortion) config.barrelDistortionCylindricalRatio else 0f + renderer.setEffect( effectId, sharpness, @@ -197,5 +234,8 @@ fun applyScreenEffectsConfig(renderer: VulkanRenderer, config: ScreenEffectsConf config.brightness / 100f, config.contrast / 100f, config.gamma, + barrelStrength, + barrelHeight, + barrelCylindricalRatio, ) } diff --git a/app/src/main/java/com/winlator/renderer/VulkanRenderer.java b/app/src/main/java/com/winlator/renderer/VulkanRenderer.java index 14f2e857a3..f89b073bb0 100644 --- a/app/src/main/java/com/winlator/renderer/VulkanRenderer.java +++ b/app/src/main/java/com/winlator/renderer/VulkanRenderer.java @@ -41,6 +41,7 @@ public class VulkanRenderer implements WindowManager.OnWindowModificationListene public static final int EFFECT_MASK_VIVID = 1 << 2; public static final int EFFECT_MASK_CRT = 1 << 3; public static final int EFFECT_MASK_NTSC = 1 << 4; + public static final int EFFECT_MASK_BARREL_DISTORTION = 1 << 5; public final XServerView xServerView; private final XServer xServer; @@ -129,7 +130,8 @@ private native void nativeUpdateCursorImage(long handle, java.nio.ByteBuffer pix private native void nativeSetSwapRB(long handle, boolean enabled); private native void nativeSetPresentMode(long handle, int mode); private native void nativeSetEffect(long handle, int effectId, float sharpness, - int effectMask, float brightness, float contrast, float gamma); + int effectMask, float brightness, float contrast, float gamma, + float barrelStrength, float barrelHeight, float barrelCylindricalRatio); private static volatile boolean gpuImageChecked = false; @@ -173,7 +175,8 @@ public void onSurfaceCreated(Surface surface) { nativeSetFilterMode(nativeHandle, pendingFilterMode); nativeSetSwapRB(nativeHandle, pendingSwapRB); nativeSetEffect(nativeHandle, pendingEffectId, pendingSharpness, - pendingEffectMask, pendingBrightness, pendingContrast, pendingGamma); + pendingEffectMask, pendingBrightness, pendingContrast, pendingGamma, + pendingBarrelStrength, pendingBarrelHeight, pendingBarrelCylindricalRatio); updateTransform(); nativeSetCursorVisible(nativeHandle, cursorVisible); if (nativeMode && !effectsRequireCompositor) { @@ -714,24 +717,31 @@ public void setEffect(int effectId, float sharpness, boolean preserveAspectFit) } public void setEffect(int effectId, float sharpness, int scalingMode) { - setEffect(effectId, sharpness, scalingMode, 0, 0.0f, 0.0f, 1.0f); + setEffect(effectId, sharpness, scalingMode, + 0, 0.0f, 0.0f, 1.0f, 0.0f, 0.176327f /* tan(15°) default FOV */, 0.5f); } + public void setEffect(int effectId, float sharpness, int scalingMode, - int effectMask, float brightness, float contrast, float gamma) { + int effectMask, float brightness,float contrast,float gamma, + float barrelStrength, float barrelHeight, float barrelCylindricalRatio) { pendingEffectId = Math.max(EFFECT_NONE, Math.min(EFFECT_NATURAL, effectId)); pendingSharpness = Math.max(0.0f, Math.min(1.0f, sharpness)); pendingEffectMask = Math.max(0, effectMask); pendingBrightness = Math.max(-1.0f, Math.min(1.0f, brightness)); pendingContrast = Math.max(-1.0f, Math.min(1.0f, contrast)); pendingGamma = Math.max(0.1f, Math.min(4.0f, gamma)); + pendingBarrelStrength = barrelStrength; + pendingBarrelHeight = barrelHeight; + pendingBarrelCylindricalRatio = barrelCylindricalRatio; outputScalingMode = Math.max(SCALE_FIT, Math.min(SCALE_FILL, scalingMode)); boolean wasRequireCompositor = effectsRequireCompositor; effectsRequireCompositor = computeEffectsRequireCompositor(); synchronized (lock) { if (nativeHandle != 0) { - nativeSetEffect(nativeHandle, pendingEffectId, pendingSharpness, - pendingEffectMask, pendingBrightness, pendingContrast, pendingGamma); + nativeSetEffect(nativeHandle,pendingEffectId,pendingSharpness, + pendingEffectMask,pendingBrightness,pendingContrast,pendingGamma, + pendingBarrelStrength, pendingBarrelHeight, pendingBarrelCylindricalRatio); updateTransform(); } } @@ -805,6 +815,9 @@ public void setOnFrameRenderedListener(Runnable r) { /* no-op: Vulkan compositor private float pendingBrightness = 0.0f; private float pendingContrast = 0.0f; private float pendingGamma = 1.0f; + private float pendingBarrelStrength = 0.0f; + private float pendingBarrelHeight = 0.0f; + private float pendingBarrelCylindricalRatio = 0.5f; // When any screen effect / filter / color adjustment is active we must route // fullscreen content through the compositor (window.frag) instead of the // zero-copy scanout fast-path, because scanout bypasses the shader entirely. diff --git a/app/src/main/java/com/winlator/renderer/effects/BarrelDistortionEffect.java b/app/src/main/java/com/winlator/renderer/effects/BarrelDistortionEffect.java new file mode 100644 index 0000000000..673ca1b7ba --- /dev/null +++ b/app/src/main/java/com/winlator/renderer/effects/BarrelDistortionEffect.java @@ -0,0 +1,94 @@ +package com.winlator.renderer.effects; + +import com.winlator.renderer.GLRenderer; +import com.winlator.renderer.material.ScreenMaterial; +import com.winlator.renderer.material.ShaderMaterial; + +public class BarrelDistortionEffect extends Effect { + private float strength = 1.0f; + private float height = 90.0f; + private float cylindricalRatio = 1.0f; + + public float getStrength() { + return strength; + } + + public void setStrength(float strength) { + this.strength = strength; + } + + public float getHeight() { + return height; + } + + public void setHeight(float height) { + this.height = height; + } + + public float getCylindricalRatio() { + return cylindricalRatio; + } + + public void setCylindricalRatio(float cylindricalRatio) { + this.cylindricalRatio = cylindricalRatio; + } + + @Override + protected ShaderMaterial createMaterial() { + return new BarrelDistortionEffectMaterial(); + } + + `@Override` + protected void onUse(ShaderMaterial material, GLRenderer renderer) { + material.setUniformFloat("strength", Math.max(0.0f, Math.min(strength, 1.0f))); + material.setUniformFloat("height", Math.max(0.05f, height)); + material.setUniformFloat("cylindricalRatio", Math.max(0.5f, Math.min(cylindricalRatio, 1.5f))); + } + + private static class BarrelDistortionEffectMaterial extends ScreenMaterial { + public BarrelDistortionEffectMaterial() { + setUniformNames("screenTexture", "resolution", "strength", "height", "cylindricalRatio"); + } + + @Override + protected String getVertexShader() { + return + "uniform float strength;\n" + // s: 0 = perspective, 1 = stereographic + "uniform float height;\n" + // h: tan(verticalFOVInRadians / 2) + "uniform vec2 resolution;\n" + // a: screenWidth / screenHeight + "uniform float cylindricalRatio;\n" + // c: cylindrical distortion ratio. 1 = spherical + "varying vec3 vUV;\n" + // output to interpolate over screen + "varying vec2 vUVDot;\n" + // output to interpolate over screen + "void main() {\n" + + " gl_Position = projectionMatrix * (modelViewMatrix * vec4(position, 1.0));\n" + + + " float scaledHeight = strength * height;\n" + + " float aspectRatio = resolution.x / resolution.y;\n" + + " float cylAspectRatio = aspectRatio * cylindricalRatio;\n" + + " float aspectDiagSq = aspectRatio * aspectRatio + 1.0;\n" + + " float diagSq = scaledHeight * scaledHeight * aspectDiagSq;\n" + + " vec2 signedUV = (2.0 * uv + vec2(-1.0, -1.0));\n" + + + " float z = 0.5 * sqrt(diagSq + 1.0) + 0.5;\n" + + " float ny = (z - 1.0) / (cylAspectRatio * cylAspectRatio + 1.0);\n" + + + " vUVDot = sqrt(ny) * vec2(cylAspectRatio, 1.0) * signedUV;\n" + + " vUV = vec3(0.5, 0.5, 1.0) * z + vec3(-0.5, -0.5, 0.0);\n" + + " vUV.xy += uv;\n" + + "}"; + } + + @Override + protected String getFragmentShader() { + return + "uniform sampler2D screenTexture;\n" + // sampler of rendered scenes render target + "varying vec3 vUV;\n" + // interpolated vertex output data + "varying vec2 vUVDot;\n" + // interpolated vertex output data + + "void main() {\n" + + " vec3 uv = dot(vUVDot, vUVDot) * vec3(-0.5, -0.5, -1.0) + vUV;\n" + + " gl_FragColor = texture2DProj(screenTexture, uv);\n" + + "}"; + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d4e4ba9ad8..24d0ad19e5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,4 @@ - + GameNative User Login Two Factor @@ -1628,4 +1628,9 @@ Tip: If you have a Mali GPU, please use System Drivers. Tip: Getting a blank screen? Try using the \"%s\" option in the menu to check if your drivers are working correctly. Tip: Use Proton x86-64 if you can\'t click the mouse in games. + Barrel Zoom + Enable Barrel Zoom. Must be configured to match the vertical FOV of your game - can reduce visual fatigue with high FOV applications on a small screen. + Barrel Zoom Strength + Barrel Zoom VFOV + Barrel Zoom Cyl Ratio diff --git a/app/src/modern/jniLibs/arm64-v8a/libvulkan_renderer.so b/app/src/modern/jniLibs/arm64-v8a/libvulkan_renderer.so index 429b4019c9..15d8fdfd38 100644 Binary files a/app/src/modern/jniLibs/arm64-v8a/libvulkan_renderer.so and b/app/src/modern/jniLibs/arm64-v8a/libvulkan_renderer.so differ