Browse Source

WIP

feature/wmt
Nikita Tsukanov 2 years ago
parent
commit
3c2a1d9a6c
  1. 15
      src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs
  2. 32
      src/Browser/Avalonia.Browser/Interop/TimerHelper.cs
  3. 8
      src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs
  4. 5
      src/Browser/Avalonia.Browser/Rendering/RenderTargetBrowserSurface.cs
  5. 3
      src/Browser/Avalonia.Browser/WindowingPlatform.cs
  6. 1
      src/Browser/Avalonia.Browser/build/Avalonia.Browser.targets
  7. 12
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/jsExports.ts
  8. 1
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/htmlSurfaceBase.ts
  9. 24
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/webRenderTargetRegistry.ts
  10. 31
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts

15
src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs

@ -14,16 +14,17 @@ internal class BrowserDispatcherImpl : IDispatcherImpl
private bool _signaled;
private int? _timerId;
private readonly Action _timerCallback;
private readonly Action _signalCallback;
public BrowserDispatcherImpl()
{
_thread = Thread.CurrentThread;
_clock = Stopwatch.StartNew();
_timerCallback = () => Timer?.Invoke();
_signalCallback = () =>
TimerHelper.Interval += () =>
{
Timer?.Invoke();
};
TimerHelper.Timeout = () =>
{
_signaled = false;
Signaled?.Invoke();
@ -44,7 +45,7 @@ internal class BrowserDispatcherImpl : IDispatcherImpl
// NOTE: by HTML5 spec minimal timeout is 4ms, but Chrome seems to work well with 1ms as well.
var interval = 1;
TimerHelper.SetTimeout(_signalCallback, interval);
TimerHelper.SetTimeout(interval);
}
public void UpdateTimer(long? dueTimeInMs)
@ -58,7 +59,7 @@ internal class BrowserDispatcherImpl : IDispatcherImpl
if (dueTimeInMs.HasValue)
{
var interval = Math.Max(1, dueTimeInMs.Value - _clock.ElapsedMilliseconds);
_timerId = TimerHelper.SetInterval(_timerCallback, (int)interval);
_timerId = TimerHelper.SetInterval((int)interval);
}
}
}

32
src/Browser/Avalonia.Browser/Interop/TimerHelper.cs

@ -6,17 +6,37 @@ namespace Avalonia.Browser.Interop;
internal static partial class TimerHelper
{
[JSImport("TimerHelper.runAnimationFrames", AvaloniaModule.MainModuleName)]
public static partial void RunAnimationFrames(
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Boolean>>] Func<double, bool> renderFrameCallback);
public static partial void RunAnimationFrames();
[JSImport("globalThis.setTimeout")]
public static partial int SetTimeout([JSMarshalAs<JSType.Function>] Action callback, int intervalMs);
public static Action<double>? AnimationFrame;
[JSExport]
public static void JsExportOnAnimationFrame(double d)
{
AnimationFrame?.Invoke(d);
}
public static Action? Timeout;
[JSExport]
public static void JsExportOnTimeout()
{
Timeout?.Invoke();
}
[JSImport("TimerHelper.setTimeout", AvaloniaModule.MainModuleName)]
public static partial int SetTimeout(int intervalMs);
[JSImport("globalThis.clearTimeout")]
public static partial int ClearTimeout(int id);
[JSImport("globalThis.setInterval")]
public static partial int SetInterval([JSMarshalAs<JSType.Function>] Action callback, int intervalMs);
public static Action? Interval;
[JSExport]
public static void JsExportOnInterval()
{
Interval?.Invoke();
}
[JSImport("TimerHelper.setInterval", AvaloniaModule.MainModuleName)]
public static partial int SetInterval( int intervalMs);
[JSImport("globalThis.clearInterval")]
public static partial int ClearInterval(int id);

8
src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs

