react-native-vroom-chart 0.13.0 → 0.14.0

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.
@@ -124,6 +124,14 @@ target_compile_options(vroomchart PRIVATE -Wno-mismatched-tags)
124
124
 
125
125
  find_library(VROOM_LOG_LIB log)
126
126
 
127
+ # Android 15+ devices can use 16KB pages. NDK r27 and older default to 4KB
128
+ # ELF alignment (align=2**12); Play/SDK 35 then rejects libvroomchart.so.
129
+ # r28+ already aligns to 16KB, so these flags are redundant there but harmless.
130
+ target_link_options(vroomchart PRIVATE
131
+ "-Wl,-z,max-page-size=16384"
132
+ "-Wl,-z,common-page-size=16384"
133
+ )
134
+
127
135
  target_link_libraries(vroomchart
128
136
  ${VROOM_LOG_LIB}
129
137
  ReactAndroid::jsi
package/android/README.md CHANGED
@@ -52,13 +52,14 @@ different, unmerged RTTI record for "the same" class and fails with
52
52
  `Expected a Skia object of a different type`.
53
53
 
54
54
  `VroomChartHostObject.cpp`'s `wrapPicture()` works around this only on
55
- Android: instead of constructing `RNSkia::JsiSkPicture` directly (as iOS
56
- does), it serializes the `SkPicture` (`SkPicture::serialize()`) and calls
57
- RN-Skia's own public JS API, `Skia.Picture.MakePicture(bytes)`, via JSI. That
58
- runs `JsiSkPictureFactory::MakePicture` genuinely compiled inside
59
- `librnskia.so` — so the returned object's RTTI matches what
60
- `librnskia.so`'s own code expects. This costs a serialize + re-parse of the
61
- picture's draw commands on every `render()`/gesture call, which is real
62
- overhead on what's meant to be a 60fps hot path; if that shows up in
63
- profiling, revisit (e.g. a merged-`.so` build, or a lighter-weight bridge
64
- that avoids the round trip).
55
+ Android. Two copies of Skia cannot share an `sk_sp<SkPicture>` pointer, and
56
+ serializing the chart picture (the original workaround) embedded the Android
57
+ system typeface on every pan/zoom frame an OOM that killed the process after
58
+ a few seconds of gesturing.
59
+
60
+ The hot path now rasterizes the picture in vroom's Skia and hands RN-Skia raw
61
+ pixels via `Skia.Image.MakeImage`, so `<Image>` paints a framebuffer whose size
62
+ is bounded by the view. If rasterization fails, we still serialize, but with
63
+ `SkTypeface::SerializeBehavior::kDontIncludeData` so the font file is not
64
+ copied; `MakePicture` re-resolves `sans-serif` inside `librnskia.so`. iOS is
65
+ unchanged (one binary, direct `JsiSkPicture` wrap).
@@ -4,6 +4,7 @@
4
4
  #include <string>
5
5
  #include <vector>
6
6
 
7
+ #include "chart.h"
7
8
  #include "chart_internal.h"
8
9
  #include "vroom/vroom_chart.h"
9
10
 
@@ -11,6 +12,18 @@
11
12
  #include "JsiSkNativeObjects.h"
12
13
  #include "JsiSkPicture.h"
13
14
 
15
+ #if defined(__ANDROID__)
16
+ #include <algorithm>
17
+ #include <cmath>
18
+
19
+ #include "include/core/SkBitmap.h"
20
+ #include "include/core/SkCanvas.h"
21
+ #include "include/core/SkData.h"
22
+ #include "include/core/SkImageInfo.h"
23
+ #include "include/core/SkSerialProcs.h"
24
+ #include "include/core/SkTypeface.h"
25
+ #endif
26
+
14
27
  namespace vroom {
15
28
 
16
29
  namespace jsi = facebook::jsi;
@@ -74,27 +87,21 @@ std::vector<jsi::PropNameID> ChartHostObject::getPropertyNames(
74
87
  }
75
88
 
76
89
  #if defined(__ANDROID__)
77
- // Android only: RN-Skia is compiled into its own librnskia.so, separate from
78
- // libvroomchart.so. Constructing a RNSkia::JsiSkPicture directly here (as iOS
79
- // does, below) gives it a vtable/typeinfo from *our* .so; when RN-Skia's own
80
- // compiled code later consumes it (e.g. Convertor.h's
81
- // `getPropertyValue<sk_sp<SkPicture>>`, which does
82
- // `getJsiObject<JsiSkPicture>(rt, value)` — a dynamic_pointer_cast
83
- // under the hood), the cast fails cross-.so with "Expected a Skia object of
84
- // a different type", because the object's *runtime* type was never actually
85
- // compiled inside librnskia.so. iOS statically links everything into one
86
- // binary, so no such mismatch exists there.
90
+ // Android only: RN-Skia lives in librnskia.so, vroom in libvroomchart.so, and
91
+ // each statically links its own libskia.a. A JsiSkPicture constructed here has
92
+ // our .so's RTTI, so RN-Skia's dynamic_pointer_cast fails with "Expected a
93
+ // Skia object of a different type". iOS statically links one binary, so the
94
+ // iOS wrap below can hand the sk_sp<SkPicture> through directly.
87
95
  //
88
- // The fix: build the picture through RN-Skia's own public JS API instead
89
- // (`Skia.Picture.MakePicture(bytes)`, the same call `require(...).png`-style
90
- // static pictures use), so the resulting JsiSkPicture is genuinely
91
- // constructed by librnskia.so's own code. This costs a serialize +
92
- // re-parse of the picture's draw ops per call — real overhead on a gesture
93
- // hot path but is the only ABI-safe option found so far. Revisit if
94
- // profiling shows this mattering (e.g. a merged-.so build, or a lighter
95
- // bridge that avoids the round trip).
96
- #include "include/core/SkData.h"
97
-
96
+ // Crossing that boundary used to serialize the chart picture and call
97
+ // Skia.Picture.MakePicture(bytes). Default SkPicture::serialize() embeds the
98
+ // Android system typeface (Noto-scale, megabytes) on every pan/zoom frame;
99
+ // Hermes never kept up and the process was OOM-killed (~3 GB RSS in ~10s).
100
+ //
101
+ // Preferred path: rasterize in *our* Skia and hand RN-Skia raw pixels via
102
+ // Skia.Image.MakeImage memory is one framebuffer, labels stay correct.
103
+ // Fallback: still serialize, but with kDontIncludeData so the font file is
104
+ // not copied; MakePicture re-resolves sans-serif on RN-Skia's side.
98
105
  namespace {
99
106
  class SkDataMutableBuffer : public facebook::jsi::MutableBuffer {
100
107
  public:
@@ -108,41 +115,119 @@ class SkDataMutableBuffer : public facebook::jsi::MutableBuffer {
108
115
  private:
109
116
  sk_sp<SkData> data_;
110
117
  };
111
- } // namespace
112
118
 
113
- static facebook::jsi::Value wrapPicture(facebook::jsi::Runtime& rt,
114
- const sk_sp<SkPicture>& pic) {
115
- if (!pic) return facebook::jsi::Value::null();
116
- sk_sp<SkData> serialized = pic->serialize();
117
- if (!serialized) return facebook::jsi::Value::null();
119
+ jsi::Value skiaApiObject(jsi::Runtime& rt, const char* name) {
120
+ auto skiaApi = rt.global().getProperty(rt, "SkiaApi");
121
+ if (!skiaApi.isObject()) return jsi::Value::undefined();
122
+ auto prop = skiaApi.asObject(rt).getProperty(rt, name);
123
+ return prop.isObject() ? std::move(prop) : jsi::Value::undefined();
124
+ }
118
125
 
119
- auto buffer = std::make_shared<SkDataMutableBuffer>(std::move(serialized));
126
+ // Uint8Array-shaped `{ buffer }` — MakePicture and Data.fromBytes both
127
+ // read the ArrayBuffer off that property.
128
+ jsi::Object bytesLike(jsi::Runtime& rt, sk_sp<SkData> data) {
129
+ auto buffer = std::make_shared<SkDataMutableBuffer>(std::move(data));
120
130
  jsi::ArrayBuffer arrayBuffer(rt, buffer);
131
+ jsi::Object arg(rt);
132
+ arg.setProperty(rt, "buffer", arrayBuffer);
133
+ return arg;
134
+ }
121
135
 
122
- auto skiaApi = rt.global().getProperty(rt, "SkiaApi");
123
- if (!skiaApi.isObject()) return facebook::jsi::Value::null();
124
- auto pictureFactory = skiaApi.asObject(rt).getProperty(rt, "Picture");
125
- if (!pictureFactory.isObject()) return facebook::jsi::Value::null();
136
+ sk_sp<const SkData> serializeTypefaceNoEmbed(SkTypeface* tf, void*) {
137
+ if (!tf) return nullptr;
138
+ return tf->serialize(SkTypeface::SerializeBehavior::kDontIncludeData);
139
+ }
140
+
141
+ jsi::Value wrapSerializedPicture(jsi::Runtime& rt, const sk_sp<SkPicture>& pic) {
142
+ SkSerialProcs procs;
143
+ procs.fTypefaceProc = serializeTypefaceNoEmbed;
144
+ sk_sp<SkData> serialized = pic->serialize(&procs);
145
+ if (!serialized) return jsi::Value::null();
146
+
147
+ auto pictureFactory = skiaApiObject(rt, "Picture");
148
+ if (pictureFactory.isUndefined()) return jsi::Value::null();
126
149
  auto pictureFactoryObj = pictureFactory.asObject(rt);
127
150
  auto makePicture =
128
151
  pictureFactoryObj.getPropertyAsFunction(rt, "MakePicture");
129
152
 
130
- // Mirrors JsiSkPictureFactory::MakePicture's expected argument shape: an
131
- // object with a `.buffer` property holding the ArrayBuffer (matching a
132
- // Uint8Array-like value; the factory ignores everything else about it).
133
- //
134
153
  // Skia 2.11's NativeState host methods recover the factory via `this`
135
154
  // (`fromThis` → "Expected PictureFactory but got non-object" if unbound).
136
- jsi::Object arg(rt);
137
- arg.setProperty(rt, "buffer", arrayBuffer);
138
- return makePicture.callWithThis(rt, pictureFactoryObj, arg);
155
+ return makePicture.callWithThis(rt, pictureFactoryObj,
156
+ bytesLike(rt, std::move(serialized)));
157
+ }
158
+
159
+ jsi::Value wrapRasterImage(jsi::Runtime& rt, const sk_sp<SkPicture>& pic,
160
+ float dpr) {
161
+ const SkRect cull = pic->cullRect();
162
+ const float scale = dpr > 0.f ? dpr : 1.f;
163
+ const int w =
164
+ std::max(1, static_cast<int>(std::ceil(cull.width() * scale)));
165
+ const int h =
166
+ std::max(1, static_cast<int>(std::ceil(cull.height() * scale)));
167
+
168
+ // Reused across frames on this JS thread so pan/zoom doesn't heap-allocate
169
+ // a new bitmap every time. Multiple charts share it; a size change reallocs.
170
+ thread_local SkBitmap raster;
171
+ const auto info = SkImageInfo::MakeN32Premul(w, h);
172
+ if ((raster.width() != w || raster.height() != h) &&
173
+ !raster.tryAllocPixels(info)) {
174
+ return jsi::Value::null();
175
+ }
176
+ raster.eraseColor(SK_ColorTRANSPARENT);
177
+ SkCanvas canvas(raster);
178
+ canvas.scale(scale, scale);
179
+ canvas.translate(-cull.left(), -cull.top());
180
+ canvas.drawPicture(pic);
181
+
182
+ SkPixmap pixmap;
183
+ if (!raster.peekPixels(&pixmap) || pixmap.addr() == nullptr) {
184
+ return jsi::Value::null();
185
+ }
186
+ sk_sp<SkData> pixels =
187
+ SkData::MakeWithCopy(pixmap.addr(), pixmap.computeByteSize());
188
+ if (!pixels) return jsi::Value::null();
189
+
190
+ auto dataFactory = skiaApiObject(rt, "Data");
191
+ auto imageFactory = skiaApiObject(rt, "Image");
192
+ if (dataFactory.isUndefined() || imageFactory.isUndefined()) {
193
+ return jsi::Value::null();
194
+ }
195
+ auto dataFactoryObj = dataFactory.asObject(rt);
196
+ auto imageFactoryObj = imageFactory.asObject(rt);
197
+ auto fromBytes = dataFactoryObj.getPropertyAsFunction(rt, "fromBytes");
198
+ auto makeImage = imageFactoryObj.getPropertyAsFunction(rt, "MakeImage");
199
+
200
+ auto skData =
201
+ fromBytes.callWithThis(rt, dataFactoryObj, bytesLike(rt, std::move(pixels)));
202
+ if (!skData.isObject()) return jsi::Value::null();
203
+
204
+ jsi::Object imageInfo(rt);
205
+ imageInfo.setProperty(rt, "width", static_cast<double>(w));
206
+ imageInfo.setProperty(rt, "height", static_cast<double>(h));
207
+ imageInfo.setProperty(rt, "colorType",
208
+ static_cast<double>(pixmap.colorType()));
209
+ imageInfo.setProperty(rt, "alphaType",
210
+ static_cast<double>(pixmap.alphaType()));
211
+ return makeImage.callWithThis(rt, imageFactoryObj, imageInfo, skData,
212
+ static_cast<double>(pixmap.rowBytes()));
213
+ }
214
+ } // namespace
215
+
216
+ static facebook::jsi::Value wrapPicture(facebook::jsi::Runtime& rt,
217
+ const sk_sp<SkPicture>& pic,
218
+ float dpr = 1.f) {
219
+ if (!pic) return facebook::jsi::Value::null();
220
+ auto image = wrapRasterImage(rt, pic, dpr);
221
+ if (!image.isNull()) return image;
222
+ return wrapSerializedPicture(rt, pic);
139
223
  }
140
224
  #else
141
225
  // Shared helper: wraps a fresh picture for return to JS. Memory pressure is
142
226
  // applied inside JsiSkPicture::create (NativeObject::create reads
143
227
  // getMemoryPressure() → picture->approximateBytesUsed()).
144
228
  static facebook::jsi::Value wrapPicture(facebook::jsi::Runtime& rt,
145
- const sk_sp<SkPicture>& pic) {
229
+ const sk_sp<SkPicture>& pic,
230
+ float /*dpr*/ = 1.f) {
146
231
  if (!pic) return facebook::jsi::Value::null();
147
232
  auto host =
148
233
  std::make_shared<RNSkia::JsiSkPicture>(/*context=*/nullptr, pic);
@@ -399,17 +484,25 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
399
484
  }
400
485
 
401
486
  if (name == "beginIntervalMorph") {
402
- // beginIntervalMorph() — capture the visible candle geometry so the next
403
- // setCandles can be animated as a reshape. Call before setCandles.
487
+ // beginIntervalMorph(mode?) — capture the visible candle geometry so the
488
+ // next setCandles can animate. Optional string `'fade'` or `'transform'`
489
+ // (default). Call before setCandles.
404
490
  return jsi::Function::createFromHostFunction(
405
491
  rt,
406
492
  jsi::PropNameID::forAscii(rt, "beginIntervalMorph"),
407
- 0,
408
- [this](jsi::Runtime& /*rt2*/,
493
+ 1,
494
+ [this](jsi::Runtime& rt2,
409
495
  const jsi::Value& /*thisVal*/,
410
- const jsi::Value* /*args*/,
411
- size_t /*count*/) -> jsi::Value {
412
- vroom_chart_begin_interval_morph(chart_);
496
+ const jsi::Value* args,
497
+ size_t count) -> jsi::Value {
498
+ int32_t mode = 0;
499
+ if (count >= 1 && args[0].isString()) {
500
+ const auto s = args[0].asString(rt2).utf8(rt2);
501
+ mode = s == "fade" ? 1 : 0;
502
+ } else if (count >= 1 && args[0].isNumber()) {
503
+ mode = args[0].asNumber() != 0 ? 1 : 0;
504
+ }
505
+ vroom_chart_begin_interval_morph(chart_, mode);
413
506
  return jsi::Value::undefined();
414
507
  });
415
508
  }
@@ -448,7 +541,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
448
541
  const float dx = static_cast<float>(args[0].asNumber());
449
542
  const float dy = static_cast<float>(args[1].asNumber());
450
543
  vroom_chart_pan(chart_, dx, dy);
451
- return wrapPicture(rt2, render_chart_picture(chart_));
544
+ return wrapPicture(rt2, render_chart_picture(chart_),
545
+ chart_->px_ratio);
452
546
  });
453
547
  }
454
548
 
@@ -467,7 +561,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
467
561
  const float dx = static_cast<float>(args[0].asNumber());
468
562
  const float dy = static_cast<float>(args[1].asNumber());
469
563
  vroom_chart_translate(chart_, dx, dy);
470
- return wrapPicture(rt2, render_chart_picture(chart_));
564
+ return wrapPicture(rt2, render_chart_picture(chart_),
565
+ chart_->px_ratio);
471
566
  });
472
567
  }
473
568
 
@@ -489,7 +584,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
489
584
  const float fx = static_cast<float>(args[2].asNumber());
490
585
  const float fy = static_cast<float>(args[3].asNumber());
491
586
  vroom_chart_zoom(chart_, sx, sy, fx, fy);
492
- return wrapPicture(rt2, render_chart_picture(chart_));
587
+ return wrapPicture(rt2, render_chart_picture(chart_),
588
+ chart_->px_ratio);
493
589
  });
494
590
  }
495
591
 
@@ -505,7 +601,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
505
601
  if (count < 1) return jsi::Value::null();
506
602
  const float dy = static_cast<float>(args[0].asNumber());
507
603
  vroom_chart_scale_price_axis(chart_, dy);
508
- return wrapPicture(rt2, render_chart_picture(chart_));
604
+ return wrapPicture(rt2, render_chart_picture(chart_),
605
+ chart_->px_ratio);
509
606
  });
510
607
  }
511
608
 
@@ -521,7 +618,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
521
618
  if (count < 1) return jsi::Value::null();
522
619
  const float dx = static_cast<float>(args[0].asNumber());
523
620
  vroom_chart_scale_time_axis(chart_, dx);
524
- return wrapPicture(rt2, render_chart_picture(chart_));
621
+ return wrapPicture(rt2, render_chart_picture(chart_),
622
+ chart_->px_ratio);
525
623
  });
