Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,4 @@ nunit-*.xml
/Brovan/Android/app/local.properties
/Brovan/Android/app/brovan/src/main/jniLibs/
/*.apk
/Brovan/Android/app/brovan/src/main/assets
3 changes: 3 additions & 0 deletions Brovan/Android/BrovanAndroidApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ public static int Init(byte* baseDirectory)
[UnmanagedCallersOnly(EntryPoint = "brovan_set_verbose")]
public static void SetVerbose(int enabled) => _verbose = enabled != 0;

[UnmanagedCallersOnly(EntryPoint = "brovan_set_jit_cache")]
public static void SetJitCache(int enabled) => UnicornCodeCache.Enabled = enabled != 0;

[UnmanagedCallersOnly(EntryPoint = "brovan_set_surface")]
public static void SetSurface(IntPtr nativeWindow, int width, int height, int densityDpi)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package dev.brovan.app;

import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Log;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
* Copies the guest-side files bundled in the APK into the emulated Windows filesystem.
*
* <p>The Vulkan shim is a Windows PE, so it cannot live in jniLibs like the host libraries. It is
* generated from vk.xml together with the managed marshalling layer, which means it only matches
* the build it shipped with and has to be refreshed whenever the app is updated.
*/
final class GuestAssets {

private static final String TAG = "Brovan";
private static final String SOURCE = "virtualfs";
private static final String STAMP = "virtualfs-assets.stamp";

private GuestAssets() {
}

static void deploy(Context context) {
File files = context.getFilesDir();
File stamp = new File(files, STAMP);
long updated = updateTime(context);

if (stamp.exists() && stamp.lastModified() >= updated) {
return;
}

File system32 = new File(files, "VirtualFS/C/Windows/System32");
File sysWow64 = new File(files, "VirtualFS/C/Windows/SysWOW64");

try {
copyDirectory(context, SOURCE + "/System32", system32);
copyDirectory(context, SOURCE + "/SysWOW64", sysWow64);

if (!stamp.exists() && !stamp.createNewFile()) {
return;
}
stamp.setLastModified(updated);
} catch (IOException error) {
Log.e(TAG, "Could not deploy the bundled guest files: " + error.getMessage());
}
}

private static long updateTime(Context context) {
try {
return context.getPackageManager()
.getPackageInfo(context.getPackageName(), 0)
.lastUpdateTime;
} catch (PackageManager.NameNotFoundException error) {
return System.currentTimeMillis();
}
}

private static void copyDirectory(Context context, String source, File target) throws IOException {
String[] names = context.getAssets().list(source);
if (names == null || names.length == 0) {
return;
}

if (!target.isDirectory() && !target.mkdirs()) {
throw new IOException("cannot create " + target);
}

for (String name : names) {
String child = source + "/" + name;
String[] nested = context.getAssets().list(child);

if (nested != null && nested.length != 0) {
copyDirectory(context, child, new File(target, name));
continue;
}

try (InputStream in = context.getAssets().open(child);
OutputStream out = new FileOutputStream(new File(target, name))) {
byte[] buffer = new byte[64 * 1024];
int read;
while ((read = in.read(buffer)) > 0) {
out.write(buffer, 0, read);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ protected void onCreate(Bundle savedInstanceState) {
library = new Library(this);
settings = new Settings(this);

// Refreshes the guest-side files bundled in the APK, so an app update cannot leave
// a stale Vulkan shim behind for the new emulator to load.
worker.execute(() -> GuestAssets.deploy(this));

drawer = findViewById(R.id.drawer);
content = findViewById(R.id.content);
toolbar = findViewById(R.id.toolbar);
Expand Down Expand Up @@ -467,6 +471,10 @@ private View createSettings() {
controls.setText(schemeLabels[settings.controlScheme()], false);
controls.setOnItemClickListener((parent, item, position, id) -> settings.setControlScheme(position));

MaterialSwitch jitCache = view.findViewById(R.id.jit_cache);
jitCache.setChecked(settings.jitCache());
jitCache.setOnCheckedChangeListener((button, checked) -> settings.setJitCache(checked));

MaterialSwitch developer = view.findViewById(R.id.developer);
developer.setChecked(settings.developerMode());
developer.setOnCheckedChangeListener((button, checked) -> settings.setDeveloperMode(checked));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public class PlayerActivity extends AppCompatActivity implements BrovanNative.Li
private static final String EXTRA_NETWORK = "network";
private static final String EXTRA_DEVELOPER = "developer";
private static final String EXTRA_CONTROLS = "controls";
private static final String EXTRA_JIT_CACHE = "jit_cache";

private static final int MAX_LINES = 1200;
private static final int TRIM_CHUNK = 200;
Expand All @@ -63,7 +64,8 @@ static Intent intentFor(Context context, Program program, Settings settings) {
.putExtra(EXTRA_NAME, program.name())
.putExtra(EXTRA_NETWORK, settings.network())
.putExtra(EXTRA_DEVELOPER, settings.developerMode())
.putExtra(EXTRA_CONTROLS, settings.controlScheme());
.putExtra(EXTRA_CONTROLS, settings.controlScheme())
.putExtra(EXTRA_JIT_CACHE, settings.jitCache());
}

@Override
Expand Down Expand Up @@ -110,6 +112,7 @@ private void start() {
}

BrovanNative.setVerbose(developerMode);
BrovanNative.setJitCache(getIntent().getBooleanExtra(EXTRA_JIT_CACHE, true));
setStatus(getIntent().getStringExtra(EXTRA_NAME));

// Developer mode leaves the guest at the debugger prompt instead of running it, so the console has to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ final class Settings {
private static final String KEY_DEVELOPER = "developer";
private static final String KEY_FIT_WINDOW = "fit_window";
private static final String KEY_CONTROLS = "controls";
private static final String KEY_JIT_CACHE = "jit_cache";

private final SharedPreferences preferences;

Expand Down Expand Up @@ -51,4 +52,12 @@ boolean fitWindow() {
void setFitWindow(boolean value) {
preferences.edit().putBoolean(KEY_FIT_WINDOW, value).apply();
}

boolean jitCache() {
return preferences.getBoolean(KEY_JIT_CACHE, true);
}

void setJitCache(boolean value) {
preferences.edit().putBoolean(KEY_JIT_CACHE, value).apply();
}
}
15 changes: 15 additions & 0 deletions Brovan/Android/app/brovan/src/main/res/layout/screen_settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@
android:textColor="@color/text_primary" />
</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/jit_cache"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/settings_jit_cache"
android:textColor="@color/text_primary" />

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/settings_jit_cache_summary"
android:textColor="@color/text_secondary"
android:textSize="13sp" />

<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/developer"
android:layout_width="match_parent"
Expand Down
2 changes: 2 additions & 0 deletions Brovan/Android/app/brovan/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
<string name="settings_advanced">Advanced</string>
<string name="settings_controls">On-screen controls</string>
<string name="settings_network">Network access</string>
<string name="settings_jit_cache">JIT code caching</string>
<string name="settings_jit_cache_summary">Experimental JIT code caching that can help with performance</string>
<string name="settings_developer">Developer mode</string>
<string name="settings_developer_summary">Show the console and emulator trace while a program runs</string>

Expand Down
38 changes: 30 additions & 8 deletions Brovan/Android/build-apk.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ CONFIG="${CONFIG:-Release}"
API_LEVEL="${API_LEVEL:-26}"
PUBLISH_DIR="${BROVAN_PUBLISH_DIR:-/tmp/brovan-android-publish}"
APK_OUTPUT="${BROVAN_APK_OUTPUT:-$REPO_ROOT/artifacts/android/brovan-arm64-v8a.apk}"
UNICORN_SRC="$REPO_ROOT/Brovan/.cache/unicorn/unicorn-2.1.4"
UNICORN_BUILD="$REPO_ROOT/Brovan/.cache/unicorn/build-android-arm64"
UNICORN_ARTIFACT="$UNICORN_BUILD/libunicorn.so"
# Resolved from Brovan.Unicorn.targets rather than hardcoded: the source tree is named
# after a hash of Brovan/native/unicorn, so editing a patch moves it.
UNICORN_SRC=""
UNICORN_PATCH_KEY=""

# Exit code 3 means "this host has no Android toolchain", which Brovan.Android/Brovan.Android.csproj treats
# as a skip rather than a build failure. Anything else is a real failure.
Expand All @@ -75,11 +76,16 @@ esac

mkdir -p "$JNI_LIBS"

if [ ! -f "$UNICORN_SRC/CMakeLists.txt" ]; then
echo "==> Fetching the Unicorn source through Brovan.Unicorn.targets"
"$DOTNET" msbuild "$PROJECT" -t:ExtractUnicornSource -nologo -v:minimal || true
[ -f "$UNICORN_SRC/CMakeLists.txt" ] || { echo "Unicorn source missing at $UNICORN_SRC" >&2; exit 1; }
fi
echo "==> Fetching and patching the Unicorn source through Brovan.Unicorn.targets"
"$DOTNET" msbuild "$PROJECT" -t:PatchUnicornSource -nologo -v:minimal
eval "$("$DOTNET" msbuild "$PROJECT" -t:PrintUnicornPaths -nologo -v:minimal \
| sed -n 's/^ *\(UNICORN_SRC=\|UNICORN_PATCH_KEY=\)/\1/p')"
[ -f "$UNICORN_SRC/CMakeLists.txt" ] || { echo "Unicorn source missing at '$UNICORN_SRC'" >&2; exit 1; }

# Keyed on the patch set like the host build, and kept separate from it: sharing one
# CMake cache between the Windows and WSL views of the same path breaks the configure.
UNICORN_BUILD="$REPO_ROOT/Brovan/.cache/unicorn/build-android-arm64-$UNICORN_PATCH_KEY"
UNICORN_ARTIFACT="$UNICORN_BUILD/libunicorn.so"

# Unicorn has to be cross-built before the publish: Brovan.Unicorn.targets copies whatever sits at
# UnicornArtifact into the publish output, and if that path is empty it configures a host-arch build there
Expand All @@ -102,6 +108,22 @@ if [ ! -f "$UNICORN_ARTIFACT" ]; then
fi
cp "$UNICORN_ARTIFACT" "$JNI_LIBS/libunicorn.so"

# The Vulkan shim is a guest PE, not a host library, so it ships as an asset and the
# app drops it into the guest's System32 on launch. It is generated from vk.xml
# alongside the managed marshaller, so it has to travel with the APK that built it.
echo "==> Building the BrovVulk Vulkan shim"
SHIM_DIR="$REPO_ROOT/Brovan.Graphics/brovvulk-icd"
GUEST_ASSETS="$GRADLE_PROJECT/brovan/src/main/assets/virtualfs"
if [ -f "$SHIM_DIR/obj/generated/brovvulk_gen.c" ]; then
sh "$SHIM_DIR/build.sh"
mkdir -p "$GUEST_ASSETS/System32" "$GUEST_ASSETS/SysWOW64"
cp -f "$SHIM_DIR/bin/vulkan-1.dll" "$GUEST_ASSETS/System32/vulkan-1.dll"
[ -f "$SHIM_DIR/bin/x86/vulkan-1.dll" ] && cp -f "$SHIM_DIR/bin/x86/vulkan-1.dll" "$GUEST_ASSETS/SysWOW64/vulkan-1.dll"
echo " packaged vulkan-1.dll into the APK assets"
else
echo "warning: BrovVulk generated sources missing; the APK will ship without the Vulkan shim." >&2
fi

# .NET's crypto shim aborts the process the moment any OpenSSL-backed primitive is touched
# ("No usable version of libssl was found"), and Android exposes no libssl to apps. Its probe list includes
# the unversioned names, which is what OpenSSL's android targets emit and what Android will package.
Expand Down
7 changes: 7 additions & 0 deletions Brovan/Android/java/dev/brovan/BrovanNative.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ public static void setVerbose(boolean enabled) {
nativeSetVerbose(enabled ? 1 : 0);
}

/** Reuses translated guest code between runs. Must be called before {@link #start}. */
public static void setJitCache(boolean enabled) {
nativeSetJitCache(enabled ? 1 : 0);
}

/** Feeds one line to the emulator's debugger prompt. Verbose mode only. */
public static void sendCommand(String command) {
nativeSendCommand(command);
Expand Down Expand Up @@ -237,6 +242,8 @@ private static native int nativeStart(String binaryPath, String guestCommandLine

private static native void nativeSetVerbose(int enabled);

private static native void nativeSetJitCache(int enabled);

private static native void nativeSendCommand(String command);

private static native int nativeIsRunning();
Expand Down
7 changes: 7 additions & 0 deletions Brovan/Android/jni/brovan_jni.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ extern void brovan_set_log_sink(void *sink);
extern void brovan_set_exit_sink(void *sink);
extern void brovan_set_install_progress_sink(void *sink);
extern void brovan_set_verbose(int enabled);
extern void brovan_set_jit_cache(int enabled);
extern void brovan_set_surface(void *nativeWindow, int width, int height, int densityDpi);
extern void brovan_clear_surface(void);
extern int brovan_start(const char *binaryPath, const char *guestCommandLine, const char *workingDirectory,
Expand Down Expand Up @@ -293,6 +294,12 @@ JNIEXPORT void JNICALL METHOD(SetVerbose)(JNIEnv *env, jclass clazz, jint enable
brovan_set_verbose(enabled);
}

JNIEXPORT void JNICALL METHOD(SetJitCache)(JNIEnv *env, jclass clazz, jint enabled) {
(void)env;
(void)clazz;
brovan_set_jit_cache(enabled);
}

JNIEXPORT void JNICALL METHOD(SendCommand)(JNIEnv *env, jclass clazz, jstring command) {
(void)clazz;

Expand Down
Loading
Loading