Skip to content

Commit 633b23a

Browse files
authored
fix(🖼️): true async createImageBitmap (#311)
1 parent 6a5d2c2 commit 633b23a

10 files changed

Lines changed: 329 additions & 107 deletions

File tree

apps/example/ios/Podfile.lock

Lines changed: 65 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1865,7 +1865,7 @@ PODS:
18651865
- ReactCommon/turbomodule/core
18661866
- SocketRocket
18671867
- Yoga
1868-
- react-native-wgpu (0.5.0):
1868+
- react-native-wgpu (0.5.2):
18691869
- boost
18701870
- DoubleConversion
18711871
- fast_float
@@ -2903,81 +2903,81 @@ SPEC CHECKSUMS:
29032903
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
29042904
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
29052905
hermes-engine: 35c763d57c9832d0eef764316ca1c4d043581394
2906-
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
2906+
RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f
29072907
RCTDeprecation: c0ed3249a97243002615517dff789bf4666cf585
29082908
RCTRequired: 58719f5124f9267b5f9649c08bf23d9aea845b23
29092909
RCTTypeSafety: 4aefa8328ab1f86da273f08517f1f6b343f6c2cc
29102910
React: 2073376f47c71b7e9a0af7535986a77522ce1049
29112911
React-callinvoker: 751b6f2c83347a0486391c3f266f291f0f53b27e
2912-
React-Core: dff5d29973349b11dd6631c9498456d75f846d5e
2913-
React-CoreModules: c0ae04452e4c5d30e06f8e94692a49107657f537
2914-
React-cxxreact: 376fd672c95dfb64ad5cc246e6a1e9edb78dec4c
2912+
React-Core: 7195661f0b48e7ea46c3360ccb575288a20c932c
2913+
React-CoreModules: 14f0054ab46000dd3b816d6528af3bd600d82073
2914+
React-cxxreact: 7f602425c63096c398dac13cd7a300efd7c281ae
29152915
React-debug: 7b56a0a7da432353287d2eedac727903e35278f5
2916-
React-defaultsnativemodule: 393b81aaa6211408f50a6ef00a277847256dd881
2917-
React-domnativemodule: 5fb5829baa7a7a0f217019cbad1eb226d94f7062
2918-
React-Fabric: a17c4ae35503673b57b91c2d1388429e7cbee452
2919-
React-FabricComponents: a76572ddeba78ebe4ec58615291e9db4a55cd46a
2920-
React-FabricImage: d806eb2695d7ef355ec28d1a21f5a14ac26b1cae
2921-
React-featureflags: 1690ec3c453920b6308e23a4e24eb9c3632f9c75
2922-
React-featureflagsnativemodule: 7b7e8483fc671c5a33aefd699b7c7a3c0bdfdfec
2923-
React-graphics: ea146ee799dc816524a3a0922fc7be0b5a52dcc1
2924-
React-hermes: fcbdc45ecf38259fe3b12642bd0757c52270a107
2925-
React-idlecallbacksnativemodule: a353f9162eaa7ad787e68aba9f52a1cfa8154098
2926-
React-ImageManager: ec5cf55ce9cc81719eb5f1f51d23d04db851c86c
2927-
React-jserrorhandler: 594c593f3d60f527be081e2cace7710c2bd9f524
2928-
React-jsi: 59ec3190dd364cca86a58869e7755477d2468948
2929-
React-jsiexecutor: b87d78a2e8dd7a6f56e9cdac038da45de98c944f
2930-
React-jsinspector: b9204adf1af622c98e78af96ec1bca615c2ce2bd
2931-
React-jsinspectorcdp: 4a356fa69e412d35d3a38c44d4a6cc555c5931e8
2932-
React-jsinspectornetwork: 7820056773178f321cbf18689e1ffcd38276a878
2933-
React-jsinspectortracing: b341c5ef6e031a33e0bd462d67fd397e8e9cd612
2934-
React-jsitooling: 401655e05cb966b0081225c5201d90734a567cb9
2935-
React-jsitracing: 67eff6dea0cb58a1e7bd8b49243012d88c0f511e
2936-
React-logger: a3cb5b29c32b8e447b5a96919340e89334062b48
2937-
React-Mapbuffer: 9d2434a42701d6144ca18f0ca1c4507808ca7696
2938-
React-microtasksnativemodule: 75b6604b667d297292345302cc5bfb6b6aeccc1b
2939-
react-native-safe-area-context: c00143b4823773bba23f2f19f85663ae89ceb460
2940-
react-native-skia: 5bf2b2107cd7f2d806fd364f5e16b1c7554ed3cd
2941-
react-native-wgpu: e54fcee5946cc2cee4814f63f425be358f097b14
2942-
React-NativeModulesApple: 879fbdc5dcff7136abceb7880fe8a2022a1bd7c3
2916+
React-defaultsnativemodule: 695d8a0b40f735edb3c4031e0f049e567fdac47a
2917+
React-domnativemodule: 6d66c1f61f277d008d98cae650ce2c025b89d3b9
2918+
React-Fabric: 997d4115d688f483cb409a1290171bff3c93dab4
2919+
React-FabricComponents: 8167e5e363ca3a3fe394d8afee355e4072bea1db
2920+
React-FabricImage: f8f9f2c97657116702acc670e3f4357bc842bed3
2921+
React-featureflags: dfb4d0d527d55dd968231370f6832b9197ee653d
2922+
React-featureflagsnativemodule: c63cfd8fe95cd98f12ebb37daa919c4544810a45
2923+
React-graphics: fd795f1c2a1133a08dde31725b20949edd545dca
2924+
React-hermes: 0a167bbb02c242664745e82154578c64e90a88e5
2925+
React-idlecallbacksnativemodule: 1798c6aa33ddc7c2e9fa3c3d67729728639889e9
2926+
React-ImageManager: c498ee6945dffacc82bfa175aa3264212f27c70b
2927+
React-jserrorhandler: 216951fea62fc26c600f4c96f0dc4fd53d1e7a9b
2928+
React-jsi: 9c27d27d3007b73c702ad3fd5a6166557c741020
2929+
React-jsiexecutor: 2b24f4ed4026344a27f717bf947a434cbbeeff7a
2930+
React-jsinspector: 02394b059c48805780f7d977366317a24168d00e
2931+
React-jsinspectorcdp: f4b6d5c5c9db05ef44d082716714f90cfeed96bb
2932+
React-jsinspectornetwork: e7c77d01b5f0664e24c0bec1aea27d5e3d7fb746
2933+
React-jsinspectortracing: aaa96a4e53abb88dc6d47da3b5744c710652fef9
2934+
React-jsitooling: 226e5f4147c7b6f1ae1954a8406ffa713f3da828
2935+
React-jsitracing: 8a2fbeaa9c53c3f0b23904ccffefc890eae48d71
2936+
React-logger: 1767babce2d28c3251039ce05556714a2c8c6ded
2937+
React-Mapbuffer: 33f678ee25b6c0ee2b01b1ecec08e3e02424cefe
2938+
React-microtasksnativemodule: 44b44a4d3cd6ffb85d928abf741acdc26722de2e
2939+
react-native-safe-area-context: 54d812805f3c4e08a4580ad086cbde1d8780c2e4
2940+
react-native-skia: 4df548eb44d05ce5e35679b15a9d765e5724126e
2941+
react-native-wgpu: b4dc1b3af4fb7e6169f504ad28a60508d530d06a
2942+
React-NativeModulesApple: b5d18bc109c45c9a1c6b71664991b5cc3adc4e48
29432943
React-oscompat: 93b5535ea7f7dff46aaee4f78309a70979bdde9d
2944-
React-perflogger: 5536d2df3d18fe0920263466f7b46a56351c0510
2945-
React-performancetimeline: 9041c53efa07f537164dcfe7670a36642352f4c2
2944+
React-perflogger: a03d913e3205b00aee4128082abe42fd45ce0c98
2945+
React-performancetimeline: 9b5986cc15afafb9bf246d7dd55bdd138df94451
29462946
React-RCTActionSheet: 42195ae666e6d79b4af2346770f765b7c29435b9
2947-
React-RCTAnimation: fa103ccc3503b1ed8dedca7e62e7823937748843
2948-
React-RCTAppDelegate: 665d4baf19424cef08276e9ac0d8771eec4519f9
2949-
React-RCTBlob: 0fa9530c255644db095f2c4fd8d89738d9d9ecc0
2950-
React-RCTFabric: 1fcd8af6e25f92532f56b4ba092e58662c14d156
2951-
React-RCTFBReactNativeSpec: db171247585774f9f0a30f75109cc51568686213
2952-
React-RCTImage: ba824e61ce2e920a239a65d130b83c3a1d426dff
2953-
React-RCTLinking: d2dc199c37e71e6f505d9eca3e5c33be930014d4
2954-
React-RCTNetwork: 87137d4b9bd77e5068f854dd5c1f30d4b072faf6
2955-
React-RCTRuntime: 137fafaa808a8b7e76a510e8be45f9f827899daa
2956-
React-RCTSettings: 71f5c7fd7b5f4e725a4e2114a4b4373d0e46048f
2957-
React-RCTText: b94d4699b49285bee22b8ebf768924d607eccee3
2958-
React-RCTVibration: 6e3993c4f6c36a3899059f9a9ead560ddaf5a7d7
2947+
React-RCTAnimation: 5c10527683128c56ff2c09297fb080f7c35bd293
2948+
React-RCTAppDelegate: c616bd5b0d12f0b21dfacee9cd2d512c6df013aa
2949+
React-RCTBlob: 6e3757bdd7dce6fd9788c0dd675fd6b6c432db9d
2950+
React-RCTFabric: e8f3b9da97477710bf0904a62eb5b5209c964694
2951+
React-RCTFBReactNativeSpec: c042f8d60d44ad9e2c722da89323c0bdab7a37af
2952+
React-RCTImage: a3482fe1ae562d1bab08b42d4670a7c9a21813cd
2953+
React-RCTLinking: d82b9adb141aef9d2b38d446b837ae7017ab60aa
2954+
React-RCTNetwork: fa9350dd99354c5695964f589bd4790bdd4f6a85
2955+
React-RCTRuntime: be99a38cd23388c08921d8969c82a1997a11ec90
2956+
React-RCTSettings: b7f4a03f44dba1d3a4dc6770843547b203ca9129
2957+
React-RCTText: 91dc597a5f6b27fd1048bb287c41ea05eeca9333
2958+
React-RCTVibration: 27b09ddf74bddfa30a58d20e48f885ea6ed6c9d9
29592959
React-rendererconsistency: b4785e5ed837dc7c242bbc5fdd464b33ef5bfae7
2960-
React-renderercss: e6fb0ba387b389c595ffa86b8b628716d31f58dc
2961-
React-rendererdebug: 60a03de5c7ea59bf2d39791eb43c4c0f5d8b24e3
2962-
React-RuntimeApple: 3df6788cd9b938bb8cb28298d80b5fbd98a4d852
2963-
React-RuntimeCore: fad8adb4172c414c00ff6980250caf35601a0f5d
2964-
React-runtimeexecutor: d2db7e72d97751855ea0bf5273d2ac84e5ea390c
2965-
React-RuntimeHermes: 04faa4cf9a285136a6d73738787fe36020170613
2966-
React-runtimescheduler: f6a1c9555e7131b4a8b64cce01489ad0405f6e8d
2967-
React-timing: 1e6a8acb66e2b7ac9d418956617fd1fdb19322fd
2968-
React-utils: 52bbb03f130319ef82e4c3bc7a85eaacdb1fec87
2969-
ReactAppDependencyProvider: 433ddfb4536948630aadd5bd925aff8a632d2fe3
2970-
ReactCodegen: 7042ec4a7316b59e8f247b8fa312891179d24f5a
2971-
ReactCommon: 394c6b92765cf6d211c2c3f7f6bc601dffb316a6
2972-
ReactNativeHost: f5e054387e917216a2a021a3f7fdc4f9f158e7e4
2973-
ReactTestApp-DevSupport: 9b7bbba5e8fed998e763809171d9906a1375f9d3
2960+
React-renderercss: cef3f26df2ddec558ce3c0790fc574b4fb62ce67
2961+
React-rendererdebug: e68433ae67738caeb672a6c8cc993e9276b298a9
2962+
React-RuntimeApple: dc1d4709bf847bc695dbe6e8aaf3e22ef25aef02
2963+
React-RuntimeCore: ca3473c8b6578693fa3bad4d44240098d49d6723
2964+
React-runtimeexecutor: 0db3ca0b09cd72489cef3a3729349b3c2cf13320
2965+
React-RuntimeHermes: f92cabaf97ef2546a74360eddfc1c74a34cb9ff8
2966+
React-runtimescheduler: 06aea75069e0d556a75d258bfc89eb0ebd5d557e
2967+
React-timing: 1a90df9a04d8e7fd165ff7fa0918b9595c776373
2968+
React-utils: 92115441fb55ce01ded4abfb5e9336a74cd93e9c
2969+
ReactAppDependencyProvider: b20fba6c3d091a393925890009999472c8f94d95
2970+
ReactCodegen: cf03d376a26d393f818d511240b026fc8c95313c
2971+
ReactCommon: 00df7b9f859c9d02181844255bb89a8bca544374
2972+
ReactNativeHost: b63ce830e7c5b4e3adcf556b2ef41665da189da0
2973+
ReactTestApp-DevSupport: c7bff1aee7663f2fb1eefcf60c41573f02916c41
29742974
ReactTestApp-Resources: 1bd9ff10e4c24f2ad87101a32023721ae923bccf
2975-
RNGestureHandler: e37bdb684df1ac17c7e1d8f71a3311b2793c186b
2976-
RNReanimated: 464375ff2caa801358547c44eca894ff0bf68e74
2977-
RNWorklets: ee58e869ea579800ec5f2f1cb6ae195fd3537546
2975+
RNGestureHandler: 92ad734ef0da16d69d0a325b7e1e9ae80bdce1f9
2976+
RNReanimated: 52dad96755e908e80b403c5ccdd7e24451f0c42a
2977+
RNWorklets: 95eb4f990dd495be6bf35a2567b0351a6d7f73cf
29782978
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
2979-
Yoga: a3ed390a19db0459bd6839823a6ac6d9c6db198d
2979+
Yoga: 922d794dce2af9c437f864bf4093abfa7a131adb
29802980

29812981
PODFILE CHECKSUM: b4c1d70c599aba416a49b6bad5eea5084b4e43d0
29822982

2983-
COCOAPODS: 1.16.2
2983+
COCOAPODS: 1.15.2

packages/webgpu/android/cpp/AndroidPlatformContext.h

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
#include <android/bitmap.h>
44
#include <jni.h>
55

6+
#include <functional>
67
#include <memory>
78
#include <string>
9+
#include <thread>
810
#include <vector>
911

1012
#include "webgpu/webgpu_cpp.h"
@@ -128,6 +130,112 @@ class AndroidPlatformContext : public PlatformContext {
128130
result.data = imageData;
129131
return result;
130132
}
133+
134+
void createImageBitmapAsync(
135+
std::string blobId, double offset, double size,
136+
std::function<void(ImageData)> onSuccess,
137+
std::function<void(std::string)> onError) override {
138+
// Capture blobModule for the background thread
139+
jobject blobModule = _blobModule;
140+
141+
// Dispatch to a background thread
142+
std::thread([blobModule, blobId = std::move(blobId), offset, size,
143+
onSuccess = std::move(onSuccess),
144+
onError = std::move(onError)]() {
145+
jni::Environment::ensureCurrentThreadIsAttached();
146+
147+
JNIEnv *env = facebook::jni::Environment::current();
148+
if (!env) {
149+
onError("Couldn't get JNI environment");
150+
return;
151+
}
152+
153+
if (!blobModule) {
154+
onError("BlobModule instance is null");
155+
return;
156+
}
157+
158+
// Get the resolve method ID
159+
jclass blobModuleClass = env->GetObjectClass(blobModule);
160+
if (!blobModuleClass) {
161+
onError("Couldn't find BlobModule class");
162+
return;
163+
}
164+
165+
jmethodID resolveMethod = env->GetMethodID(blobModuleClass, "resolve",
166+
"(Ljava/lang/String;II)[B");
167+
if (!resolveMethod) {
168+
onError("Couldn't find resolve method in BlobModule");
169+
return;
170+
}
171+
172+
// Resolve the blob data
173+
jstring jBlobId = env->NewStringUTF(blobId.c_str());
174+
jbyteArray blobData = (jbyteArray)env->CallObjectMethod(
175+
blobModule, resolveMethod, jBlobId, static_cast<jint>(offset),
176+
static_cast<jint>(size));
177+
env->DeleteLocalRef(jBlobId);
178+
179+
if (!blobData) {
180+
onError("Couldn't retrieve blob data");
181+
return;
182+
}
183+
184+
// Create a Bitmap from the blob data
185+
jclass bitmapFactoryClass =
186+
env->FindClass("android/graphics/BitmapFactory");
187+
jmethodID decodeByteArrayMethod =
188+
env->GetStaticMethodID(bitmapFactoryClass, "decodeByteArray",
189+
"([BII)Landroid/graphics/Bitmap;");
190+
jint blobLength = env->GetArrayLength(blobData);
191+
jobject bitmap = env->CallStaticObjectMethod(
192+
bitmapFactoryClass, decodeByteArrayMethod, blobData, 0, blobLength);
193+
194+
if (!bitmap) {
195+
env->DeleteLocalRef(blobData);
196+
onError("Couldn't decode image");
197+
return;
198+
}
199+
200+
// Get bitmap info
201+
AndroidBitmapInfo bitmapInfo;
202+
if (AndroidBitmap_getInfo(env, bitmap, &bitmapInfo) !=
203+
ANDROID_BITMAP_RESULT_SUCCESS) {
204+
env->DeleteLocalRef(blobData);
205+
env->DeleteLocalRef(bitmap);
206+
onError("Couldn't get bitmap info");
207+
return;
208+
}
209+
210+
// Lock the bitmap pixels
211+
void *bitmapPixels;
212+
if (AndroidBitmap_lockPixels(env, bitmap, &bitmapPixels) !=
213+
ANDROID_BITMAP_RESULT_SUCCESS) {
214+
env->DeleteLocalRef(blobData);
215+
env->DeleteLocalRef(bitmap);
216+
onError("Couldn't lock bitmap pixels");
217+
return;
218+
}
219+
220+
// Copy the bitmap data
221+
std::vector<uint8_t> imageData(bitmapInfo.height * bitmapInfo.stride);
222+
memcpy(imageData.data(), bitmapPixels, imageData.size());
223+
224+
// Unlock the bitmap pixels
225+
AndroidBitmap_unlockPixels(env, bitmap);
226+
227+
// Clean up JNI references
228+
env->DeleteLocalRef(blobData);
229+
env->DeleteLocalRef(bitmap);
230+
231+
ImageData result;
232+
result.width = static_cast<int>(bitmapInfo.width);
233+
result.height = static_cast<int>(bitmapInfo.height);
234+
result.data = std::move(imageData);
235+
236+
onSuccess(std::move(result));
237+
}).detach();
238+
}
131239
};
132240