526
624
  }
527
625
 
@@ -572,7 +670,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
572
670
  const float x = static_cast<float>(args[0].asNumber());
573
671
  const float y = static_cast<float>(args[1].asNumber());
574
672
  vroom_chart_set_crosshair(chart_, x, y);
575
- return wrapPicture(rt2, render_chart_picture(chart_));
673
+ return wrapPicture(rt2, render_chart_picture(chart_),
674
+ chart_->px_ratio);
576
675
  });
577
676
  }
578
677
 
@@ -587,7 +686,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
587
686
  const jsi::Value* /*args*/,
588
687
  size_t /*count*/) -> jsi::Value {
589
688
  vroom_chart_clear_crosshair(chart_);
590
- return wrapPicture(rt2, render_chart_picture(chart_));
689
+ return wrapPicture(rt2, render_chart_picture(chart_),
690
+ chart_->px_ratio);
591
691
  });
592
692
  }
593
693
 
@@ -1098,7 +1198,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
1098
1198
  const jsi::Value& /*thisVal*/,
1099
1199
  const jsi::Value* /*args*/,
1100
1200
  size_t /*count*/) -> jsi::Value {
1101
- return wrapPicture(rt2, render_chart_picture(chart_));
1201
+ return wrapPicture(rt2, render_chart_picture(chart_),
1202
+ chart_->px_ratio);
1102
1203
  });
1103
1204
  }
1104
1205
 
@@ -6,7 +6,7 @@
6
6
  // Methods exposed to JS:
7
7
  // setCandles(arrayBuffer) — copies packed VroomCandle[] into the core
8
8
  // setSize(width, height, pxRatio)
9
- // render() -> JsiSkPicture — returns the current chart picture
9
+ // render() -> SkPicture (iOS) / SkImage (Android)
10
10
 
11
11
  #pragma once
12
12
 
@@ -5,7 +5,7 @@
5
5
  // methods on the returned object:
6
6
  // setCandles(arrayBuffer)
7
7
  // setSize(w, h, pxRatio)
8
- // render() -> JsiSkPicture
8
+ // render() -> SkPicture (iOS) / SkImage (Android)
9
9
  //
10
10
  // The host object's lifetime is managed by JS GC — when the React component
11
11
  // drops its reference, the underlying VroomChart is destroyed.
@@ -420,14 +420,13 @@ void vroom_chart_preserve_price_envelope(VroomChart* chart,
420
420
  double prev_low, double prev_high);
421
421
 
422
422
  // Captures the currently visible candle geometry so the next data swap can be
423
- // animated as a reshape rather than a jump: each candle's wick and body slide
424
- // and stretch into the shape of its counterpart in the new data.
423
+ // animated. `mode` 0 (transform) lerps each visible column into its counterpart,
424
+ // paired by slot counting back from the right edge. `mode` 1 (fade) fades the
425
+ // capture out then the new scene in on the same envelope the axes already use.
425
426
  //
426
- // Candles are paired by *slot* position counting back from the right edge of
427
- // the visible window, which a timeframe switch preserves. Call before
428
- // set_candles, then drive vroom_chart_set_interval_morph from 0 to 1.
429
- // No-op when nothing is visible.
430
- void vroom_chart_begin_interval_morph(VroomChart* chart);
427
+ // Call before set_candles, then drive vroom_chart_set_interval_morph from 0
428
+ // to 1. No-op when nothing is visible.
429
+ void vroom_chart_begin_interval_morph(VroomChart* chart, int32_t mode);
431
430
 
432
431
  // Advances the interval morph started by vroom_chart_begin_interval_morph. `t`
433
432
  // (clamped to 0..1) is the eased progress: 0 renders the captured geometry
@@ -28,11 +28,13 @@ namespace vroom::candles {
28
28
  // body/wick heights and width shrink to the line). `opacity` (0..1) fades the
29
29
  // whole candle layer out as the line fades in. Both default to a no-op.
30
30
  //
31
- // `from` / `from_n` is the outgoing geometry of an interval morph, indexed from
32
- // the right of the visible slice (slot 0 = newest), and `morph_t` (0..1) is the
33
- // eased progress toward `visible`. Each slot's wick and body interpolate between
34
- // the two; slots present on only one side fade in or out. `morph_t == 1` (the
35
- // default) draws `visible` alone.
31
+ // `from` / `from_n` is the outgoing geometry of an interval *transform*, indexed
32
+ // from the right of the visible slice (slot 0 = newest), and `morph_t` (0..1)
33
+ // is the eased progress toward `visible`. Each slot's wick and body interpolate
34
+ // between the two; slots present on only one side fade in or out. A host-chosen
35
+ // fade (see interval_morph_fade) skips this lerp: the chart draws the capture
36
+ // then the new scene via interval_phase instead. `morph_t == 1` (the default)
37
+ // draws `visible` alone.
36
38
  void draw(SkCanvas* canvas,
37
39
  const ::VroomCandle* visible,
38
40
  std::size_t n,