Add high quality WebGPU renderer (#3388)

This commit is contained in:
w
2026-08-14 10:01:00 -07:00
committed by GitHub
parent 581a89b7b0
commit 5c3ae37d52
16 changed files with 1731 additions and 4 deletions
@@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.data.coil
import android.graphics.Bitmap
import coil3.Canvas
import coil3.Image
import coil3.ImageLoader
import coil3.asImage
import coil3.decode.DecodeResult
@@ -62,7 +64,9 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
class Factory : Decoder.Factory {
override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? {
return if (options.customDecoder || isApplicable(result.source.source())) {
return if (options.newDecoder) {
ImageDecoder2(result.source, options)
} else if (options.customDecoder || isApplicable(result.source.source())) {
TachiyomiImageDecoder(result.source, options)
} else {
null
@@ -88,3 +92,27 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
var displayProfile: ByteArray? = null
}
}
class ImageDecoder2(private val resources: ImageSource, private val options: Options) : Decoder {
class DecodeResultImage(val res: ca.mpreg.imagedecoder.ImageDecoder.DecodeResult) : Image {
override val size: Long get() = res.image.capacity().toLong()
override val width: Int get() = res.width
override val height: Int get() = res.height
override val shareable: Boolean get() = true
override fun draw(canvas: Canvas) {}
}
override suspend fun decode(): DecodeResult {
val source = resources.source()
val decoder = ca.mpreg.imagedecoder.ImageDecoder.new(source.inputStream())
val res = decoder.decode()
return DecodeResult(
image = DecodeResultImage(res),
isSampled = false,
)
}
}
@@ -42,3 +42,11 @@ val Options.customDecoder: Boolean
get() = getExtra(customDecoderKey)
private val customDecoderKey = Extras.Key(default = false)
fun ImageRequest.Builder.newDecoder(enable: Boolean) = apply {
extras[newDecoderKey] = enable
}
val Options.newDecoder: Boolean
get() = getExtra(newDecoderKey)
private val newDecoderKey = Extras.Key(default = false)
@@ -78,6 +78,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsViewModel
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.webgpu.WebGpuViewer
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.system.isNightMode
import eu.kanade.tachiyomi.util.system.openInBrowser
@@ -479,7 +480,7 @@ class ReaderActivity : BaseActivity() {
onShare = ::shareChapter.takeIf { isHttpSource },
chapterNavigatorType = if (!verticalNavigator) {
if (state.viewer is R2LPagerViewer) {
if (state.viewer is R2LPagerViewer || (state.viewer as? WebGpuViewer)?.isReversed ?: false) {
ChapterNavigatorType.HORIZONTAL_RTL
} else {
ChapterNavigatorType.HORIZONTAL_LTR
@@ -132,6 +132,11 @@ class ReaderPreferences(
false,
)
val dualPageView: Preference<DualPageView> = preferenceStore.getEnum(
"pref_dual_page_view",
DualPageView.NEVER,
)
// endregion
// region Color filter
@@ -189,6 +194,15 @@ class ReaderPreferences(
// endregion
// region WebGpu
val transitionAnimation: Preference<TransitionAnimation> =
preferenceStore.getEnum("webgpu_transition_animation", TransitionAnimation.DEFAULT)
val cutoutMode: Preference<CutoutMode> = preferenceStore.getEnum("webgpu_cutout_mode", CutoutMode.AVOID)
// endregion
enum class FlashColor {
BLACK,
WHITE,
@@ -201,8 +215,14 @@ class ReaderPreferences(
val shouldInvertVertical: Boolean = false,
) {
NONE(MR.strings.tapping_inverted_none),
HORIZONTAL(MR.strings.tapping_inverted_horizontal, shouldInvertHorizontal = true),
VERTICAL(MR.strings.tapping_inverted_vertical, shouldInvertVertical = true),
HORIZONTAL(
MR.strings.tapping_inverted_horizontal,
shouldInvertHorizontal = true,
),
VERTICAL(
MR.strings.tapping_inverted_vertical,
shouldInvertVertical = true,
),
BOTH(MR.strings.tapping_inverted_both, shouldInvertHorizontal = true, shouldInvertVertical = true),
}
@@ -213,6 +233,41 @@ class ReaderPreferences(
LOWEST(47),
}
enum class TransitionAnimation(val titleRes: StringResource) {
DEFAULT(MR.strings.transition_animation_default),
FLIP_LEFT(MR.strings.transition_animation_flip_left),
FLIP_RIGHT(
MR.strings.transition_animation_flip_right,
),
STACK_LEFT(MR.strings.transition_animation_stack_left),
STACK_RIGHT(MR.strings.transition_animation_stack_right),
STACK_UP(
MR.strings.transition_animation_stack_up,
),
STACK_DOWN(MR.strings.transition_animation_stack_down),
SPHERE(MR.strings.transition_animation_sphere),
CUBE_INSIDE(
MR.strings.transition_animation_cube_inside,
),
CUBE_OUTSIDE(MR.strings.transition_animation_cube_outside),
FADE(MR.strings.transition_animation_fade),
FADE_WHITE(
MR.strings.transition_animation_fade_white,
),
}
enum class CutoutMode(val titleRes: StringResource) {
IGNORE(MR.strings.cutout_mode_ignore),
AVOID(MR.strings.cutout_mode_avoid),
SHIFT(MR.strings.cutout_mode_shift),
}
enum class DualPageView(val titleRes: StringResource) {
NEVER(MR.strings.dual_page_view_never),
ALWAYS(MR.strings.dual_page_view_always),
WIDE(MR.strings.dual_page_view_wide),
}
companion object {
const val WEBTOON_PADDING_MIN = 0
const val WEBTOON_PADDING_MAX = 25
@@ -2,14 +2,19 @@ package eu.kanade.tachiyomi.ui.reader.setting
import androidx.annotation.DrawableRes
import dev.icerock.moko.resources.StringResource
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.L2RPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.webgpu.WebGpuViewer
import eu.kanade.tachiyomi.ui.reader.viewer.webgpu.WebGpuViewerContinuous
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
enum class ReadingMode(
val stringRes: StringResource,
@@ -67,6 +72,16 @@ enum class ReadingMode(
}
fun toViewer(preference: Int?, activity: ReaderActivity): Viewer {
if (Injekt.get<BasePreferences>().highQualityRenderer.get()) {
return when (fromPreference(preference)) {
LEFT_TO_RIGHT -> WebGpuViewer(activity, isReversed = false, isVertical = false)
RIGHT_TO_LEFT -> WebGpuViewer(activity, isReversed = true, isVertical = false)
VERTICAL -> WebGpuViewer(activity, isReversed = false, isVertical = true)
WEBTOON -> WebGpuViewerContinuous(activity)
CONTINUOUS_VERTICAL -> WebGpuViewerContinuous(activity)
DEFAULT -> throw IllegalStateException("Preference value must be resolved: $preference")
}
}
return when (fromPreference(preference)) {
LEFT_TO_RIGHT -> L2RPagerViewer(activity)
RIGHT_TO_LEFT -> R2LPagerViewer(activity)
@@ -0,0 +1,180 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webgpu
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerConfig
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.DisabledNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.EdgeNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.KindlishNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.LNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.RightAndLeftNavigation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Configuration used by pager viewers.
*/
class WebGpuConfig(
private val viewer: WebGpuViewer,
scope: CoroutineScope,
readerPreferences: ReaderPreferences = Injekt.get(),
) : ViewerConfig(readerPreferences, scope) {
var theme = readerPreferences.readerTheme.get()
private set
var automaticBackground = false
private set
var dualPageSplitChangedListener: ((Boolean) -> Unit)? = null
var imageScaleType = 1
private set
var imageZoomType = ReaderPageImageView.ZoomStartPosition.LEFT
private set
var imageCropBorders = false
private set
var navigateToPan = false
private set
var landscapeZoom = false
private set
var transitionAnimation = ReaderPreferences.TransitionAnimation.DEFAULT
private set
var cutoutMode = ReaderPreferences.CutoutMode.AVOID
private set
var dualPageView = ReaderPreferences.DualPageView.NEVER
private set
init {
readerPreferences.readerTheme
.register(
{
theme = it
automaticBackground = it == 3
},
{ imagePropertyChangedListener?.invoke() },
)
readerPreferences.imageScaleType
.register({ imageScaleType = it }, { imagePropertyChangedListener?.invoke() })
readerPreferences.zoomStart
.register({ zoomTypeFromPreference(it) }, { imagePropertyChangedListener?.invoke() })
readerPreferences.cropBorders
.register({ imageCropBorders = it }, { imagePropertyChangedListener?.invoke() })
readerPreferences.navigateToPan
.register({ navigateToPan = it })
readerPreferences.landscapeZoom
.register({ landscapeZoom = it }, { imagePropertyChangedListener?.invoke() })
readerPreferences.navigationModePager
.register({ navigationMode = it }, { updateNavigation(navigationMode) })
readerPreferences.pagerNavInverted
.register({ tappingInverted = it }, { navigator.invertMode = it })
readerPreferences.pagerNavInverted.changes()
.drop(1)
.onEach { navigationModeChangedListener?.invoke() }
.launchIn(scope)
readerPreferences.dualPageSplitPaged
.register(
{ dualPageSplit = it },
{
imagePropertyChangedListener?.invoke()
dualPageSplitChangedListener?.invoke(it)
},
)
readerPreferences.dualPageInvertPaged
.register({ dualPageInvert = it }, { imagePropertyChangedListener?.invoke() })
readerPreferences.dualPageRotateToFit
.register(
{ dualPageRotateToFit = it },
{ imagePropertyChangedListener?.invoke() },
)
readerPreferences.dualPageRotateToFitInvert
.register(
{ dualPageRotateToFitInvert = it },
{ imagePropertyChangedListener?.invoke() },
)
readerPreferences.transitionAnimation
.register(
{ transitionAnimation = it },
{ imagePropertyChangedListener?.invoke() },
)
readerPreferences.cutoutMode
.register(
{ cutoutMode = it },
{ imagePropertyChangedListener?.invoke() },
)
readerPreferences.dualPageView
.register(
{ dualPageView = it },
{ imagePropertyChangedListener?.invoke() },
)
}
private fun zoomTypeFromPreference(value: Int) {
imageZoomType = when (value) {
// Auto
1 -> if (viewer.isReversed) {
ReaderPageImageView.ZoomStartPosition.RIGHT
} else {
ReaderPageImageView.ZoomStartPosition.LEFT
}
// Left
2 -> ReaderPageImageView.ZoomStartPosition.LEFT
// Right
3 -> ReaderPageImageView.ZoomStartPosition.RIGHT
// Center
else -> ReaderPageImageView.ZoomStartPosition.CENTER
}
}
override var navigator: ViewerNavigation = defaultNavigation()
set(value) {
field = value.also { it.invertMode = this.tappingInverted }
}
override fun defaultNavigation(): ViewerNavigation {
return if (viewer.isVertical) {
LNavigation()
} else {
RightAndLeftNavigation()
}
}
override fun updateNavigation(navigationMode: Int) {
navigator = when (navigationMode) {
0 -> defaultNavigation()
1 -> LNavigation()
2 -> KindlishNavigation()
3 -> EdgeNavigation()
4 -> RightAndLeftNavigation()
5 -> DisabledNavigation()
else -> defaultNavigation()
}
navigationModeChangedListener?.invoke()
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,26 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webgpu
import ca.mpreg.webgpuviewer.ImageViewContinuous
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
class WebGpuViewerContinuous(activity: ReaderActivity) :
WebGpuViewer(activity, isReversed = false, isVertical = true, pager = ImageViewContinuous(activity)) {
override val isContinuous: Boolean = true
private fun scrollByHalfPage(direction: Int) {
val state = (pager as ImageViewContinuous).state
val totalDistance = direction * state.height / 2f
state.animateScroll(totalDistance)
}
override fun moveRight() = scrollByHalfPage(1)
override fun moveLeft() = scrollByHalfPage(-1)
override fun moveToPage(page: ReaderPage) {
super.moveToPage(page)
(pager as ImageViewContinuous).state.scrollY = 0f
}
}