react-native-webgpu 0.10.2 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,31 @@
1
1
  #pragma once
2
2
 
3
3
  #include <algorithm>
4
+ #include <atomic>
5
+ #include <cerrno>
6
+ #include <chrono>
7
+ #include <condition_variable>
8
+ #include <cstdint>
9
+ #include <deque>
4
10
  #include <functional>
5
11
  #include <memory>
6
12
  #include <mutex>
7
13
  #include <shared_mutex>
8
14
  #include <stdexcept>
15
+ #include <thread>
9
16
  #include <unordered_map>
10
17
  #include <utility>
11
18
  #include <vector>
12
19
 
13
20
  #include "webgpu/webgpu_cpp.h"
14
21
 
22
+ #if defined(__ANDROID__)
23
+ #include <android/hardware_buffer.h>
24
+ #include <android/log.h>
25
+ #include <poll.h>
26
+ #include <unistd.h>
27
+ #endif
28
+
15
29
  #ifdef __APPLE__
16
30
  namespace dawn::native::metal {
17
31
  void WaitForCommandsToBeScheduled(WGPUDevice device);
@@ -44,6 +58,64 @@ struct Size {
44
58
  // CAMetalLayer on Apple platforms). May run on any thread.
45
59
  using NativeSurfaceReleaser = std::function<void(void *)>;
46
60
 
61
+ #if defined(__ANDROID__)
62
+ // --- AHB-pool presentation mode (WebGPUHardwareBufferView)
63
+ // -----------------------------
64
+ //
65
+ // A third presentation backend (besides the on-screen wgpu::Surface swapchain
66
+ // and the offscreen texture). WebGPU renders into a small pool of native
67
+ // AHardwareBuffers imported as Dawn SharedTextureMemory; the view draws each
68
+ // finished buffer inline via Bitmap.wrapHardwareBuffer so it behaves like a
69
+ // normal RN view. The pool is sized from the JS canvas drawing buffer
70
+ // (_canvas->getWidth()/Height()) and reallocated when that changes, exactly
71
+ // like the swapchain, so the canvas texture always matches the app's other
72
+ // attachments (e.g. its depth texture).
73
+
74
+ #define RNWGPU_LOG_TAG "WebGPUHardwareBufferView"
75
+ // Failure / anomaly reporting only (never per-frame).
76
+ #define RNWGPU_POOL_WARN(...) \
77
+ __android_log_print(ANDROID_LOG_WARN, RNWGPU_LOG_TAG, __VA_ARGS__)
78
+
79
+ enum class SlotState : uint8_t {
80
+ Free, // available for poolGetCurrentTexture
81
+ Rendering, // BeginAccess done, JS rendering into it
82
+ Presented, // EndAccess done, fence(s) queued for the waiter
83
+ Ready, // fence signaled, awaiting UI pickup
84
+ Displayed, // handed to the consumer, held until released (held-ring)
85
+ };
86
+
87
+ struct PoolSlot {
88
+ AHardwareBuffer *ahb = nullptr; // owned by the pool (allocated natively)
89
+ wgpu::SharedTextureMemory memory = nullptr;
90
+ wgpu::Texture texture = nullptr;
91
+ SlotState state = SlotState::Free;
92
+ };
93
+
94
+ // One generation of the pool (one buffer size). Reference-counted (shared_ptr)
95
+ // so an in-flight render / present / ready slot keeps the whole generation
96
+ // alive across a resize. The destructor frees every AHB and tears down the Dawn
97
+ // imports. The Java side keeps its own ref (via wrapHardwareBuffer) for
98
+ // anything it is still drawing, so a generation can be freed here without
99
+ // disturbing a frame still on screen.
100
+ struct AHBPool {
101
+ std::vector<PoolSlot> slots;
102
+ uint32_t generation = 0;
103
+ int width = 0;
104
+ int height = 0;
105
+
106
+ ~AHBPool() {
107
+ for (auto &slot : slots) {
108
+ slot.texture = nullptr;
109
+ slot.memory = nullptr;
110
+ if (slot.ahb != nullptr) {
111
+ AHardwareBuffer_release(slot.ahb);
112
+ slot.ahb = nullptr;
113
+ }
114
+ }
115
+ }
116
+ };
117
+ #endif
118
+
47
119
  // Bridges the asynchronous native surface lifecycle (surfaces appear and
48
120
  // disappear on the platform UI thread) with the synchronous WebGPU canvas API
49
121
  // (the JS render loop must always be able to acquire a texture).
@@ -79,6 +151,16 @@ public:
79
151
  : _gpu(std::move(gpu)), _width(width), _height(height) {}
80
152
 
81
153
  ~SurfaceInfo() {
154
+ #if defined(__ANDROID__)
155
+ // Stop the fence waiter before any pool generation is freed.
156
+ stopWaiter();
157
+ {
158
+ std::lock_guard<std::mutex> poolLock(_poolMutex);
159
+ _presentQueue.clear();
160
+ poolResetLocked();
161
+ _poolDevice = nullptr;
162
+ }
163
+ #endif
82
164
  // Drop the Dawn objects before releasing the native surfaces they borrow.
83
165
  _surface = nullptr;
84
166
  #if defined(__ANDROID__)
@@ -250,6 +332,20 @@ public:
250
332
  _config.presentMode = wgpu::PresentMode::Fifo;
251
333
  _texture = nullptr;
252
334
  _frameEpoch++;
335
+ #if defined(__ANDROID__)
336
+ {
337
+ std::lock_guard<std::mutex> poolLock(_poolMutex);
338
+ if (_poolDevice.Get() != _config.device.Get()) {
339
+ // Buffers imported for another device cannot be reused.
340
+ poolResetLocked();
341
+ }
342
+ _poolDevice = _config.device;
343
+ _poolFormat = _config.format;
344
+ _poolUsage = _config.usage;
345
+ // A new device may support AHB sharing even if the previous one did not.
346
+ _poolUnsupported = false;
347
+ }
348
+ #endif
253
349
  _configureLocked();
254
350
  }
255
351
 
@@ -276,6 +372,13 @@ public:
276
372
  _viewFormats.clear();
277
373
  _acquiredFromSurface = false;
278
374
  _frameEpoch++;
375
+ #if defined(__ANDROID__)
376
+ {
377
+ std::lock_guard<std::mutex> poolLock(_poolMutex);
378
+ poolResetLocked();
379
+ _poolDevice = nullptr;
380
+ }
381
+ #endif
279
382
  }
280
383
 
281
384
  bool isConfigured() {
@@ -303,6 +406,16 @@ public:
303
406
  void releaseSurfaceForDevice(const wgpu::Device &device) {
304
407
  #if defined(__ANDROID__)
305
408
  std::unique_lock<std::shared_mutex> lock(_mutex);
409
+ {
410
+ // The pool's SharedTextureMemory imports belong to the device too: drop
411
+ // them while it is alive. The next getCurrentTexture() after a configure
412
+ // with a live device reallocates the pool.
413
+ std::lock_guard<std::mutex> poolLock(_poolMutex);
414
+ if (_poolDevice && _poolDevice.Get() == device.Get()) {
415
+ poolResetLocked();
416
+ _poolDevice = nullptr;
417
+ }
418
+ }
306
419
  if (!_surface || !_surfaceDevice || _surfaceDevice.Get() != device.Get()) {
307
420
  return;
308
421
  }
@@ -321,6 +434,13 @@ public:
321
434
  // otherwise (see RNWebGPU::destroyContext).
322
435
  bool hasNativeSurface() {
323
436
  std::shared_lock<std::shared_mutex> lock(_mutex);
437
+ #if defined(__ANDROID__)
438
+ // A WebGPUHardwareBufferView in pool mode owns presentation the same way a
439
+ // surface-backed view does, and retires the entry on its own teardown.
440
+ if (_poolMode.load()) {
441
+ return true;
442
+ }
443
+ #endif
324
444
  return _nativeSurface != nullptr || _hasPendingAttach;
325
445
  }
326
446
 
@@ -331,6 +451,34 @@ public:
331
451
  // Start-of-frame boundary; a new acquire supersedes any previous frame
332
452
  // that never presented.
333
453
  applyPendingAttach(/* supersedeInFlightFrame = */ true);
454
+ #if defined(__ANDROID__)
455
+ if (_poolMode.load()) {
456
+ // The pool tracks the drawing buffer (_config.width/height, kept in sync
457
+ // with canvas.width/height by reconfigure()), like the swapchain, so the
458
+ // canvas texture always matches the app's other attachments.
459
+ int poolW = 0;
460
+ int poolH = 0;
461
+ {
462
+ std::shared_lock<std::shared_mutex> lock(_mutex);
463
+ if (_config.device != nullptr) {
464
+ poolW = static_cast<int>(_config.width);
465
+ poolH = static_cast<int>(_config.height);
466
+ }
467
+ }
468
+ poolResize(poolW, poolH);
469
+ if (auto texture = poolGetCurrentTexture()) {
470
+ std::unique_lock<std::shared_mutex> lock(_mutex);
471
+ _frameInFlight = true;
472
+ _acquiredFromSurface = false;
473
+ _frameEpoch++;
474
+ return texture;
475
+ }
476
+ // No slot available (pool not allocated yet, unsupported device, view
477
+ // detached mid-frame, or a free-slot timeout): fall through to the
478
+ // offscreen fallback so the render loop survives; this frame is simply
479
+ // not shown.
480
+ }
481
+ #endif
334
482
  std::unique_lock<std::shared_mutex> lock(_mutex);
335
483
  if (_config.device == nullptr) {
336
484
  throw std::runtime_error(
@@ -363,6 +511,13 @@ public:
363
511
  // acquire failure) are dropped. This is also the end-of-frame boundary: it
364
512
  // adopts a surface that attached while the frame was in flight.
365
513
  void presentFrame() {
514
+ #if defined(__ANDROID__)
515
+ if (_poolMode.load()) {
516
+ // Ends access on the pool slot and hands it to the fence waiter; the
517
+ // view picks it up once its render-complete fence signals.
518
+ poolPresent();
519
+ }
520
+ #endif
366
521
  #ifdef __APPLE__
367
522
  // Ensure command buffers are scheduled before presenting. Read the device
368
523
  // under a shared lock, then wait without holding it (the wait can block).
@@ -404,12 +559,161 @@ public:
404
559
  return _config;
405
560
  }
406
561
 
562
+ #if defined(__ANDROID__)
563
+ // --- AHB-pool API (called from the JNI layer, see cpp-adapter.cpp) ---------
564
+
565
+ // Turn on pool mode for this context. dpW/dpH is the canvas-client (dp) size
566
+ // reported to JS via getSize(); the actual buffers are sized in pool px from
567
+ // the canvas drawing buffer in poolResize().
568
+ void enablePool(int dpW, int dpH) {
569
+ {
570
+ std::unique_lock<std::shared_mutex> lock(_mutex);
571
+ _width = dpW;
572
+ _height = dpH;
573
+ }
574
+ std::lock_guard<std::mutex> poolLock(_poolMutex);
575
+ _poolMode.store(true);
576
+ _shutdown = false;
577
+ }
578
+
579
+ // Set the canvas-client (dp) size without changing pool mode. Keeps getSize()
580
+ // (and thus the JS canvas.clientWidth/Height) in sync on resize.
581
+ void setPoolClientSize(int dpW, int dpH) {
582
+ std::unique_lock<std::shared_mutex> lock(_mutex);
583
+ _width = dpW;
584
+ _height = dpH;
585
+ }
586
+
587
+ // (Re)allocate the pool to match the canvas drawing buffer (px). Called from
588
+ // getCurrentTexture() on the rendering thread before acquiring a slot, so the
589
+ // canvas texture size always tracks the app's other attachments.
590
+ void poolResize(int pxW, int pxH) {
591
+ std::lock_guard<std::mutex> poolLock(_poolMutex);
592
+ if (!_poolMode.load() || _poolDevice == nullptr || pxW <= 0 || pxH <= 0) {
593
+ return;
594
+ }
595
+ if (_pool != nullptr && _pool->width == pxW && _pool->height == pxH) {
596
+ return;
597
+ }
598
+ if (_poolUnsupported) {
599
+ return;
600
+ }
601
+ if (!_poolDevice.HasFeature(
602
+ wgpu::FeatureName::SharedTextureMemoryAHardwareBuffer)) {
603
+ _poolUnsupported = true;
604
+ RNWGPU_POOL_WARN("device lacks SharedTextureMemoryAHardwareBuffer; the "
605
+ "transparent canvas cannot present and stays blank");
606
+ return;
607
+ }
608
+ auto pool = allocatePoolLocked(pxW, pxH);
609
+ if (pool == nullptr) {
610
+ return;
611
+ }
612
+ _pool = pool;
613
+ _pools[pool->generation] = pool;
614
+ // Keep current + previous generation (in-flight frames of the previous size
615
+ // may still be on screen during the cross-fade); drop anything older.
616
+ for (auto it = _pools.begin(); it != _pools.end();) {
617
+ if (it->first + 1 < pool->generation) {
618
+ it = _pools.erase(it);
619
+ } else {
620
+ ++it;
621
+ }
622
+ }
623
+ _freeCv.notify_all();
624
+ }
625
+
626
+ // Latest signaled frame awaiting display, encoded as (generation << 32 |
627
+ // slot). Returns -1 when there is nothing new. Marks the slot Displayed so
628
+ // the producer will not re-render into it until releaseSlot() is called.
629
+ // Called on the UI thread after the frame-ready wake-up.
630
+ int64_t poolPollReady() {
631
+ std::lock_guard<std::mutex> poolLock(_poolMutex);
632
+ if (_readySlot < 0 || _readyPool == nullptr) {
633
+ return -1;
634
+ }
635
+ int idx = _readySlot;
636
+ uint32_t gen = _readyPool->generation;
637
+ _readyPool->slots[idx].state = SlotState::Displayed;
638
+ _readyPool = nullptr;
639
+ _readySlot = -1;
640
+ return (static_cast<int64_t>(gen) << 32) | static_cast<uint32_t>(idx);
641
+ }
642
+
643
+ // The AHardwareBuffer backing a (generation, slot), for the consumer to wrap
644
+ // in a Bitmap. Returns nullptr for a retired generation. Called on the UI
645
+ // thread; the JNI layer converts it to a HardwareBuffer jobject.
646
+ void *poolBufferForDisplay(uint32_t gen, int slot) {
647
+ std::lock_guard<std::mutex> poolLock(_poolMutex);
648
+ auto it = _pools.find(gen);
649
+ if (it == _pools.end() || slot < 0 ||
650
+ slot >= static_cast<int>(it->second->slots.size())) {
651
+ return nullptr;
652
+ }
653
+ return it->second->slots[slot].ahb;
654
+ }
655
+
656
+ // The consumer is done displaying (and holding) a slot: return it to the free
657
+ // list so the producer may render into it again. No-op for a retired
658
+ // generation (those buffers are never reused). Called on the UI thread.
659
+ void poolReleaseSlot(uint32_t gen, int idx) {
660
+ std::lock_guard<std::mutex> poolLock(_poolMutex);
661
+ if (_pool != nullptr && _pool->generation == gen && idx >= 0 &&
662
+ idx < static_cast<int>(_pool->slots.size())) {
663
+ _pool->slots[idx].state = SlotState::Free;
664
+ _freeCv.notify_one();
665
+ }
666
+ }
667
+
668
+ // Invoked from the waiter thread (never under _poolMutex) each time a new
669
+ // frame becomes ready, so the consumer can wake up instead of polling every
670
+ // vsync. Pass nullptr to unregister.
671
+ void setPoolFrameReadyCallback(std::function<void()> cb) {
672
+ std::lock_guard<std::mutex> poolLock(_poolMutex);
673
+ _poolFrameReady = std::move(cb);
674
+ }
675
+ #endif
676
+
407
677
  private:
408
678
  void detach(bool createFallbackTexture) {
409
679
  void *releasedSurfaces[2] = {nullptr, nullptr};
410
680
  NativeSurfaceReleaser releasers[2];
681
+ #if defined(__ANDROID__)
682
+ // Destroyed outside the lock: it releases a JNI reference.
683
+ std::function<void()> releasedFrameReady;
684
+ #endif
411
685
  {
412
686
  std::unique_lock<std::shared_mutex> lock(_mutex);
687
+ #if defined(__ANDROID__)
688
+ // Leaving pool mode (the WebGPUHardwareBufferView detached): drop the
689
+ // pool. A configured context keeps rendering into the offscreen
690
+ // fallback, exactly like the surface path.
691
+ if (_poolMode.load()) {
692
+ _poolMode.store(false);
693
+ {
694
+ std::lock_guard<std::mutex> poolLock(_poolMutex);
695
+ // A frame between getCurrentTexture and present still has an open
696
+ // BeginAccess; end it and queue it so the waiter drains its fences
697
+ // before the generation is freed (presentFrame() no-ops for it
698
+ // because pool mode is already off).
699
+ if (_renderPool != nullptr && _currentRenderSlot >= 0 &&
700
+ _renderPool->slots[_currentRenderSlot].state ==
701
+ SlotState::Rendering) {
702
+ poolQueuePresentLocked();
703
+ }
704
+ // _presentQueue is intentionally left alone: the waiter still has
705
+ // to wait on the queued fences before the generations they pin can
706
+ // be released; it discards them on publish (no live pool).
707
+ poolResetLocked();
708
+ releasedFrameReady = std::move(_poolFrameReady);
709
+ _poolFrameReady = nullptr;
710
+ }
711
+ if (createFallbackTexture && _config.device != nullptr && !_texture) {
712
+ _texture = createOffscreenTextureLocked();
713
+ }
714
+ _frameEpoch++;
715
+ }
716
+ #endif
413
717
  // The platform is tearing surfaces down; a not-yet-adopted attach is
414
718
  // stale, cancel it.
415
719
  if (_hasPendingAttach) {
@@ -583,6 +887,281 @@ private:
583
887
  }
584
888
  #endif
585
889
 
890
+ #if defined(__ANDROID__)
891
+ // Drop every pool generation and the ready / in-flight slot bookkeeping.
892
+ // Caller holds _poolMutex. Generations pinned by a queued present survive
893
+ // through their PendingPresent until the waiter is done with them.
894
+ void poolResetLocked() {
895
+ _pool = nullptr;
896
+ _pools.clear();
897
+ _readyPool = nullptr;
898
+ _readySlot = -1;
899
+ _renderPool = nullptr;
900
+ _currentRenderSlot = -1;
901
+ _freeCv.notify_all();
902
+ }
903
+
904
+ // Allocate + import one generation of the pool. Caller holds _poolMutex.
905
+ std::shared_ptr<AHBPool> allocatePoolLocked(int w, int h) {
906
+ auto pool = std::make_shared<AHBPool>();
907
+ pool->generation = ++_genCounter;
908
+ pool->width = w;
909
+ pool->height = h;
910
+ pool->slots.resize(kPoolSize);
911
+ for (auto &slot : pool->slots) {
912
+ AHardwareBuffer_Desc desc = {};
913
+ desc.width = static_cast<uint32_t>(w);
914
+ desc.height = static_cast<uint32_t>(h);
915
+ desc.layers = 1;
916
+ desc.format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
917
+ desc.usage = AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE |
918
+ AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT;
919
+ int err = AHardwareBuffer_allocate(&desc, &slot.ahb);
920
+ if (err != 0 || slot.ahb == nullptr) {
921
+ RNWGPU_POOL_WARN("AHardwareBuffer_allocate failed (%d) %dx%d", err, w,
922
+ h);
923
+ return nullptr;
924
+ }
925
+ wgpu::SharedTextureMemoryDescriptor memDesc{};
926
+ wgpu::SharedTextureMemoryAHardwareBufferDescriptor ahbDesc{};
927
+ ahbDesc.handle = slot.ahb;
928
+ memDesc.nextInChain = &ahbDesc;
929
+ slot.memory = _poolDevice.ImportSharedTextureMemory(&memDesc);
930
+ // A failed import yields a non-null Dawn error object, so probing the
931
+ // properties is the actual validity check.
932
+ wgpu::SharedTextureMemoryProperties props{};
933
+ if (slot.memory == nullptr ||
934
+ slot.memory.GetProperties(&props) != wgpu::Status::Success) {
935
+ RNWGPU_POOL_WARN("ImportSharedTextureMemory failed (%dx%d)", w, h);
936
+ return nullptr;
937
+ }
938
+ // The AHB is rgba8unorm; the pool cannot honor another configured format.
939
+ // The canvas is configured rgba8unorm on Android (the preferred format),
940
+ // so this only fires for apps hardcoding e.g. bgra8unorm.
941
+ if (_poolFormat != wgpu::TextureFormat::Undefined &&
942
+ _poolFormat != wgpu::TextureFormat::RGBA8Unorm) {
943
+ RNWGPU_POOL_WARN("configured canvas format %d is not supported by the "
944
+ "transparent canvas; using rgba8unorm (use "
945
+ "navigator.gpu.getPreferredCanvasFormat())",
946
+ static_cast<int>(_poolFormat));
947
+ }
948
+ // Honor the configured usage flags as far as the imported memory allows.
949
+ wgpu::TextureUsage wanted =
950
+ _poolUsage | wgpu::TextureUsage::RenderAttachment;
951
+ wgpu::TextureUsage usage = wanted & props.usage;
952
+ if (usage != wanted) {
953
+ RNWGPU_POOL_WARN("configured canvas usage 0x%llx narrowed to 0x%llx "
954
+ "(unsupported by the transparent canvas buffers)",
955
+ static_cast<unsigned long long>(wanted),
956
+ static_cast<unsigned long long>(usage));
957
+ }
958
+ wgpu::TextureDescriptor texDesc{};
959
+ texDesc.format = props.format;
960
+ texDesc.usage = usage;
961
+ texDesc.size.width = static_cast<uint32_t>(w);
962
+ texDesc.size.height = static_cast<uint32_t>(h);
963
+ slot.texture = slot.memory.CreateTexture(&texDesc);
964
+ slot.state = SlotState::Free;
965
+ }
966
+ return pool;
967
+ }
968
+
969
+ wgpu::Texture poolGetCurrentTexture() {
970
+ std::unique_lock<std::mutex> lock(_poolMutex);
971
+ // Per the WebGPU spec, getCurrentTexture returns the same texture until the
972
+ // frame is presented; a repeat call must not consume another slot.
973
+ if (_renderPool != nullptr && _currentRenderSlot >= 0) {
974
+ return _renderPool->slots[_currentRenderSlot].texture;
975
+ }
976
+ if (_pool == nullptr) {
977
+ return nullptr; // allocation failed / unsupported (already reported)
978
+ }
979
+ auto pool = _pool;
980
+ int idx = -1;
981
+ // Bounded wait: if no slot frees up (e.g. a wedged fence), skip the frame
982
+ // instead of parking the JS/worklet thread forever.
983
+ bool ready = _freeCv.wait_for(lock, std::chrono::seconds(1), [&]() {
984
+ if (_shutdown || pool != _pool) {
985
+ return true; // pool replaced or shutting down: bail out
986
+ }
987
+ for (size_t i = 0; i < pool->slots.size(); i++) {
988
+ if (pool->slots[i].state == SlotState::Free) {
989
+ idx = static_cast<int>(i);
990
+ return true;
991
+ }
992
+ }
993
+ return false;
994
+ });
995
+ if (!ready) {
996
+ RNWGPU_POOL_WARN("poolGetCurrentTexture: timed out waiting for a free "
997
+ "slot; skipping frame");
998
+ return nullptr;
999
+ }
1000
+ if (idx < 0 || _shutdown || pool != _pool) {
1001
+ return nullptr;
1002
+ }
1003
+ auto &slot = pool->slots[idx];
1004
+ slot.state = SlotState::Rendering;
1005
+ _renderPool = pool;
1006
+ _currentRenderSlot = idx;
1007
+ poolBeginAccess(slot);
1008
+ return slot.texture;
1009
+ }
1010
+
1011
+ void poolPresent() {
1012
+ std::lock_guard<std::mutex> lock(_poolMutex);
1013
+ if (_currentRenderSlot < 0 || _renderPool == nullptr) {
1014
+ return;
1015
+ }
1016
+ poolQueuePresentLocked();
1017
+ }
1018
+
1019
+ // Ends access on the in-flight render slot and hands it (with its fences) to
1020
+ // the waiter. Caller holds _poolMutex and guarantees _renderPool /
1021
+ // _currentRenderSlot are valid.
1022
+ void poolQueuePresentLocked() {
1023
+ auto pool = _renderPool;
1024
+ int idx = _currentRenderSlot;
1025
+ auto &slot = pool->slots[idx];
1026
+
1027
+ wgpu::SharedTextureMemoryEndAccessState state{};
1028
+ wgpu::SharedTextureMemoryVkImageLayoutEndState vkLayout{};
1029
+ state.nextInChain = &vkLayout;
1030
+ slot.memory.EndAccess(slot.texture, &state);
1031
+
1032
+ std::vector<wgpu::SharedFence> fences;
1033
+ fences.reserve(state.fenceCount);
1034
+ for (size_t i = 0; i < state.fenceCount; i++) {
1035
+ fences.push_back(state.fences[i]);
1036
+ }
1037
+
1038
+ slot.state = SlotState::Presented;
1039
+ _presentQueue.push_back({pool, idx, std::move(fences)});
1040
+ _renderPool = nullptr;
1041
+ _currentRenderSlot = -1;
1042
+ startWaiterLocked();
1043
+ _presentCv.notify_one();
1044
+ }
1045
+
1046
+ void poolBeginAccess(PoolSlot &slot) {
1047
+ wgpu::SharedTextureMemoryBeginAccessDescriptor desc{};
1048
+ desc.initialized = false; // canvas contents are fully redrawn each frame
1049
+ desc.concurrentRead = false;
1050
+ desc.fenceCount = 0;
1051
+ desc.fences = nullptr;
1052
+ desc.signaledValues = nullptr;
1053
+ wgpu::SharedTextureMemoryVkImageLayoutBeginState vkLayout{};
1054
+ vkLayout.oldLayout = 0;
1055
+ vkLayout.newLayout = 0;
1056
+ desc.nextInChain = &vkLayout;
1057
+ slot.memory.BeginAccess(slot.texture, &desc);
1058
+ }
1059
+
1060
+ void startWaiterLocked() {
1061
+ if (_waiterRunning) {
1062
+ return;
1063
+ }
1064
+ _waiterRunning = true;
1065
+ _waiter = std::thread([this]() { waiterLoop(); });
1066
+ }
1067
+
1068
+ void stopWaiter() {
1069
+ {
1070
+ std::lock_guard<std::mutex> lock(_poolMutex);
1071
+ _shutdown = true;
1072
+ _presentCv.notify_all();
1073
+ _freeCv.notify_all();
1074
+ }
1075
+ if (_waiter.joinable()) {
1076
+ _waiter.join();
1077
+ }
1078
+ _waiterRunning = false;
1079
+ }
1080
+
1081
+ // Blocks on each presented frame's render-complete fence (off the UI and JS
1082
+ // threads), then publishes it as the latest ready slot. This is the rigorous
1083
+ // acquire-side wait: HWUI only samples a buffer after the UI thread picks it
1084
+ // up via poolPollReady, which happens strictly after this wait returns.
1085
+ void waiterLoop() {
1086
+ while (true) {
1087
+ PendingPresent pending;
1088
+ {
1089
+ std::unique_lock<std::mutex> lock(_poolMutex);
1090
+ _presentCv.wait(lock,
1091
+ [&]() { return _shutdown || !_presentQueue.empty(); });
1092
+ if (_shutdown && _presentQueue.empty()) {
1093
+ return;
1094
+ }
1095
+ pending = std::move(_presentQueue.front());
1096
+ _presentQueue.pop_front();
1097
+ }
1098
+
1099
+ // Wait outside the lock. Each fd is owned by its SharedFence and closed
1100
+ // when `pending.fences` is destroyed at the end of this iteration, so we
1101
+ // never dup / double-close (avoids the fdsan abort, see
1102
+ // GPUSharedFence.cpp).
1103
+ for (auto &fence : pending.fences) {
1104
+ wgpu::SharedFenceExportInfo info{};
1105
+ wgpu::SharedFenceSyncFDExportInfo fdInfo{};
1106
+ info.nextInChain = &fdInfo;
1107
+ fence.ExportInfo(&info);
1108
+ if (info.type != wgpu::SharedFenceType::SyncFD) {
1109
+ // fdInfo was not populated (its default handle 0 is NOT a fence fd);
1110
+ // we have no way to wait on this fence type.
1111
+ RNWGPU_POOL_WARN("waiter: unexpected shared fence type %d, cannot "
1112
+ "wait for render completion",
1113
+ static_cast<int>(info.type));
1114
+ continue;
1115
+ }
1116
+ if (fdInfo.handle >= 0) {
1117
+ // Sync-fence fds become readable (POLLIN) when signaled; poll()
1118
+ // avoids any libsync linkage concern. Poll in bounded slices so
1119
+ // teardown (which flips _shutdown) cannot hang here on a wedged GPU.
1120
+ struct pollfd pfd;
1121
+ pfd.fd = fdInfo.handle;
1122
+ pfd.events = POLLIN;
1123
+ while (!_shutdown.load()) {
1124
+ pfd.revents = 0;
1125
+ int r = poll(&pfd, 1, 100);
1126
+ if (r > 0) {
1127
+ break; // signaled (or POLLERR): stop waiting
1128
+ }
1129
+ if (r < 0 && errno != EINTR) {
1130
+ break; // real poll error; EINTR just retries
1131
+ }
1132
+ }
1133
+ }
1134
+ }
1135
+
1136
+ std::function<void()> frameReady;
1137
+ {
1138
+ std::lock_guard<std::mutex> lock(_poolMutex);
1139
+ // Pool torn down (switchToOffscreen) while this frame was in flight:
1140
+ // discard it now that its fences have been drained. Publishing it would
1141
+ // pin the retired generation forever (nothing polls anymore).
1142
+ if (_pool == nullptr) {
1143
+ continue;
1144
+ }
1145
+ // Supersede a still-unclaimed ready frame: drop it back to Free if it
1146
+ // belongs to the live pool (a retired pool is simply discarded).
1147
+ if (_readyPool != nullptr && _readySlot >= 0) {
1148
+ if (_readyPool == _pool) {
1149
+ _readyPool->slots[_readySlot].state = SlotState::Free;
1150
+ _freeCv.notify_one();
1151
+ }
1152
+ }
1153
+ pending.pool->slots[pending.slot].state = SlotState::Ready;
1154
+ _readyPool = pending.pool;
1155
+ _readySlot = pending.slot;
1156
+ frameReady = _poolFrameReady;
1157
+ }
1158
+ if (frameReady) {
1159
+ frameReady(); // outside the lock: it calls into Java
1160
+ }
1161
+ }
1162
+ }
1163
+ #endif
1164
+
586
1165
  mutable std::shared_mutex _mutex;
587
1166
  #if defined(__ANDROID__)
588
1167
  // DAWN_WORKAROUND_DEVICE_DESTROY_BEFORE_SURFACE_RELEASE
@@ -628,6 +1207,40 @@ private:
628
1207
  // canvas).
629
1208
  int _width;
630
1209
  int _height;
1210
+
1211
+ #if defined(__ANDROID__)
1212
+ // Pool state. Guarded by _poolMutex (never nested under _mutex while
1213
+ // blocking).
1214
+ struct PendingPresent {
1215
+ std::shared_ptr<AHBPool> pool;
1216
+ int slot;
1217
+ std::vector<wgpu::SharedFence> fences;
1218
+ };
1219
+
1220
+ static constexpr int kPoolSize =
1221
+ 5; // 2 held + 1 rendering + 1 waiting + 1 ready
1222
+
1223
+ std::atomic<bool> _poolMode{false};
1224
+ std::mutex _poolMutex;
1225
+ std::condition_variable _freeCv; // a slot returned to Free
1226
+ std::condition_variable _presentCv; // a frame was queued for the waiter
1227
+ wgpu::Device _poolDevice = nullptr;
1228
+ wgpu::TextureFormat _poolFormat = wgpu::TextureFormat::Undefined;
1229
+ wgpu::TextureUsage _poolUsage = wgpu::TextureUsage::RenderAttachment;
1230
+ bool _poolUnsupported = false; // device lacks AHB shared-texture support
1231
+ std::shared_ptr<AHBPool> _pool; // current generation
1232
+ std::unordered_map<uint32_t, std::shared_ptr<AHBPool>> _pools; // gen -> pool
1233
+ std::shared_ptr<AHBPool> _renderPool; // generation of the in-flight render
1234
+ int _currentRenderSlot = -1;
1235
+ std::shared_ptr<AHBPool> _readyPool; // generation of the latest ready frame
1236
+ int _readySlot = -1;
1237
+ std::deque<PendingPresent> _presentQueue;
1238
+ std::thread _waiter;
1239
+ bool _waiterRunning = false;
1240
+ std::atomic<bool> _shutdown{false};
1241
+ uint32_t _genCounter = 0;
1242
+ std::function<void()> _poolFrameReady; // consumer wake-up, see setter
1243
+ #endif
631
1244
  };
632
1245
 
633
1246
  class SurfaceRegistry {