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
76 changes: 73 additions & 3 deletions Brovan/Android/AndroidGdiSurface.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ private sealed class WindowBuffer
public int Width;
public int Height;
public bool Dirty;
public long LastUsed;
}

private readonly object _sync = new();
private readonly Dictionary<ulong, WindowBuffer> _windows = new();

private ulong _lastDrawn;
private long _accessCounter;

public void Execute(in GdiPrimitive primitive)
{
Expand Down Expand Up @@ -106,15 +108,15 @@ private WindowBuffer Resolve(ulong hwnd)
{
if (!_windows.TryGetValue(hwnd, out WindowBuffer buffer))
{
// A guest that churns windows would otherwise grow this without bound; the emulator only
// presents one at a time, so dropping the oldest costs nothing visible.
if (_windows.Count >= MaximumWindows)
_windows.Clear();
Evict();

buffer = new WindowBuffer();
_windows[hwnd] = buffer;
}

buffer.LastUsed = ++_accessCounter;

int width = AndroidHost.Width;
int height = AndroidHost.Height;
if (width <= 0 || height <= 0)
Expand All @@ -131,6 +133,28 @@ private WindowBuffer Resolve(ulong hwnd)
return buffer;
}

private void Evict()
{
ulong selected = AndroidGuestWindows.Selected;
ulong victim = 0;
long oldest = long.MaxValue;

foreach (KeyValuePair<ulong, WindowBuffer> entry in _windows)
{
if (entry.Key == _lastDrawn || entry.Key == selected)
continue;

if (entry.Value.LastUsed < oldest)
{
oldest = entry.Value.LastUsed;
victim = entry.Key;
}
}

if (oldest != long.MaxValue)
_windows.Remove(victim);
}

private static void Draw(WindowBuffer target, in GdiPrimitive primitive)
{
int fill = ToPixel(primitive.Brush.ColorRef);
Expand Down Expand Up @@ -164,6 +188,52 @@ private static void Draw(WindowBuffer target, in GdiPrimitive primitive)
case GdiPrimitiveKind.Polyline:
DrawPolyline(target, primitive.Points, primitive.Kind == GdiPrimitiveKind.Polygon, primitive.HasPen ? stroke : fill, thickness);
break;

case GdiPrimitiveKind.Blit:
DrawBlit(target, primitive);
break;
}
}

private static void DrawBlit(WindowBuffer target, in GdiPrimitive primitive)
{
uint[] pixels = primitive.Pixels;
int sourceWidth = primitive.SourceWidth;
int sourceHeight = primitive.SourceHeight;
if (pixels == null || sourceWidth <= 0 || sourceHeight <= 0 || pixels.Length < sourceWidth * sourceHeight)
return;

int left = primitive.X1;
int top = primitive.Y1;
int right = primitive.X2;
int bottom = primitive.Y2;
Normalize(ref left, ref right);
Normalize(ref top, ref bottom);

int width = right - left;
int height = bottom - top;
if (width <= 0 || height <= 0)
return;

int clippedLeft = Math.Max(left, 0);
int clippedTop = Math.Max(top, 0);
int clippedRight = Math.Min(right, target.Width);
int clippedBottom = Math.Min(bottom, target.Height);

for (int y = clippedTop; y < clippedBottom; y++)
{
int sourceRow = (int)((long)(y - top) * sourceHeight / height);
int row = y * target.Width;

for (int x = clippedLeft; x < clippedRight; x++)
{
int sourceColumn = (int)((long)(x - left) * sourceWidth / width);
uint pixel = pixels[(sourceRow * sourceWidth) + sourceColumn];
target.Pixels[row + x] = unchecked((int)(0xFF000000u
| ((pixel & 0x00FF0000u) >> 16)
| (pixel & 0x0000FF00u)
| ((pixel & 0x000000FFu) << 16)));
}
}
}

Expand Down
25 changes: 25 additions & 0 deletions Brovan/Android/AndroidWinManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,31 @@ public void DeleteFont(IntPtr font)
{
}

// Android draws with one system face, so that is the whole enumeration.
public IReadOnlyList<FontFamilyData> EnumerateFontFamilies(string faceName, byte charSet)
{
const string SystemFace = "Roboto";

if (!string.IsNullOrEmpty(faceName) && !faceName.Equals(SystemFace, StringComparison.OrdinalIgnoreCase))
return Array.Empty<FontFamilyData>();

AndroidText.GetMetrics(out TextMetricsData metrics);

return new FontFamilyData[]
{
new FontFamilyData
{
FaceName = SystemFace,
FullName = SystemFace,
Style = "Regular",
CharSet = metrics.CharSet,
PitchAndFamily = metrics.PitchAndFamily,
Weight = metrics.Weight != 0 ? metrics.Weight : 400,
Metrics = metrics,
}
};
}

public void InvalidateSurface()
{
if (!_disposed)
Expand Down
10 changes: 5 additions & 5 deletions Brovan/Android/build-apk.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Builds the Brovan APK. Must run on a Linux host (WSL is fine): NativeAOT does not cross-compile from
# Windows to linux-bionic.
#
# Expects a .NET 9 SDK (the source generator needs Roslyn >= 4.10), a JDK 17, Gradle 8.7+, and an Android
# Expects a .NET 10 SDK, a JDK 17, Gradle 8.7+, and an Android
# SDK with NDK 26. Point the variables below at them if they are not already on PATH.
set -euo pipefail

Expand All @@ -16,8 +16,8 @@ TOOLS="${BROVAN_TOOLCHAIN:-$HOME/brovan-toolchain}"

DOTNET="${DOTNET:-}"
if [ -z "$DOTNET" ]; then
if [ -x "$HOME/.dotnet9/dotnet" ]; then
DOTNET="$HOME/.dotnet9/dotnet"
if [ -x "$HOME/.dotnet10/dotnet" ]; then
DOTNET="$HOME/.dotnet10/dotnet"
else
DOTNET="$(command -v dotnet || true)"
fi
Expand Down Expand Up @@ -64,12 +64,12 @@ UNICORN_PATCH_KEY=""
# as a skip rather than a build failure. Anything else is a real failure.
missing() { echo "$1" >&2; exit 3; }

[ -n "$DOTNET" ] && [ -x "$DOTNET" ] || missing "dotnet SDK not found; set DOTNET or install one at $HOME/.dotnet9"
[ -n "$DOTNET" ] && [ -x "$DOTNET" ] || missing "dotnet SDK not found; set DOTNET or install one at $HOME/.dotnet10"
DOTNET_MAJOR="$("$DOTNET" --version 2>/dev/null | cut -d. -f1)"
case "${DOTNET_MAJOR:-}" in
''|*[!0-9]*) missing "could not read the SDK version of $DOTNET" ;;
esac
[ "$DOTNET_MAJOR" -ge 9 ] || missing "the source generator needs Roslyn >= 4.10, so a .NET 9 SDK is required; $DOTNET is $DOTNET_MAJOR.x"
[ "$DOTNET_MAJOR" -ge 10 ] || missing "the projects target net10.0, so a .NET 10 SDK is required; $DOTNET is $DOTNET_MAJOR.x"
[ -n "$GRADLE" ] && [ -x "$GRADLE" ] || missing "gradle not found; set GRADLE (8.7 or newer, required by AGP 8.5)"
[ -n "$NDK" ] && [ -d "$NDK" ] || missing "Android NDK not found under $ANDROID_SDK_ROOT/ndk"
[ -n "${CMAKE:-}" ] && [ -x "$CMAKE" ] || missing "cmake not found"
Expand Down
Loading
Loading