mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 11:11:10 +02:00
Merge pull request #1448 from AverageConsumer/codex/android-companion-contract
This commit is contained in:
+9
-3
@@ -733,9 +733,15 @@ the finished `worldCanvas` and `uiCanvas` with their SGB `zones` / `worldZones`,
|
|||||||
`worldActive`, the frame metrics (`ww`, `wh`, `pw`, `ph`, `ox`, `oy`, `vpw`,
|
`worldActive`, the frame metrics (`ww`, `wh`, `pw`, `ph`, `ox`, `oy`, `vpw`,
|
||||||
`vph`, `scale`, `Sx`, `Sy`, `dpiX`, `dpiY`), `renderer:blitCanvas(...)` for a
|
`vph`, `scale`, `Sx`, `Sy`, `dpiX`, `dpiY`), `renderer:blitCanvas(...)` for a
|
||||||
palette-correct blit of either canvas into an arbitrary screen rect, and the
|
palette-correct blit of either canvas into an arbitrary screen rect, and the
|
||||||
`secondScreen` bridge (`available()` / `push(imageData, w, h)` / `pollTouch()` /
|
`secondScreen` bridge (`available()` / `detected()` / `push(...)` /
|
||||||
`setEnabled`) for driving a second physical display. `pollTouch()` returns the
|
`pollTouch()` / `setEnabled`) for driving a second physical display.
|
||||||
oldest queued event as `"action,x,y"` in submitted-frame coordinates, or `nil`.
|
`detected()` reports a connected target even while its output is being created;
|
||||||
|
`available()` means it can accept a frame now. `push(imageData, w, h)` retains
|
||||||
|
the original contract. Its optional `background` (`0xRRGGBB`) and `preference`
|
||||||
|
arguments request an extended presentation; a preference ending in `:cover`
|
||||||
|
fills and crops the target, while other values preserve the whole frame.
|
||||||
|
`pollTouch()` returns the oldest queued event as `"action,x,y"` in submitted-frame
|
||||||
|
coordinates, or `nil`.
|
||||||
This is what lets a mod lay the two passes out as two stacked Game Boy screens,
|
This is what lets a mod lay the two passes out as two stacked Game Boy screens,
|
||||||
or push one onto a second screen, without the engine knowing the layout.
|
or push one onto a second screen, without the engine knowing the layout.
|
||||||
On process-capable Windows, Linux and macOS hosts without a native display
|
On process-capable Windows, Linux and macOS hosts without a native display
|
||||||
|
|||||||
@@ -1229,6 +1229,54 @@ void love_android_secondary_enable(int on)
|
|||||||
env->DeleteLocalRef(activity);
|
env->DeleteLocalRef(activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extern "C" __attribute__((visibility("default")))
|
||||||
|
int love_android_secondary_detected()
|
||||||
|
{
|
||||||
|
JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
|
||||||
|
jclass activity = env->FindClass("org/love2d/android/GameActivity");
|
||||||
|
jmethodID method = env->GetStaticMethodID(activity,
|
||||||
|
"hasSecondaryDisplayCandidate", "()Z");
|
||||||
|
jboolean detected = JNI_FALSE;
|
||||||
|
if (method)
|
||||||
|
detected = env->CallStaticBooleanMethod(activity, method);
|
||||||
|
else
|
||||||
|
env->ExceptionClear();
|
||||||
|
env->DeleteLocalRef(activity);
|
||||||
|
return detected ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" __attribute__((visibility("default")))
|
||||||
|
int love_android_present_secondary(const void *rgba, int width, int height,
|
||||||
|
unsigned int background, int cover)
|
||||||
|
{
|
||||||
|
if (!rgba || width <= 0 || height <= 0)
|
||||||
|
return 0;
|
||||||
|
jlong size = (jlong) width * (jlong) height * 4;
|
||||||
|
if (size <= 0)
|
||||||
|
return 0;
|
||||||
|
JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
|
||||||
|
jclass activity = env->FindClass("org/love2d/android/GameActivity");
|
||||||
|
jmethodID method = env->GetStaticMethodID(activity, "presentSecondaryFrame",
|
||||||
|
"(Ljava/nio/ByteBuffer;IIIZ)Z");
|
||||||
|
if (!method)
|
||||||
|
{
|
||||||
|
env->ExceptionClear();
|
||||||
|
env->DeleteLocalRef(activity);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
jobject frame = env->NewDirectByteBuffer((void *) rgba, size);
|
||||||
|
if (!frame)
|
||||||
|
{
|
||||||
|
env->DeleteLocalRef(activity);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
jboolean shown = env->CallStaticBooleanMethod(activity, method, frame,
|
||||||
|
width, height, (jint) background, cover ? JNI_TRUE : JNI_FALSE);
|
||||||
|
env->DeleteLocalRef(frame);
|
||||||
|
env->DeleteLocalRef(activity);
|
||||||
|
return shown ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
extern "C" __attribute__((visibility("default")))
|
extern "C" __attribute__((visibility("default")))
|
||||||
const char *love_android_poll_secondary_touch()
|
const char *love_android_poll_secondary_touch()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1478,6 +1478,14 @@ public class GameActivity extends SDLActivity {
|
|||||||
// in src/jni/love/src/common/android.cpp.
|
// in src/jni/love/src/common/android.cpp.
|
||||||
private static volatile SecondaryPresentation secondaryPresentation;
|
private static volatile SecondaryPresentation secondaryPresentation;
|
||||||
private static volatile boolean secondaryEnabled = false;
|
private static volatile boolean secondaryEnabled = false;
|
||||||
|
private static volatile byte[] secondaryFrame;
|
||||||
|
private static volatile int secondaryFrameWidth;
|
||||||
|
private static volatile int secondaryFrameHeight;
|
||||||
|
private static volatile int secondaryBackground;
|
||||||
|
private static volatile boolean secondaryFrameCover;
|
||||||
|
private static final Object secondaryFrameLock = new Object();
|
||||||
|
private static volatile long secondaryDetectionAt;
|
||||||
|
private static volatile boolean secondaryDetected;
|
||||||
private SecondaryDisplayMonitor secondaryDisplayMonitor;
|
private SecondaryDisplayMonitor secondaryDisplayMonitor;
|
||||||
private static final int MAX_SECONDARY_TOUCHES = 32;
|
private static final int MAX_SECONDARY_TOUCHES = 32;
|
||||||
private static final java.util.ArrayDeque<String> secondaryTouches =
|
private static final java.util.ArrayDeque<String> secondaryTouches =
|
||||||
@@ -1496,6 +1504,7 @@ public class GameActivity extends SDLActivity {
|
|||||||
} else {
|
} else {
|
||||||
self.unregisterSecondaryDisplayListener();
|
self.unregisterSecondaryDisplayListener();
|
||||||
teardownSecondaryDisplay();
|
teardownSecondaryDisplay();
|
||||||
|
synchronized (secondaryFrameLock) { secondaryFrame = null; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1532,24 +1541,7 @@ public class GameActivity extends SDLActivity {
|
|||||||
GameActivity self = (GameActivity) mSingleton;
|
GameActivity self = (GameActivity) mSingleton;
|
||||||
if (self == null || !secondaryEnabled || secondaryPresentation != null) return;
|
if (self == null || !secondaryEnabled || secondaryPresentation != null) return;
|
||||||
try {
|
try {
|
||||||
android.hardware.display.DisplayManager dm =
|
Display chosen = findSecondaryDisplay(self, true);
|
||||||
(android.hardware.display.DisplayManager) self.getSystemService(Context.DISPLAY_SERVICE);
|
|
||||||
if (dm == null) return;
|
|
||||||
Display chosen = null;
|
|
||||||
for (Display d : dm.getDisplays()) {
|
|
||||||
android.graphics.Point size = new android.graphics.Point();
|
|
||||||
d.getRealSize(size);
|
|
||||||
Log.d("GameActivity", "display id=" + d.getDisplayId() + " name=" + d.getName()
|
|
||||||
+ " size=" + size.x + "x" + size.y);
|
|
||||||
if (chosen == null && d.getDisplayId() != Display.DEFAULT_DISPLAY) {
|
|
||||||
chosen = d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (chosen == null) {
|
|
||||||
Display[] pres =
|
|
||||||
dm.getDisplays(android.hardware.display.DisplayManager.DISPLAY_CATEGORY_PRESENTATION);
|
|
||||||
if (pres != null && pres.length > 0) chosen = pres[0];
|
|
||||||
}
|
|
||||||
if (chosen == null) {
|
if (chosen == null) {
|
||||||
Log.d("GameActivity", "no secondary display found");
|
Log.d("GameActivity", "no secondary display found");
|
||||||
return;
|
return;
|
||||||
@@ -1557,6 +1549,13 @@ public class GameActivity extends SDLActivity {
|
|||||||
SecondaryPresentation p = new SecondaryPresentation(self, chosen);
|
SecondaryPresentation p = new SecondaryPresentation(self, chosen);
|
||||||
p.show();
|
p.show();
|
||||||
secondaryPresentation = p;
|
secondaryPresentation = p;
|
||||||
|
synchronized (secondaryFrameLock) {
|
||||||
|
if (secondaryFrame != null) {
|
||||||
|
p.setBackground(secondaryBackground);
|
||||||
|
p.updateFrame(java.nio.ByteBuffer.wrap(secondaryFrame),
|
||||||
|
secondaryFrameWidth, secondaryFrameHeight, secondaryFrameCover);
|
||||||
|
}
|
||||||
|
}
|
||||||
Log.d("GameActivity", "secondary display presentation started on id=" + chosen.getDisplayId());
|
Log.d("GameActivity", "secondary display presentation started on id=" + chosen.getDisplayId());
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
Log.d("GameActivity", "secondary display setup failed: " + t);
|
Log.d("GameActivity", "secondary display setup failed: " + t);
|
||||||
@@ -1564,6 +1563,28 @@ public class GameActivity extends SDLActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Display findSecondaryDisplay(GameActivity self, boolean logDisplays) {
|
||||||
|
android.hardware.display.DisplayManager dm =
|
||||||
|
(android.hardware.display.DisplayManager) self.getSystemService(Context.DISPLAY_SERVICE);
|
||||||
|
if (dm == null) return null;
|
||||||
|
Display chosen = null;
|
||||||
|
for (Display d : dm.getDisplays()) {
|
||||||
|
if (logDisplays) {
|
||||||
|
android.graphics.Point size = new android.graphics.Point();
|
||||||
|
d.getRealSize(size);
|
||||||
|
Log.d("GameActivity", "display id=" + d.getDisplayId()
|
||||||
|
+ " name=" + d.getName() + " size=" + size.x + "x" + size.y);
|
||||||
|
}
|
||||||
|
if (chosen == null && d.getDisplayId() != Display.DEFAULT_DISPLAY) chosen = d;
|
||||||
|
}
|
||||||
|
if (chosen == null) {
|
||||||
|
Display[] presentations =
|
||||||
|
dm.getDisplays(android.hardware.display.DisplayManager.DISPLAY_CATEGORY_PRESENTATION);
|
||||||
|
if (presentations != null && presentations.length > 0) chosen = presentations[0];
|
||||||
|
}
|
||||||
|
return chosen;
|
||||||
|
}
|
||||||
|
|
||||||
private static void teardownSecondaryDisplay() {
|
private static void teardownSecondaryDisplay() {
|
||||||
SecondaryPresentation p = secondaryPresentation;
|
SecondaryPresentation p = secondaryPresentation;
|
||||||
secondaryPresentation = null;
|
secondaryPresentation = null;
|
||||||
@@ -1597,9 +1618,14 @@ public class GameActivity extends SDLActivity {
|
|||||||
return manager.getDisplay(displayId) != null;
|
return manager.getDisplay(displayId) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public void onDisplayAdded(int displayId) { refreshSecondaryDisplay(); }
|
private void changed() {
|
||||||
@Override public void onDisplayRemoved(int displayId) { refreshSecondaryDisplay(); }
|
secondaryDetectionAt = 0;
|
||||||
@Override public void onDisplayChanged(int displayId) { refreshSecondaryDisplay(); }
|
refreshSecondaryDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void onDisplayAdded(int displayId) { changed(); }
|
||||||
|
@Override public void onDisplayRemoved(int displayId) { changed(); }
|
||||||
|
@Override public void onDisplayChanged(int displayId) { changed(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@@ -1607,6 +1633,56 @@ public class GameActivity extends SDLActivity {
|
|||||||
return secondaryPresentation != null;
|
return secondaryPresentation != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public static boolean hasSecondaryDisplayCandidate() {
|
||||||
|
GameActivity self = (GameActivity) mSingleton;
|
||||||
|
if (self == null) return false;
|
||||||
|
if (secondaryPresentation != null) return true;
|
||||||
|
long now = android.os.SystemClock.uptimeMillis();
|
||||||
|
if (secondaryDetectionAt != 0 && now - secondaryDetectionAt < 500) {
|
||||||
|
return secondaryDetected;
|
||||||
|
}
|
||||||
|
secondaryDetected = findSecondaryDisplay(self, false) != null;
|
||||||
|
secondaryDetectionAt = now;
|
||||||
|
return secondaryDetected;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public static boolean presentSecondaryFrame(
|
||||||
|
java.nio.ByteBuffer rgba, int width, int height,
|
||||||
|
int backgroundColor, boolean cover) {
|
||||||
|
long bytes = (long) width * height * 4;
|
||||||
|
if (rgba == null || width <= 0 || height <= 0
|
||||||
|
|| bytes <= 0 || bytes > Integer.MAX_VALUE
|
||||||
|
|| rgba.capacity() < bytes) return false;
|
||||||
|
synchronized (secondaryFrameLock) {
|
||||||
|
if (secondaryFrame == null || secondaryFrame.length != (int) bytes) {
|
||||||
|
secondaryFrame = new byte[(int) bytes];
|
||||||
|
}
|
||||||
|
rgba.rewind();
|
||||||
|
rgba.get(secondaryFrame, 0, (int) bytes);
|
||||||
|
rgba.rewind();
|
||||||
|
secondaryFrameWidth = width;
|
||||||
|
secondaryFrameHeight = height;
|
||||||
|
secondaryBackground = backgroundColor;
|
||||||
|
secondaryFrameCover = cover;
|
||||||
|
SecondaryPresentation p = secondaryPresentation;
|
||||||
|
if (p == null) return false;
|
||||||
|
try {
|
||||||
|
p.setBackground(backgroundColor);
|
||||||
|
p.updateFrame(rgba, width, height, cover);
|
||||||
|
return true;
|
||||||
|
} catch (Throwable t) {
|
||||||
|
GameActivity self = (GameActivity) mSingleton;
|
||||||
|
if (self != null) self.runOnUiThread(() -> {
|
||||||
|
teardownSecondaryDisplay();
|
||||||
|
setupSecondaryDisplay();
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
public static void updateSecondaryFrame(java.nio.ByteBuffer buf, int w, int h) {
|
public static void updateSecondaryFrame(java.nio.ByteBuffer buf, int w, int h) {
|
||||||
SecondaryPresentation p = secondaryPresentation;
|
SecondaryPresentation p = secondaryPresentation;
|
||||||
@@ -1680,6 +1756,14 @@ public class GameActivity extends SDLActivity {
|
|||||||
void updateFrame(java.nio.ByteBuffer buf, int w, int h) {
|
void updateFrame(java.nio.ByteBuffer buf, int w, int h) {
|
||||||
frameView.updateFrame(buf, w, h);
|
frameView.updateFrame(buf, w, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void updateFrame(java.nio.ByteBuffer buf, int w, int h, boolean cover) {
|
||||||
|
frameView.updateFrame(buf, w, h, cover);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setBackground(int color) {
|
||||||
|
frameView.setFrameBackground(color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class FrameView extends View {
|
private static class FrameView extends View {
|
||||||
@@ -1688,7 +1772,9 @@ public class GameActivity extends SDLActivity {
|
|||||||
private final android.graphics.Paint paint = new android.graphics.Paint();
|
private final android.graphics.Paint paint = new android.graphics.Paint();
|
||||||
private final Object lock = new Object();
|
private final Object lock = new Object();
|
||||||
private int fw, fh;
|
private int fw, fh;
|
||||||
|
private int backgroundColor = 0xFF000000;
|
||||||
private int activePointer = -1;
|
private int activePointer = -1;
|
||||||
|
private boolean cover;
|
||||||
|
|
||||||
FrameView(Context context) {
|
FrameView(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
@@ -1698,7 +1784,12 @@ public class GameActivity extends SDLActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void updateFrame(java.nio.ByteBuffer buf, int w, int h) {
|
void updateFrame(java.nio.ByteBuffer buf, int w, int h) {
|
||||||
|
updateFrame(buf, w, h, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateFrame(java.nio.ByteBuffer buf, int w, int h, boolean cover) {
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
|
this.cover = cover;
|
||||||
if (bitmap == null || fw != w || fh != h) {
|
if (bitmap == null || fw != w || fh != h) {
|
||||||
if (bitmap != null) bitmap.recycle();
|
if (bitmap != null) bitmap.recycle();
|
||||||
bitmap = android.graphics.Bitmap.createBitmap(w, h, android.graphics.Bitmap.Config.ARGB_8888);
|
bitmap = android.graphics.Bitmap.createBitmap(w, h, android.graphics.Bitmap.Config.ARGB_8888);
|
||||||
@@ -1710,6 +1801,13 @@ public class GameActivity extends SDLActivity {
|
|||||||
postInvalidate();
|
postInvalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setFrameBackground(int color) {
|
||||||
|
synchronized (lock) {
|
||||||
|
backgroundColor = 0xFF000000 | (color & 0x00FFFFFF);
|
||||||
|
}
|
||||||
|
postInvalidate();
|
||||||
|
}
|
||||||
|
|
||||||
private void enqueueTouch(String event) {
|
private void enqueueTouch(String event) {
|
||||||
synchronized (secondaryTouches) {
|
synchronized (secondaryTouches) {
|
||||||
if (secondaryTouches.size() >= MAX_SECONDARY_TOUCHES) {
|
if (secondaryTouches.size() >= MAX_SECONDARY_TOUCHES) {
|
||||||
@@ -1761,12 +1859,15 @@ public class GameActivity extends SDLActivity {
|
|||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
if (bitmap == null || fw == 0 || fh == 0) return;
|
if (bitmap == null || fw == 0 || fh == 0) return;
|
||||||
int vw = getWidth(), vh = getHeight();
|
int vw = getWidth(), vh = getHeight();
|
||||||
int s = Math.min(vw / fw, vh / fh);
|
float fit = Math.min((float) vw / fw, (float) vh / fh);
|
||||||
if (s < 1) s = 1;
|
if (fit <= 0) return;
|
||||||
int dw = fw * s, dh = fh * s;
|
float scale = cover
|
||||||
|
? Math.max((float) vw / fw, (float) vh / fh)
|
||||||
|
: fit >= 2f ? (float) Math.floor(fit) : fit;
|
||||||
|
int dw = Math.round(fw * scale), dh = Math.round(fh * scale);
|
||||||
int dx = (vw - dw) / 2, dy = (vh - dh) / 2;
|
int dx = (vw - dw) / 2, dy = (vh - dh) / 2;
|
||||||
dst.set(dx, dy, dx + dw, dy + dh);
|
dst.set(dx, dy, dx + dw, dy + dh);
|
||||||
canvas.drawColor(0xFF000000);
|
canvas.drawColor(backgroundColor);
|
||||||
canvas.drawBitmap(bitmap, null, dst, paint);
|
canvas.drawBitmap(bitmap, null, dst, paint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ local SecondScreen = {}
|
|||||||
local C = nil
|
local C = nil
|
||||||
local ffi = nil
|
local ffi = nil
|
||||||
local desktop = nil
|
local desktop = nil
|
||||||
|
local nativePresent = false
|
||||||
|
|
||||||
local function log(msg)
|
local function log(msg)
|
||||||
pcall(function() require("src.core.Logger").info("SecondScreen: %s", msg) end)
|
pcall(function() require("src.core.Logger").info("SecondScreen: %s", msg) end)
|
||||||
@@ -21,6 +22,9 @@ do
|
|||||||
int love_android_secondary_ready();
|
int love_android_secondary_ready();
|
||||||
void love_android_push_secondary(const void *rgba, int w, int h);
|
void love_android_push_secondary(const void *rgba, int w, int h);
|
||||||
void love_android_secondary_enable(int on);
|
void love_android_secondary_enable(int on);
|
||||||
|
int love_android_secondary_detected();
|
||||||
|
int love_android_present_secondary(const void *rgba, int w, int h,
|
||||||
|
unsigned int background, int cover);
|
||||||
const char *love_android_poll_secondary_touch();
|
const char *love_android_poll_secondary_touch();
|
||||||
]])
|
]])
|
||||||
local okLib, lib = pcall(ffi.load, "love")
|
local okLib, lib = pcall(ffi.load, "love")
|
||||||
@@ -34,6 +38,16 @@ do
|
|||||||
log(("bridge symbols not found (ffi.load ok=%s); second display disabled")
|
log(("bridge symbols not found (ffi.load ok=%s); second display disabled")
|
||||||
:format(tostring(okLib)))
|
:format(tostring(okLib)))
|
||||||
end
|
end
|
||||||
|
if C then
|
||||||
|
local okDetected, detected = pcall(function()
|
||||||
|
return C.love_android_secondary_detected
|
||||||
|
end)
|
||||||
|
local okPresent, present = pcall(function()
|
||||||
|
return C.love_android_present_secondary
|
||||||
|
end)
|
||||||
|
nativePresent = okDetected and detected ~= nil
|
||||||
|
and okPresent and present ~= nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -60,6 +74,10 @@ end
|
|||||||
-- distinction lets a companion retry its first frame after hotplug/re-target.
|
-- distinction lets a companion retry its first frame after hotplug/re-target.
|
||||||
function SecondScreen.detected()
|
function SecondScreen.detected()
|
||||||
if desktop then return desktop.detected() end
|
if desktop then return desktop.detected() end
|
||||||
|
if nativePresent then
|
||||||
|
local ok, r = pcall(C.love_android_secondary_detected)
|
||||||
|
return ok and r ~= 0
|
||||||
|
end
|
||||||
return SecondScreen.available()
|
return SecondScreen.available()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -68,6 +86,13 @@ function SecondScreen.push(imageData, w, h, background, preference)
|
|||||||
return desktop.push(imageData, w, h, background, preference)
|
return desktop.push(imageData, w, h, background, preference)
|
||||||
end
|
end
|
||||||
if not C or not imageData then return false end
|
if not C or not imageData then return false end
|
||||||
|
if nativePresent and (background ~= nil or preference ~= nil) then
|
||||||
|
local cover = type(preference) == "string"
|
||||||
|
and preference:sub(-6) == ":cover"
|
||||||
|
local ok, shown = pcall(C.love_android_present_secondary,
|
||||||
|
imageData:getFFIPointer(), w, h, background or 0, cover and 1 or 0)
|
||||||
|
return ok and shown ~= 0
|
||||||
|
end
|
||||||
return pcall(function()
|
return pcall(function()
|
||||||
C.love_android_push_secondary(imageData:getFFIPointer(), w, h)
|
C.love_android_push_secondary(imageData:getFFIPointer(), w, h)
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
local function read(path)
|
||||||
|
local file = assert(io.open(path, "rb"))
|
||||||
|
local source = file:read("*a")
|
||||||
|
file:close()
|
||||||
|
return source
|
||||||
|
end
|
||||||
|
|
||||||
|
local function check(value, message)
|
||||||
|
if not value then error(message, 2) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local java = read(
|
||||||
|
"mobile/android/love/src/main/java/org/love2d/android/GameActivity.java")
|
||||||
|
local cpp = read("mobile/android/love/src/jni/love/src/common/android.cpp")
|
||||||
|
|
||||||
|
check(java:find("hasSecondaryDisplayCandidate", 1, true)
|
||||||
|
and java:find("findSecondaryDisplay(self, false)", 1, true)
|
||||||
|
and java:find("now %- secondaryDetectionAt < 500"),
|
||||||
|
"Android exposes cached physical detection before Presentation is ready")
|
||||||
|
check(java:find("presentSecondaryFrame", 1, true)
|
||||||
|
and java:find("secondaryFrame = new byte", 1, true)
|
||||||
|
and java:find("rgba.get(secondaryFrame", 1, true),
|
||||||
|
"extended presentation reuses a retained frame buffer")
|
||||||
|
check(java:find("java.nio.ByteBuffer.wrap(secondaryFrame)", 1, true),
|
||||||
|
"a recreated Presentation receives the retained frame")
|
||||||
|
check(java:find("0xFF000000 | (color & 0x00FFFFFF)", 1, true),
|
||||||
|
"RGB companion backgrounds become opaque Android colors")
|
||||||
|
check(java:find("Math.max((float) vw / fw, (float) vh / fh)", 1, true)
|
||||||
|
and java:find("Math.floor(fit)", 1, true),
|
||||||
|
"FrameView supports cover and pixel-friendly contain fits")
|
||||||
|
check(cpp:find("love_android_secondary_detected", 1, true)
|
||||||
|
and cpp:find("love_android_present_secondary", 1, true)
|
||||||
|
and cpp:find('"(Ljava/nio/ByteBuffer;IIIZ)Z"', 1, true),
|
||||||
|
"JNI exports the optional detected and presentation calls")
|
||||||
|
|
||||||
|
print("android secondary presentation: ok")
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
|
local T = require("tests.harness")
|
||||||
|
local name = "src.render.SecondScreen"
|
||||||
|
local oldModule = package.loaded[name]
|
||||||
|
local oldFfi = package.loaded.ffi
|
||||||
|
local oldPreload = package.preload.ffi
|
||||||
|
local calls = {}
|
||||||
|
local null = {}
|
||||||
|
|
||||||
|
local C = {
|
||||||
|
love_android_secondary_ready = function() return 0 end,
|
||||||
|
love_android_push_secondary = function(ptr, w, h)
|
||||||
|
calls.push = { ptr, w, h }
|
||||||
|
end,
|
||||||
|
love_android_secondary_enable = function(on) calls.enabled = on end,
|
||||||
|
love_android_secondary_detected = function() return 1 end,
|
||||||
|
love_android_present_secondary = function(ptr, w, h, background, cover)
|
||||||
|
calls.present = { ptr, w, h, background, cover }
|
||||||
|
return 1
|
||||||
|
end,
|
||||||
|
love_android_poll_secondary_touch = function() return null end,
|
||||||
|
}
|
||||||
|
local fakeFfi = {
|
||||||
|
C = C,
|
||||||
|
NULL = null,
|
||||||
|
cdef = function() end,
|
||||||
|
load = function() return C end,
|
||||||
|
string = function(value) return value end,
|
||||||
|
}
|
||||||
|
|
||||||
|
package.loaded[name] = nil
|
||||||
|
package.loaded.ffi = nil
|
||||||
|
package.preload.ffi = function() return fakeFfi end
|
||||||
|
|
||||||
|
local SecondScreen = require(name)
|
||||||
|
local image = { getFFIPointer = function() return "pixels" end }
|
||||||
|
|
||||||
|
T.eq(SecondScreen.available(), false,
|
||||||
|
"an unbound presentation is not render-ready")
|
||||||
|
T.eq(SecondScreen.detected(), true,
|
||||||
|
"physical display detection is independent of presentation readiness")
|
||||||
|
T.eq(SecondScreen.push(image, 160, 144, 0x112233, "secondary:cover"), true,
|
||||||
|
"extended Android presentation accepts frame metadata")
|
||||||
|
T.same(calls.present, { "pixels", 160, 144, 0x112233, 1 },
|
||||||
|
"cover and RGB background reach the native bridge")
|
||||||
|
T.eq(SecondScreen.push(image, 160, 144, 0x112233, "secondary"), true,
|
||||||
|
"contain presentation remains available")
|
||||||
|
T.same(calls.present, { "pixels", 160, 144, 0x112233, 0 },
|
||||||
|
"contain is the default native fit")
|
||||||
|
T.eq(SecondScreen.push(image, 160, 144, nil, "secondary:cover"), true,
|
||||||
|
"a fit preference can request extended presentation by itself")
|
||||||
|
T.same(calls.present, { "pixels", 160, 144, 0, 1 },
|
||||||
|
"preference-only presentation defaults to a black background")
|
||||||
|
T.eq(SecondScreen.push(image, 160, 144), true,
|
||||||
|
"the original push ABI remains available")
|
||||||
|
T.same(calls.push, { "pixels", 160, 144 },
|
||||||
|
"legacy callers retain the original frame path")
|
||||||
|
|
||||||
|
package.loaded[name] = oldModule
|
||||||
|
package.loaded.ffi = oldFfi
|
||||||
|
package.preload.ffi = oldPreload
|
||||||
|
|
||||||
|
T.finish("Android secondary presentation facade")
|
||||||
Reference in New Issue
Block a user