133241
} // namespace rnwgpu

packages/webgpu/apple/ApplePlatformContext.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ class ApplePlatformContext : public PlatformContext {
1515

1616
ImageData createImageBitmap(std::string blobId, double offset,
1717
double size) override;
18+
19+
void createImageBitmapAsync(
20+
std::string blobId, double offset, double size,
21+
std::function<void(ImageData)> onSuccess,
22+
std::function<void(std::string)> onError) override;
1823
};
1924

2025
} // namespace rnwgpu

packages/webgpu/apple/ApplePlatformContext.mm

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,73 @@ void checkIfUsingSimulatorWithAPIValidation() {
9595
return result;
9696
}
9797

98+
void ApplePlatformContext::createImageBitmapAsync(
99+
std::string blobId, double offset, double size,
100+
std::function<void(ImageData)> onSuccess,
101+
std::function<void(std::string)> onError) {
102+
// Capture blob data on the current thread (requires RCTBridge access)
103+
RCTBlobManager *blobManager =
104+
[[RCTBridge currentBridge] moduleForClass:RCTBlobManager.class];
105+
NSData *blobData =
106+
[blobManager resolve:[NSString stringWithUTF8String:blobId.c_str()]
107+
offset:(long)offset
108+
size:(long)size];
109+
110+
if (!blobData) {
111+
onError("Couldn't retrieve blob data");
112+
return;
113+
}
114+
115+
// Retain the data for the background block
116+
NSData *retainedData = [blobData copy];
117+
118+
// Dispatch heavy image decoding work to a background queue
119+
dispatch_async(
120+
dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
121+
@autoreleasepool {
122+
#if !TARGET_OS_OSX
123+
UIImage *image = [UIImage imageWithData:retainedData];
124+
#else
125+
NSImage *image = [[NSImage alloc] initWithData:retainedData];
126+
#endif
127+
if (!image) {
128+
onError("Couldn't decode image");
129+
return;
130+
}
131+
132+
#if !TARGET_OS_OSX
133+
CGImageRef cgImage = image.CGImage;
134+
#else
135+
CGImageRef cgImage = [image CGImageForProposedRect:NULL
136+
context:NULL
137+
hints:NULL];
138+
#endif
139+
size_t width = CGImageGetWidth(cgImage);
140+
size_t height = CGImageGetHeight(cgImage);
141+
size_t bitsPerComponent = 8;
142+
size_t bytesPerRow = width * 4;
143+
std::vector<uint8_t> imageData(height * bytesPerRow);
144+
145+
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
146+
CGContextRef context = CGBitmapContextCreate(
147+
imageData.data(), width, height, bitsPerComponent, bytesPerRow,
148+
colorSpace,
149+
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
150+
151+
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
152+
153+
CGContextRelease(context);
154+
CGColorSpaceRelease(colorSpace);
155+
156+
ImageData result;
157+
result.width = static_cast<int>(width);
158+
result.height = static_cast<int>(height);
159+
result.data = std::move(imageData);
160+
result.format = wgpu::TextureFormat::RGBA8Unorm;
161+
162+
onSuccess(std::move(result));
163+
}
164+
});
165+
}
166+
98167
} // namespace rnwgpu

