forked from openrs2/openrs2
parent
a7c9f3eb17
commit
c276610a6d
@ -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 == "<clinit>" } |
||||||
|
if (clinit == null) { |
||||||
|
clinit = MethodNode(Opcodes.ACC_STATIC, "<clinit>", "()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<List<AbstractInsnNode>>() |
||||||
|
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") |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue