mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-08-23 07:56:52 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 424705d193 | |||
| 5b86242313 | |||
| d24e79c991 |
@@ -31,6 +31,7 @@ object GameHelper {
|
||||
|
||||
fun getGames(): List<Game> {
|
||||
val games = mutableListOf<Game>()
|
||||
val gamesByProgramId = mutableMapOf<String, Game>()
|
||||
val context = YuzuApplication.appContext
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
@@ -63,6 +64,7 @@ object GameHelper {
|
||||
|
||||
addGamesRecursive(
|
||||
games,
|
||||
gamesByProgramId,
|
||||
FileUtil.listFiles(gameDirUri),
|
||||
scanDepth,
|
||||
mountedContainerUris
|
||||
@@ -136,6 +138,7 @@ object GameHelper {
|
||||
|
||||
private fun addGamesRecursive(
|
||||
games: MutableList<Game>,
|
||||
gamesByProgramId: MutableMap<String, Game>,
|
||||
files: Array<MinimalDocumentFile>,
|
||||
depth: Int,
|
||||
mountedContainerUris: MutableSet<String>
|
||||
@@ -148,6 +151,7 @@ object GameHelper {
|
||||
if (it.isDirectory) {
|
||||
addGamesRecursive(
|
||||
games,
|
||||
gamesByProgramId,
|
||||
FileUtil.listFiles(it.uri),
|
||||
depth - 1,
|
||||
mountedContainerUris
|
||||
@@ -156,8 +160,9 @@ object GameHelper {
|
||||
val extension = FileUtil.getExtension(it.uri).lowercase()
|
||||
val filePath = it.uri.toString()
|
||||
|
||||
if (externalContentExtensions.contains(extension) &&
|
||||
mountedContainerUris.add(filePath)) {
|
||||
val mountedContainer = externalContentExtensions.contains(extension) &&
|
||||
mountedContainerUris.add(filePath)
|
||||
if (mountedContainer) {
|
||||
NativeLibrary.addGameFolderFileToFilesystemProvider(filePath)
|
||||
}
|
||||
|
||||
@@ -165,6 +170,20 @@ object GameHelper {
|
||||
val game = getGame(it.uri, true, false)
|
||||
if (game != null) {
|
||||
games.add(game)
|
||||
if (game.programId != "0") {
|
||||
gamesByProgramId[game.programId] = game
|
||||
}
|
||||
} else if (mountedContainer) {
|
||||
GameMetadata.getProgramId(filePath).toLongOrNull()?.let { programId ->
|
||||
gamesByProgramId[(programId and 0x800L.inv()).toString()]
|
||||
}?.let { existingGame ->
|
||||
NativeLibrary.getPatchesForFile(existingGame.path, existingGame.programId)
|
||||
existingGame.version = GameMetadata.getVersion(
|
||||
existingGame.path,
|
||||
true
|
||||
)
|
||||
GameIconUtils.refreshGameIcon(existingGame)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -24,6 +27,15 @@ import coil.request.Options
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.model.Game
|
||||
import java.util.Collections
|
||||
import java.util.WeakHashMap
|
||||
|
||||
private val gameIconHashes = Collections.synchronizedMap(mutableMapOf<String, Int>())
|
||||
private val gameIconTargets = Collections.synchronizedMap(WeakHashMap<ImageView, GameIconTarget>())
|
||||
|
||||
private fun Game.iconCacheKey(): String = "$path|$version"
|
||||
|
||||
private data class GameIconTarget(val game: Game, var iconHash: Int? = null)
|
||||
|
||||
class GameIconFetcher(
|
||||
private val game: Game,
|
||||
@@ -31,14 +43,15 @@ class GameIconFetcher(
|
||||
) : Fetcher {
|
||||
override suspend fun fetch(): FetchResult {
|
||||
return DrawableResult(
|
||||
drawable = decodeGameIcon(game.path)!!.toDrawable(options.context.resources),
|
||||
drawable = decodeGameIcon(game)!!.toDrawable(options.context.resources),
|
||||
isSampled = false,
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
}
|
||||
|
||||
private fun decodeGameIcon(uri: String): Bitmap? {
|
||||
val data = GameMetadata.getIcon(uri)
|
||||
private fun decodeGameIcon(game: Game): Bitmap? {
|
||||
val data = GameMetadata.getIcon(game.path)
|
||||
gameIconHashes[game.iconCacheKey()] = data.contentHashCode()
|
||||
return BitmapFactory.decodeByteArray(
|
||||
data,
|
||||
0,
|
||||
@@ -54,7 +67,7 @@ class GameIconFetcher(
|
||||
}
|
||||
|
||||
class GameIconKeyer : Keyer<Game> {
|
||||
override fun key(data: Game, options: Options): String = data.path
|
||||
override fun key(data: Game, options: Options): String = data.iconCacheKey()
|
||||
}
|
||||
|
||||
object GameIconUtils {
|
||||
@@ -71,14 +84,58 @@ object GameIconUtils {
|
||||
.build()
|
||||
|
||||
fun loadGameIcon(game: Game, imageView: ImageView) {
|
||||
gameIconTargets[imageView] = GameIconTarget(game)
|
||||
val request = ImageRequest.Builder(YuzuApplication.appContext)
|
||||
.data(game)
|
||||
.target(imageView)
|
||||
.error(R.drawable.default_icon)
|
||||
.listener(
|
||||
onSuccess = { _, _ ->
|
||||
val target = gameIconTargets[imageView]
|
||||
if (target?.game?.iconCacheKey() == game.iconCacheKey()) {
|
||||
gameIconHashes[game.iconCacheKey()]?.let {
|
||||
target.iconHash = it
|
||||
}
|
||||
}
|
||||
},
|
||||
onError = { _, _ ->
|
||||
gameIconTargets[imageView]?.iconHash = null
|
||||
}
|
||||
)
|
||||
.build()
|
||||
imageLoader.enqueue(request)
|
||||
}
|
||||
|
||||
fun refreshGameIcon(game: Game) {
|
||||
val targets = synchronized(gameIconTargets) {
|
||||
gameIconTargets
|
||||
.filterValues { it.game.path == game.path && it.game.programId == game.programId }
|
||||
.keys
|
||||
.toList()
|
||||
}
|
||||
if (targets.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val iconHash = GameMetadata.getIcon(game.path).contentHashCode()
|
||||
val targetsToRefresh = targets.filter { gameIconTargets[it]?.iconHash != iconHash }
|
||||
if (targetsToRefresh.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
imageLoader.memoryCache?.remove(MemoryCache.Key(game.iconCacheKey()))
|
||||
targetsToRefresh.forEach { imageView ->
|
||||
imageView.post {
|
||||
val target = gameIconTargets[imageView] ?: return@post
|
||||
if (target.game.path == game.path && target.game.programId == game.programId) {
|
||||
if (target.iconHash != iconHash) {
|
||||
loadGameIcon(game, imageView)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getGameIcon(lifecycleOwner: LifecycleOwner, game: Game): Bitmap {
|
||||
val request = ImageRequest.Builder(YuzuApplication.appContext)
|
||||
.data(game)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_pressed="true" android:color="?attr/colorControlHighlight" />
|
||||
<item android:state_focused="true" android:color="@android:color/transparent" />
|
||||
<item android:state_selected="true" android:color="@android:color/transparent" />
|
||||
<item android:state_hovered="true" android:color="@android:color/transparent" />
|
||||
<item android:color="@android:color/transparent" />
|
||||
</selector>
|
||||
@@ -10,6 +10,7 @@
|
||||
android:clipChildren="true"
|
||||
android:layout_margin="0dp"
|
||||
app:cardBackgroundColor="@color/eden_card_background"
|
||||
app:rippleColor="@color/game_card_ripple"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="@color/eden_border">
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardPreventCornerOverlap="true"
|
||||
android:clipChildren="true"
|
||||
app:rippleColor="@color/game_card_ripple"
|
||||
android:layout_margin="4dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
android:focusable="true"
|
||||
android:transitionName="card_game"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:rippleColor="@color/game_card_ripple"
|
||||
android:foreground="@color/eden_border_gradient_start">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
android:focusable="true"
|
||||
android:transitionName="card_game_compact"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:rippleColor="@color/game_card_ripple"
|
||||
android:foreground="@color/eden_border_gradient_start">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="0dp"
|
||||
app:cardBackgroundColor="@android:color/transparent"
|
||||
app:rippleColor="@color/game_card_ripple"
|
||||
app:strokeWidth="0dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
||||
@@ -7,4 +7,6 @@
|
||||
#define STB_IMAGE_IMPLEMENTATION 1
|
||||
#define STB_IMAGE_RESIZE_IMPLEMENTATION 1
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION 1
|
||||
#define STBI_ONLY_JPEG 1
|
||||
|
||||
#include "common/stb.h"
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#pragma once
|
||||
|
||||
#define STBI_ONLY_JPEG 1
|
||||
#define STBI_WRITE_NO_STDIO 1
|
||||
#include <stb_image.h>
|
||||
#include <stb_image_resize.h>
|
||||
#include <stb_image_write.h>
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "common/stb.h"
|
||||
#define STBI_ONLY_JPEG 1
|
||||
#include <stb_image.h>
|
||||
#include <stb_image_resize.h>
|
||||
#include <stb_image_write.h>
|
||||
|
||||
#include "common/settings.h"
|
||||
#include "core/file_sys/control_metadata.h"
|
||||
#include "core/file_sys/patch_manager.h"
|
||||
|
||||
@@ -5,7 +5,6 @@ layout(location = 0) out vec4 color;
|
||||
layout(binding = 0) uniform sampler2D color_texture;
|
||||
|
||||
#ifdef VULKAN
|
||||
|
||||
struct ScreenRectVertex {
|
||||
vec2 position;
|
||||
vec2 tex_coord;
|
||||
@@ -14,94 +13,79 @@ layout (push_constant) uniform PushConstants {
|
||||
mat4 modelview_matrix;
|
||||
ScreenRectVertex vertices[4];
|
||||
};
|
||||
|
||||
#else // OpenGL
|
||||
|
||||
layout(location = 1) uniform uvec2 screen_size;
|
||||
|
||||
#endif
|
||||
|
||||
/***** Area Sampling *****/
|
||||
|
||||
// By Sam Belliveau and Filippo Tarpini. Public Domain license.
|
||||
// Effectively a more accurate sharp bilinear filter when upscaling,
|
||||
// that also works as a mathematically perfect downscale filter.
|
||||
// https://entropymine.com/imageworsener/pixelmixing/
|
||||
// https://github.com/obsproject/obs-studio/pull/1715
|
||||
// https://legacy.imagemagick.org/Usage/filter/
|
||||
vec4 AreaSampling(sampler2D textureSampler, vec2 texCoords, vec2 source_size, vec2 target_size) {
|
||||
// Determine the sizes of the source and target images.
|
||||
vec2 inverted_target_size = vec2(1.0) / target_size;
|
||||
|
||||
// Determine the range of the source image that the target pixel will cover.
|
||||
vec2 range = source_size * inverted_target_size;
|
||||
vec2 beg = (texCoords.xy * source_size) - (range * 0.5);
|
||||
vec2 end = beg + range;
|
||||
|
||||
vec4 AreaSampling(sampler2D s, vec2 tc, vec4 trans_bounds) {
|
||||
// Compute the top-left and bottom-right corners of the pixel box.
|
||||
ivec2 f_beg = ivec2(floor(beg));
|
||||
ivec2 f_end = ivec2(floor(end));
|
||||
|
||||
ivec4 b = ivec4(floor(trans_bounds));
|
||||
// Compute how much of the start and end pixels are covered horizontally & vertically.
|
||||
float area_w = 1.0 - fract(beg.x);
|
||||
float area_n = 1.0 - fract(beg.y);
|
||||
float area_e = fract(end.x);
|
||||
float area_s = fract(end.y);
|
||||
|
||||
// W,N,E,S
|
||||
vec4 kb = vec4(1.0f - fract(trans_bounds.xy), fract(trans_bounds.zw));
|
||||
// Compute the areas of the corner pixels in the pixel box.
|
||||
float area_nw = area_n * area_w;
|
||||
float area_ne = area_n * area_e;
|
||||
float area_sw = area_s * area_w;
|
||||
float area_se = area_s * area_e;
|
||||
|
||||
// Initialize the color accumulator.
|
||||
vec4 avg_color = vec4(0.0, 0.0, 0.0, 0.0);
|
||||
|
||||
// Accumulate corner pixels.
|
||||
avg_color += area_nw * texelFetch(textureSampler, ivec2(f_beg.x, f_beg.y), 0);
|
||||
avg_color += area_ne * texelFetch(textureSampler, ivec2(f_end.x, f_beg.y), 0);
|
||||
avg_color += area_sw * texelFetch(textureSampler, ivec2(f_beg.x, f_end.y), 0);
|
||||
avg_color += area_se * texelFetch(textureSampler, ivec2(f_end.x, f_end.y), 0);
|
||||
// NW,NE,SW,SE
|
||||
vec4 kc = kb.yyww * kb.xzxz;
|
||||
// Accumulate corner pixels by forming a corner matrix.
|
||||
vec4 r = mat4x4(
|
||||
texelFetch(s, ivec2(b.xy), 0),
|
||||
texelFetch(s, ivec2(b.zy), 0),
|
||||
texelFetch(s, ivec2(b.xw), 0),
|
||||
texelFetch(s, ivec2(b.zw), 0)
|
||||
) * kc;
|
||||
|
||||
// Determine the size of the pixel box.
|
||||
int x_range = int(f_end.x - f_beg.x - 0.5);
|
||||
int y_range = int(f_end.y - f_beg.y - 0.5);
|
||||
|
||||
ivec2 q = clamp(ivec2(
|
||||
int(b.z - b.x - 0.5f),
|
||||
int(b.w - b.y - 0.5f)
|
||||
), ivec2(-16), ivec2(16));
|
||||
vec2 qf = vec2(q);
|
||||
// Accumulate top and bottom edge pixels.
|
||||
for (int x = f_beg.x + 1; x <= f_beg.x + x_range; ++x) {
|
||||
avg_color += area_n * texelFetch(textureSampler, ivec2(x, f_beg.y), 0);
|
||||
avg_color += area_s * texelFetch(textureSampler, ivec2(x, f_end.y), 0);
|
||||
for (int x = 0; x < q.x; ++x) {
|
||||
r += kb.y * texelFetch(s, ivec2(x + b.x + 1, b.y), 0);
|
||||
r += kb.w * texelFetch(s, ivec2(x + b.x + 1, b.w), 0);
|
||||
}
|
||||
|
||||
// Accumulate left and right edge pixels and all the pixels in between.
|
||||
for (int y = f_beg.y + 1; y <= f_beg.y + y_range; ++y) {
|
||||
avg_color += area_w * texelFetch(textureSampler, ivec2(f_beg.x, y), 0);
|
||||
avg_color += area_e * texelFetch(textureSampler, ivec2(f_end.x, y), 0);
|
||||
|
||||
for (int x = f_beg.x + 1; x <= f_beg.x + x_range; ++x) {
|
||||
avg_color += texelFetch(textureSampler, ivec2(x, y), 0);
|
||||
for (int y = 0; y < q.y; ++y) {
|
||||
r += kb.x * texelFetch(s, ivec2(b.x, y + b.y + 1), 0);
|
||||
r += kb.z * texelFetch(s, ivec2(b.z, y + b.y + 1), 0);
|
||||
for (int x = 0; x < q.x; ++x) {
|
||||
r += texelFetch(s, ivec2(x, y) + b.xy + 1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the area of the pixel box that was sampled.
|
||||
float area_corners = area_nw + area_ne + area_sw + area_se;
|
||||
float area_edges = float(x_range) * (area_n + area_s) + float(y_range) * (area_w + area_e);
|
||||
float area_center = float(x_range) * float(y_range);
|
||||
|
||||
// Return the normalized average color.
|
||||
return avg_color / (area_corners + area_edges + area_center);
|
||||
return r / (
|
||||
kc.x + kc.y + kc.z + kc.w //corners
|
||||
+ qf.x * (kb.y + kb.w) + qf.y * (kb.x + kb.z) //edges
|
||||
+ qf.x * qf.y // center
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 source_image_size = textureSize(color_texture, 0);
|
||||
vec2 window_size;
|
||||
#ifdef VULKAN
|
||||
vec2 dst_size = vec2(
|
||||
vertices[1].position.x - vertices[0].position.x,
|
||||
vertices[2].position.y - vertices[0].position.y
|
||||
);
|
||||
#else // OpenGL
|
||||
vec2 dst_size = screen_size;
|
||||
#endif
|
||||
|
||||
#ifdef VULKAN
|
||||
window_size.x = vertices[1].position.x - vertices[0].position.x;
|
||||
window_size.y = vertices[2].position.y - vertices[0].position.y;
|
||||
#else // OpenGL
|
||||
window_size = screen_size;
|
||||
#endif
|
||||
|
||||
color = AreaSampling(color_texture, frag_tex_coord, source_image_size, window_size);
|
||||
vec2 src_size = textureSize(color_texture, 0);
|
||||
// Determine the range of the source image that the target pixel will cover.
|
||||
vec2 scale = src_size * (1.0f / dst_size);
|
||||
color = AreaSampling(color_texture, frag_tex_coord, vec4(
|
||||
(frag_tex_coord * src_size) - (scale * 0.5f),
|
||||
// (scale * 0.5f) + scale
|
||||
// {A / 2 + A} ==> {(A + 2A) / 2} ==> {3A / 2} ==> {A * (3/2)}
|
||||
(frag_tex_coord * src_size) + scale * 0.5f
|
||||
));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user