packages/webgpu/cpp/rnwgpu/PlatformContext.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#pragma once
22

3+
#include <functional>
34
#include <memory>
45
#include <string>
56
#include <vector>
@@ -24,6 +25,12 @@ class PlatformContext {
2425
int width, int height) = 0;
2526
virtual ImageData createImageBitmap(std::string blobId, double offset,
2627
double size) = 0;
28+
29+
// Async version that performs image decoding on a background thread
30+
virtual void createImageBitmapAsync(
31+
std::string blobId, double offset, double size,
32+
std::function<void(ImageData)> onSuccess,
33+
std::function<void(std::string)> onError) = 0;
2734
};
2835

2936
} // namespace rnwgpu

packages/webgpu/cpp/rnwgpu/RNWebGPUManager.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ RNWebGPUManager::RNWebGPUManager(
6060
BaseRuntimeAwareCache::setMainJsRuntime(_jsRuntime);
6161

6262
auto gpu = std::make_shared<GPU>(*_jsRuntime);
63-
auto rnWebGPU = std::make_shared<RNWebGPU>(gpu, _platformContext);
63+
auto rnWebGPU = std::make_shared<RNWebGPU>(gpu, _platformContext, _jsCallInvoker);
6464
_gpu = gpu->get();
6565
_jsRuntime->global().setProperty(*_jsRuntime, "RNWebGPU",
6666
RNWebGPU::create(*_jsRuntime, rnWebGPU));

0 commit comments

Comments
 (0)