@ -38,18 +38,16 @@ internal class BrowserRenderTimer : IRenderTimer
if (!_started)
{
_started = true;
TimerHelper.RunAnimationFrames(RenderFrameCallback);
TimerHelper.AnimationFrame += RenderFrameCallback;
TimerHelper.RunAnimationFrames();
}
}
private bool RenderFrameCallback(double timestamp)
private void RenderFrameCallback(double timestamp)
{
if (_tick is { } tick)
{
tick.Invoke(TimeSpan.FromMilliseconds(timestamp));
return true;
}
return false;
}
}

5
src/Browser/Avalonia.Browser/Rendering/RenderTargetBrowserSurface.cs

@ -62,7 +62,7 @@ internal class RenderTargetBrowserSurface : BrowserSurface
public BrowserRenderTarget? Target =>
_target ??= BrowserRenderTarget.GetRenderTarget(_targetId, () => CanvasSize);
public bool IsReady => Target != null;
public bool IsReady => Target != null && CanvasSize.Size != default;
public bool UsesContexts => Target!.PlatformGraphicsContext != null;
public bool UsesSharedContext => UsesContexts;
public (PixelSize Size, double Scaling) CanvasSize { get; set; }
@ -95,8 +95,7 @@ internal class RenderTargetBrowserSurface : BrowserSurface
public static RenderTargetBrowserSurface Create(JSObject container, IReadOnlyList<BrowserRenderingMode> modes)
{
// TODO: Get thread id from JSWebWorker
var js = CanvasHelper.CreateRenderTargetSurface(container, modes.Select(m => (int)m).ToArray(), 0);
var js = CanvasHelper.CreateRenderTargetSurface(container, modes.Select(m => (int)m).ToArray(), RenderWorker.WorkerThreadId);
return new RenderTargetBrowserSurface(js);
}
}

3
src/Browser/Avalonia.Browser/WindowingPlatform.cs

@ -4,6 +4,7 @@ using System.Threading;
using Avalonia.Browser.Interop;
using Avalonia.Browser.Skia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Platform;
@ -64,12 +65,12 @@ internal class BrowserWindowingPlatform : IWindowingPlatform
.Bind<ICursorFactory>().ToSingleton<CssCursorFactory>()
.Bind<IKeyboardDevice>().ToConstant(s_keyboard)
.Bind<IPlatformSettings>().ToSingleton<BrowserPlatformSettings>()
.Bind<IDispatcherImpl>().ToSingleton<BrowserDispatcherImpl>()
.Bind<IWindowingPlatform>().ToConstant(instance)
.Bind<IPlatformGraphics>().ToConstant(new BrowserSkiaGraphics())
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<IActivatableLifetime>().ToSingleton<BrowserActivatableLifetime>();
AvaloniaLocator.CurrentMutable.Bind<IDispatcherImpl>().ToSingleton<BrowserDispatcherImpl>();
if (AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() is { } options
&& options.RegisterAvaloniaServiceWorker)

1
src/Browser/Avalonia.Browser/build/Avalonia.Browser.targets

@ -6,6 +6,7 @@
<ItemGroup>
<!-- So we can access Emscripten APIs -->
<EmccExportedRuntimeMethod Include="GL" />
<EmccExportedRuntimeMethod Include="PThread" />
</ItemGroup>
<!-- Fallback for applications without StaticWebAssetsEnabled (legacy WASM SDK) -->

12
src/Browser/Avalonia.Browser/webapp/modules/avalonia/jsExports.ts

@ -0,0 +1,12 @@
export class JsExports {
public static resolvedExports?: any;
public static exportsPromise: Promise<any>;
}
async function resolveExports (): Promise<any> {
const runtimeApi = await globalThis.getDotnetRuntime(0);
if (runtimeApi == null) { return; }
JsExports.resolvedExports = await runtimeApi.getAssemblyExports("Avalonia.Browser.dll");
return JsExports.resolvedExports;
}
JsExports.exportsPromise = resolveExports();

1
src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/htmlSurfaceBase.ts

@ -26,6 +26,7 @@ export abstract class HtmlCanvasSurfaceBase extends CanvasSurface {
public onSizeChanged(sizeChangedCallback: (width: number, height: number, dpr: number) => void) {
if (this.sizeChangedCallback) { throw new Error("For simplicity, we don't support multiple size changed callbacks per surface, not needed yet."); }
this.sizeChangedCallback = sizeChangedCallback;
if (this.sizeParams) { this.sizeChangedCallback(this.sizeParams[0], this.sizeParams[1], this.sizeParams[2]); }
}
public ensureSize() {

24
src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/webRenderTargetRegistry.ts

@ -21,9 +21,13 @@ export class WebRenderTargetRegistry {
} else {
const self = globalThis as any;
const module = self.Module ?? self.getDotnetRuntime(0)?.Module;
const pthread = module?.PThread;
if (pthread == null) { throw new Error("Unable to access emscripten PThread api"); }
const worker = pthread.pthreads[pthreadId]?.worker as Worker;
const pthreads = module?.PThread;
if (pthreads == null) { throw new Error("Unable to access emscripten PThread api"); }
const pthread = pthreads.pthreads[pthreadId];
if (pthread == null) { throw new Error(`Unable get pthread with id ${pthreadId}`); }
let worker: Worker | undefined;
if (pthread.postMessage != null) { worker = pthread as Worker; } else { worker = pthread.worker; }
if (worker == null) { throw new Error(`Unable get Worker for pthread ${pthreadId}`); }
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({
@ -31,7 +35,7 @@ export class WebRenderTargetRegistry {
canvas: offscreen,
modes: preferredModes,
id
});
}, [offscreen]);
WebRenderTargetRegistry.registry[id] = {
canvas,
worker
@ -41,18 +45,18 @@ export class WebRenderTargetRegistry {
}
static initializeWorker() {
self.addEventListener("onmessage", ev => {
const msg = ev as MessageEvent;
const oldHandler = self.onmessage;
self.onmessage = ev => {
const msg = ev;
if (msg.data.avaloniaCmd === "registerCanvas") {
WebRenderTargetRegistry.targets[msg.data.id] = WebRenderTargetRegistry.createRenderTarget(msg.data.canvas, msg.data.modes);
}
if (msg.data.avaloniaCmd === "unregisterCanvas") {
} else if (msg.data.avaloniaCmd === "unregisterCanvas") {
/* eslint-disable */
// Our keys are _always_ numbers and are safe to delete
delete WebRenderTargetRegistry.targets[msg.data.id];
/* eslint-enable */
}
});
} else if (oldHandler != null) { oldHandler.call(self, ev); }
};
}
static getRenderTarget(id: number): WebRenderTarget | undefined {

31
src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts

@ -1,12 +1,33 @@
import { JsExports } from "./jsExports";
export class TimerHelper {
public static runAnimationFrames(renderFrameCallback: (timestamp: number) => boolean): void {
public static runAnimationFrames(): void {
function render(time: number) {
const next = renderFrameCallback(time);
if (next) {
self.requestAnimationFrame(render);
if (JsExports.resolvedExports != null) {
JsExports.resolvedExports.Avalonia.Browser.Interop.TimerHelper.JsExportOnAnimationFrame(time);
}
self.requestAnimationFrame(render);
}
self.requestAnimationFrame(render);
}
static onTimeout() {
if (JsExports.resolvedExports != null) {
JsExports.resolvedExports.Avalonia.Browser.Interop.TimerHelper.JsExportOnTimeout();
} else { console.error("TimerHelper.onTimeout call while uninitialized"); }
}
static onInterval() {
if (JsExports.resolvedExports != null) {
JsExports.resolvedExports.Avalonia.Browser.Interop.TimerHelper.JsExportOnInterval();
} else { console.error("TimerHelper.onInterval call while uninitialized"); }
}
public static setTimeout(interval: number): number {
return setTimeout(TimerHelper.onTimeout, interval);
}
public static setInterval(interval: number): number {
return setInterval(TimerHelper.onInterval, interval);
}
}

Loading…
Cancel
Save