diff --git a/bundler/src/main/java/dev/openrs2/bundler/BundlerModule.kt b/bundler/src/main/java/dev/openrs2/bundler/BundlerModule.kt index 0e0269e1..b8281174 100644 --- a/bundler/src/main/java/dev/openrs2/bundler/BundlerModule.kt +++ b/bundler/src/main/java/dev/openrs2/bundler/BundlerModule.kt @@ -6,6 +6,7 @@ import dev.openrs2.asm.transform.Transformer import dev.openrs2.bundler.transform.BufferSizeTransformer import dev.openrs2.bundler.transform.CachePathTransformer import dev.openrs2.bundler.transform.DomainTransformer +import dev.openrs2.bundler.transform.HighDpiTransformer import dev.openrs2.bundler.transform.HostCheckTransformer import dev.openrs2.bundler.transform.LoadLibraryTransformer import dev.openrs2.bundler.transform.MacResizeTransformer @@ -36,5 +37,6 @@ public object BundlerModule : AbstractModule() { binder.addBinding().to(PublicKeyTransformer::class.java) binder.addBinding().to(RightClickTransformer::class.java) binder.addBinding().to(TypoTransformer::class.java) + binder.addBinding().to(HighDpiTransformer::class.java) } } diff --git a/bundler/src/main/java/dev/openrs2/bundler/transform/HighDpiTransformer.kt b/bundler/src/main/java/dev/openrs2/bundler/transform/HighDpiTransformer.kt new file mode 100644 index 00000000..a3bb1abe --- /dev/null +++ b/bundler/src/main/java/dev/openrs2/bundler/transform/HighDpiTransformer.kt @@ -0,0 +1,358 @@ +package dev.openrs2.bundler.transform + +import com.github.michaelbull.logging.InlineLogger +import dev.openrs2.asm.InsnMatcher +import dev.openrs2.asm.classpath.ClassPath +import dev.openrs2.asm.classpath.Library +import dev.openrs2.asm.getExpression +import dev.openrs2.asm.previousReal +import dev.openrs2.asm.stackMetadata +import dev.openrs2.asm.transform.Transformer +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.AbstractInsnNode +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.FieldInsnNode +import org.objectweb.asm.tree.FieldNode +import org.objectweb.asm.tree.InsnList +import org.objectweb.asm.tree.InsnNode +import org.objectweb.asm.tree.JumpInsnNode +import org.objectweb.asm.tree.LabelNode +import org.objectweb.asm.tree.LdcInsnNode +import org.objectweb.asm.tree.MethodInsnNode +import org.objectweb.asm.tree.MethodNode +import org.objectweb.asm.tree.ParameterNode +import org.objectweb.asm.tree.TypeInsnNode +import org.objectweb.asm.tree.VarInsnNode +import javax.inject.Singleton +import kotlin.math.max + +@Singleton +public class HighDpiTransformer : Transformer() { + private var isGl: Boolean = false + private var gameShell: String? = null + private var newMembers: Int = 0 + private var initBlocks: Int = 0 + private var scaledCalls: Int = 0 + + override fun preTransform(classPath: ClassPath) { + newMembers = 0 + initBlocks = 0 + scaledCalls = 0 + + isGl = classPath.libraryClasses.any { it.name in GL_IMPLS } + + gameShell = if (isGl) { + val client = classPath["client!client"] ?: classPath["client"] + client?.superClass?.name + } else { + null + } + + if (gameShell != null) { + logger.info { "Identified game shell: $gameShell" } + } + } + + override fun transformClass(classPath: ClassPath, library: Library, clazz: ClassNode): Boolean { + if (!isGl || gameShell == null) { + return false + } + + if (clazz.name == gameShell) { + addCanvasScaleField(clazz) + newMembers++ + } else if (clazz.name in GL_INTERFACES) { + addPixelZoomMethod(clazz, Opcodes.ACC_ABSTRACT) + newMembers++ + } else if (clazz.name in GL_IMPLS) { + addPixelZoomMethod(clazz, Opcodes.ACC_FINAL or Opcodes.ACC_NATIVE) + newMembers++ + } + + return false + } + + override fun transformCode(classPath: ClassPath, library: Library, clazz: ClassNode, method: MethodNode): Boolean { + if (!isGl || gameShell == null) { + return false + } + + var extraStack = 0 + + for (insn in method.instructions) { + if (insn !is MethodInsnNode || insn.opcode != Opcodes.INVOKEINTERFACE) { + continue + } else if (insn.owner !in GL_INTERFACES) { + continue + } + + if (insn.name == "glViewport" && insn.desc == "(IIII)V") { + if (method.access and Opcodes.ACC_STATIC == 0) { + /* + * Non-static glViewport() calls are used for off-screen + * framebuffers, so don't need scaling. + */ + continue + } + + if (transformBounds(method, insn)) { + scaledCalls++ + extraStack = max(extraStack, 4) + } + } else if (insn.name == "glScissor" && insn.desc == "(IIII)V") { + if (transformBounds(method, insn)) { + scaledCalls++ + extraStack = max(extraStack, 4) + } + } else if ((insn.name == "glPointSize" || insn.name == "glLineWidth") && insn.desc == "(F)V") { + transform(method, insn) + scaledCalls++ + extraStack = max(extraStack, 2) + } + } + + for (match in DRAW_PIXELS_MATCHER.match(method)) { + val aload = match[0] as VarInsnNode + val invoke = match[11] as MethodInsnNode + + if (invoke.owner !in GL_INTERFACES) { + continue + } else if (invoke.name != "glDrawPixels") { + continue + } else if (invoke.desc != "(IIIILjava/nio/Buffer;)V") { + continue + } + + transformDrawPixelsCall(method, aload, invoke) + scaledCalls++ + extraStack = max(extraStack, 3) + } + + for (match in GET_GL_MATCHER.match(method)) { + val invoke = match[1] as MethodInsnNode + val putstatic = match[2] as FieldInsnNode + + if (invoke.owner !in GL_CONTEXTS) { + continue + } else if (invoke.name != "getGL") { + continue + } else if (invoke.desc !in GET_GL_DESCRIPTORS) { + continue + } + + addDefaultLineWidthCall(method, invoke, putstatic) + initBlocks++ + extraStack = max(extraStack, 3) + } + + if (clazz.name == gameShell && (method.access and Opcodes.ACC_STATIC) == 0) { + for (match in SET_CANVAS_VISIBLE_MATCHER.match(method)) { + val getstatic = match[0] as FieldInsnNode + if (getstatic.desc != "Ljava/awt/Canvas;") { + continue + } + + val invoke = match[2] as MethodInsnNode + if (invoke.owner != "java/awt/Canvas" || invoke.name != "setVisible" || invoke.desc != "(Z)V") { + continue + } + + addScaleDetector(method, getstatic, invoke) + initBlocks++ + extraStack = max(extraStack, 2) + } + } + + method.maxStack += extraStack + return false + } + + override fun postTransform(classPath: ClassPath) { + if (!isGl) { + logger.info { "Skipped as the SD client does not require patching" } + } else if (gameShell == null) { + logger.info { "Skipped as the game shell could not be identified" } + } else { + logger.info { "Added $newMembers members and $initBlocks blocks of initialisation code" } + logger.info { "Scaled $scaledCalls OpenGL calls" } + } + } + + private fun addCanvasScaleField(clazz: ClassNode) { + clazz.fields.add(FieldNode(Opcodes.ACC_PUBLIC or Opcodes.ACC_STATIC, "canvasScale", "D", null, null)) + + var clinit = clazz.methods.find { it.name == "" } + if (clinit == null) { + clinit = MethodNode(Opcodes.ACC_STATIC, "", "()V", null, null) + clinit.instructions = InsnList() + clinit.instructions.add(InsnNode(Opcodes.RETURN)) + } + + val list = InsnList() + list.add(InsnNode(Opcodes.DCONST_1)) + list.add(FieldInsnNode(Opcodes.PUTSTATIC, clazz.name, "canvasScale", "D")) + clinit.instructions.insert(list) + } + + private fun addPixelZoomMethod(clazz: ClassNode, access: Int) { + val method = MethodNode( + Opcodes.ACC_PUBLIC or access, + "glPixelZoom", + "(FF)V", + null, + null + ) + method.parameters = mutableListOf(ParameterNode("xfactor", 0), ParameterNode("yfactor", 0)) + clazz.methods.add(method) + } + + private fun addScaleDetector(method: MethodNode, getstatic: FieldInsnNode, invoke: MethodInsnNode) { + val graphicsVar = method.maxLocals++ + val endLabel = LabelNode() + val noGraphics2dLabel = LabelNode() + + val list = InsnList() + list.add(FieldInsnNode(Opcodes.GETSTATIC, getstatic.owner, getstatic.name, getstatic.desc)) + list.add(MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/awt/Canvas", "getGraphics", "()Ljava/awt/Graphics;")) + list.add(VarInsnNode(Opcodes.ASTORE, graphicsVar)) + + list.add(VarInsnNode(Opcodes.ALOAD, graphicsVar)) + list.add(JumpInsnNode(Opcodes.IFNULL, endLabel)) + + list.add(VarInsnNode(Opcodes.ALOAD, graphicsVar)) + list.add(TypeInsnNode(Opcodes.INSTANCEOF, "java/awt/Graphics2D")) + list.add(JumpInsnNode(Opcodes.IFEQ, noGraphics2dLabel)) + + list.add(VarInsnNode(Opcodes.ALOAD, graphicsVar)) + list.add(TypeInsnNode(Opcodes.CHECKCAST, "java/awt/Graphics2D")) + list.add(MethodInsnNode( + Opcodes.INVOKEVIRTUAL, + "java/awt/Graphics2D", + "getTransform", + "()Ljava/awt/geom/AffineTransform;" + )) + list.add(MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/awt/geom/AffineTransform", "getScaleX", "()D")) + list.add(FieldInsnNode(Opcodes.PUTSTATIC, gameShell, "canvasScale", "D")) + list.add(JumpInsnNode(Opcodes.GOTO, endLabel)) + + list.add(noGraphics2dLabel) + list.add(InsnNode(Opcodes.DCONST_1)) + list.add(FieldInsnNode(Opcodes.PUTSTATIC, gameShell, "canvasScale", "D")) + + list.add(endLabel) + + method.instructions.insert(invoke, list) + } + + private fun addDefaultLineWidthCall(method: MethodNode, invoke: MethodInsnNode, putstatic: FieldInsnNode) { + val owner = if (invoke.desc.contains('!')) { + "gl!javax/media/opengl/GL" + } else { + "javax/media/opengl/GL" + } + + val list = InsnList() + list.add(FieldInsnNode(Opcodes.GETSTATIC, putstatic.owner, putstatic.name, putstatic.desc)) + list.add(FieldInsnNode(Opcodes.GETSTATIC, gameShell, "canvasScale", "D")) + list.add(InsnNode(Opcodes.D2F)) + list.add(MethodInsnNode(Opcodes.INVOKEINTERFACE, owner, "glLineWidth", "(F)V")) + method.instructions.insert(putstatic, list) + } + + private fun transformBounds(method: MethodNode, invoke: MethodInsnNode): Boolean { + val exprs = mutableListOf>() + var head = invoke.previousReal ?: return false + + while (exprs.size < 4) { + val expr = if (head.stackMetadata.pops == 0) { + listOf(head) + } else { + getExpression(head)?.plus(head) ?: return false + } + + if (invoke.name == "glViewport" && expr.any { it.opcode == Opcodes.IALOAD }) { + /* + * The glViewport() call that uses IALOAD restores viewport + * bounds previously saved with glGetIntegerv(), so there's no + * need for us to scale it again. + */ + return false + } + + exprs += expr + head = expr.first().previousReal ?: return false + } + + exprs.reverse() + + for (expr in exprs) { + val single = expr.singleOrNull() + if (single != null && single.opcode == Opcodes.ICONST_0) { + continue + } + + val list = InsnList() + list.add(InsnNode(Opcodes.I2D)) + list.add(FieldInsnNode(Opcodes.GETSTATIC, gameShell, "canvasScale", "D")) + list.add(InsnNode(Opcodes.DMUL)) + list.add(LdcInsnNode(0.5)) + list.add(InsnNode(Opcodes.DADD)) + list.add(InsnNode(Opcodes.D2I)) + method.instructions.insert(expr.last(), list) + } + + return true + } + + private fun transform(method: MethodNode, invoke: MethodInsnNode) { + val previous = invoke.previousReal!! + if (previous.opcode == Opcodes.FCONST_1) { + val list = InsnList() + list.add(FieldInsnNode(Opcodes.GETSTATIC, gameShell, "canvasScale", "D")) + list.add(InsnNode(Opcodes.D2F)) + method.instructions.insertBefore(invoke, list) + method.instructions.remove(previous) + } else { + val list = InsnList() + list.add(InsnNode(Opcodes.F2D)) + list.add(FieldInsnNode(Opcodes.GETSTATIC, gameShell, "canvasScale", "D")) + list.add(InsnNode(Opcodes.DMUL)) + list.add(InsnNode(Opcodes.D2F)) + method.instructions.insertBefore(invoke, list) + } + } + + private fun transformDrawPixelsCall(method: MethodNode, aload: VarInsnNode, invoke: MethodInsnNode) { + val enableZoom = InsnList() + enableZoom.add(VarInsnNode(Opcodes.ALOAD, aload.`var`)) + enableZoom.add(FieldInsnNode(Opcodes.GETSTATIC, gameShell, "canvasScale", "D")) + enableZoom.add(InsnNode(Opcodes.D2F)) + enableZoom.add(FieldInsnNode(Opcodes.GETSTATIC, gameShell, "canvasScale", "D")) + enableZoom.add(InsnNode(Opcodes.D2F)) + enableZoom.add(MethodInsnNode(Opcodes.INVOKEINTERFACE, invoke.owner, "glPixelZoom", "(FF)V")) + + val disableZoom = InsnList() + disableZoom.add(VarInsnNode(Opcodes.ALOAD, aload.`var`)) + disableZoom.add(InsnNode(Opcodes.FCONST_1)) + disableZoom.add(InsnNode(Opcodes.FCONST_1)) + disableZoom.add(MethodInsnNode(Opcodes.INVOKEINTERFACE, invoke.owner, "glPixelZoom", "(FF)V")) + + method.instructions.insertBefore(aload, enableZoom) + method.instructions.insert(invoke, disableZoom) + } + + private companion object { + private val logger = InlineLogger() + + private val GL_INTERFACES = setOf("javax/media/opengl/GL", "gl!javax/media/opengl/GL") + private val GL_IMPLS = setOf("jaggl/opengl", "gl!jaggl/opengl") + private val GL_CONTEXTS = setOf("javax/media/opengl/GLContext", "gl!javax/media/opengl/GLContext") + private val GET_GL_DESCRIPTORS = setOf("()Ljavax/media/opengl/GL;", "()Lgl!javax/media/opengl/GL;") + + private val DRAW_PIXELS_MATCHER = InsnMatcher.compile(""" + ALOAD ILOAD ILOAD LDC GETSTATIC IFEQ LDC GOTO SIPUSH ALOAD INVOKESTATIC INVOKEINTERFACE + """.trimIndent()) + private val GET_GL_MATCHER = InsnMatcher.compile("GETSTATIC INVOKEVIRTUAL PUTSTATIC") + private val SET_CANVAS_VISIBLE_MATCHER = InsnMatcher.compile("GETSTATIC ICONST_1 INVOKEVIRTUAL") + } +}