react-native-webrtc-kaleidoscope 1.0.0 → 2.0.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.
Files changed (301) hide show
  1. package/NOTICE.md +17 -6
  2. package/README.md +67 -19
  3. package/android/build.gradle +6 -5
  4. package/android/src/main/assets/backgrounds/dark-office.webp +0 -0
  5. package/android/src/main/assets/backgrounds/debug-resolutions.webp +0 -0
  6. package/android/src/main/assets/backgrounds/home-dark.webp +0 -0
  7. package/android/src/main/assets/backgrounds/home-light.webp +0 -0
  8. package/android/src/main/assets/backgrounds/light-office.webp +0 -0
  9. package/android/src/main/assets/backgrounds/nature-dark.webp +0 -0
  10. package/android/src/main/assets/backgrounds/nature-light.webp +0 -0
  11. package/android/src/main/assets/backgrounds/simiancraft-dark.webp +0 -0
  12. package/android/src/main/assets/backgrounds/simiancraft-light.webp +0 -0
  13. package/android/src/main/assets/backgrounds/stylized-dark.webp +0 -0
  14. package/android/src/main/assets/backgrounds/stylized-light.webp +0 -0
  15. package/android/src/main/assets/selfie_segmenter.tflite +0 -0
  16. package/android/src/main/java/com/simiancraft/kaleidoscope/EffectTuning.kt +41 -10
  17. package/android/src/main/java/com/simiancraft/kaleidoscope/KaleidoscopeModule.kt +8 -0
  18. package/android/src/main/java/com/simiancraft/kaleidoscope/Registration.kt +52 -13
  19. package/android/src/main/java/com/simiancraft/kaleidoscope/effects/BackgroundImageFactory.kt +53 -23
  20. package/android/src/main/java/com/simiancraft/kaleidoscope/effects/BlurFactory.kt +114 -50
  21. package/android/src/main/java/com/simiancraft/kaleidoscope/effects/TransformFactory.kt +277 -0
  22. package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/FramePipeline.kt +158 -0
  23. package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/GlProgram.kt +5 -0
  24. package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/Ingest.kt +123 -0
  25. package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/Orientation.kt +58 -0
  26. package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/Shaders.kt +23 -51
  27. package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/ShadersGenerated.kt +76 -0
  28. package/android/src/main/java/com/simiancraft/kaleidoscope/segmentation/Mask.kt +100 -89
  29. package/android/src/main/java/com/simiancraft/kaleidoscope/segmentation/SegmentationEngine.kt +148 -0
  30. package/app.plugin.js +254 -4
  31. package/dist/backgrounds/dark-office.d.ts +3 -0
  32. package/dist/backgrounds/dark-office.d.ts.map +1 -0
  33. package/dist/backgrounds/dark-office.js +5 -0
  34. package/dist/backgrounds/dark-office.js.map +1 -0
  35. package/dist/backgrounds/dark-office.web.d.ts +3 -0
  36. package/dist/backgrounds/dark-office.web.d.ts.map +1 -0
  37. package/dist/backgrounds/dark-office.web.js +8 -0
  38. package/dist/backgrounds/dark-office.web.js.map +1 -0
  39. package/dist/backgrounds/dark-office.webp +0 -0
  40. package/dist/backgrounds/debug-resolutions.d.ts +3 -0
  41. package/dist/backgrounds/debug-resolutions.d.ts.map +1 -0
  42. package/dist/backgrounds/debug-resolutions.js +6 -0
  43. package/dist/backgrounds/debug-resolutions.js.map +1 -0
  44. package/dist/backgrounds/debug-resolutions.web.d.ts +3 -0
  45. package/dist/backgrounds/debug-resolutions.web.d.ts.map +1 -0
  46. package/dist/backgrounds/debug-resolutions.web.js +8 -0
  47. package/dist/backgrounds/debug-resolutions.web.js.map +1 -0
  48. package/dist/backgrounds/debug-resolutions.webp +0 -0
  49. package/dist/backgrounds/home-dark.d.ts +3 -0
  50. package/dist/backgrounds/home-dark.d.ts.map +1 -0
  51. package/dist/backgrounds/home-dark.js +5 -0
  52. package/dist/backgrounds/home-dark.js.map +1 -0
  53. package/dist/backgrounds/home-dark.web.d.ts +3 -0
  54. package/dist/backgrounds/home-dark.web.d.ts.map +1 -0
  55. package/dist/backgrounds/home-dark.web.js +6 -0
  56. package/dist/backgrounds/home-dark.web.js.map +1 -0
  57. package/dist/backgrounds/home-dark.webp +0 -0
  58. package/dist/backgrounds/home-light.d.ts +3 -0
  59. package/dist/backgrounds/home-light.d.ts.map +1 -0
  60. package/dist/backgrounds/home-light.js +5 -0
  61. package/dist/backgrounds/home-light.js.map +1 -0
  62. package/dist/backgrounds/home-light.web.d.ts +3 -0
  63. package/dist/backgrounds/home-light.web.d.ts.map +1 -0
  64. package/dist/backgrounds/home-light.web.js +6 -0
  65. package/dist/backgrounds/home-light.web.js.map +1 -0
  66. package/dist/backgrounds/home-light.webp +0 -0
  67. package/dist/backgrounds/index.d.ts +3 -0
  68. package/dist/backgrounds/index.d.ts.map +1 -0
  69. package/dist/backgrounds/index.js +6 -0
  70. package/dist/backgrounds/index.js.map +1 -0
  71. package/dist/backgrounds/light-office.d.ts +3 -0
  72. package/dist/backgrounds/light-office.d.ts.map +1 -0
  73. package/dist/backgrounds/light-office.js +5 -0
  74. package/dist/backgrounds/light-office.js.map +1 -0
  75. package/dist/backgrounds/light-office.web.d.ts +3 -0
  76. package/dist/backgrounds/light-office.web.d.ts.map +1 -0
  77. package/dist/backgrounds/light-office.web.js +8 -0
  78. package/dist/backgrounds/light-office.web.js.map +1 -0
  79. package/dist/backgrounds/light-office.webp +0 -0
  80. package/dist/backgrounds/nature-dark.d.ts +3 -0
  81. package/dist/backgrounds/nature-dark.d.ts.map +1 -0
  82. package/dist/backgrounds/nature-dark.js +5 -0
  83. package/dist/backgrounds/nature-dark.js.map +1 -0
  84. package/dist/backgrounds/nature-dark.web.d.ts +3 -0
  85. package/dist/backgrounds/nature-dark.web.d.ts.map +1 -0
  86. package/dist/backgrounds/nature-dark.web.js +6 -0
  87. package/dist/backgrounds/nature-dark.web.js.map +1 -0
  88. package/dist/backgrounds/nature-dark.webp +0 -0
  89. package/dist/backgrounds/nature-light.d.ts +3 -0
  90. package/dist/backgrounds/nature-light.d.ts.map +1 -0
  91. package/dist/backgrounds/nature-light.js +5 -0
  92. package/dist/backgrounds/nature-light.js.map +1 -0
  93. package/dist/backgrounds/nature-light.web.d.ts +3 -0
  94. package/dist/backgrounds/nature-light.web.d.ts.map +1 -0
  95. package/dist/backgrounds/nature-light.web.js +6 -0
  96. package/dist/backgrounds/nature-light.web.js.map +1 -0
  97. package/dist/backgrounds/nature-light.webp +0 -0
  98. package/dist/backgrounds/preset-source.types.d.ts +2 -0
  99. package/dist/backgrounds/preset-source.types.d.ts.map +1 -0
  100. package/dist/backgrounds/preset-source.types.js +2 -0
  101. package/dist/backgrounds/preset-source.types.js.map +1 -0
  102. package/dist/backgrounds/presets.d.ts +3 -0
  103. package/dist/backgrounds/presets.d.ts.map +1 -0
  104. package/dist/backgrounds/presets.js +34 -0
  105. package/dist/backgrounds/presets.js.map +1 -0
  106. package/dist/backgrounds/simiancraft-dark.d.ts +3 -0
  107. package/dist/backgrounds/simiancraft-dark.d.ts.map +1 -0
  108. package/dist/backgrounds/simiancraft-dark.js +5 -0
  109. package/dist/backgrounds/simiancraft-dark.js.map +1 -0
  110. package/dist/backgrounds/simiancraft-dark.web.d.ts +3 -0
  111. package/dist/backgrounds/simiancraft-dark.web.d.ts.map +1 -0
  112. package/dist/backgrounds/simiancraft-dark.web.js +6 -0
  113. package/dist/backgrounds/simiancraft-dark.web.js.map +1 -0
  114. package/dist/backgrounds/simiancraft-dark.webp +0 -0
  115. package/dist/backgrounds/simiancraft-light.d.ts +3 -0
  116. package/dist/backgrounds/simiancraft-light.d.ts.map +1 -0
  117. package/dist/backgrounds/simiancraft-light.js +5 -0
  118. package/dist/backgrounds/simiancraft-light.js.map +1 -0
  119. package/dist/backgrounds/simiancraft-light.web.d.ts +3 -0
  120. package/dist/backgrounds/simiancraft-light.web.d.ts.map +1 -0
  121. package/dist/backgrounds/simiancraft-light.web.js +6 -0
  122. package/dist/backgrounds/simiancraft-light.web.js.map +1 -0
  123. package/dist/backgrounds/simiancraft-light.webp +0 -0
  124. package/dist/backgrounds/stylized-dark.d.ts +3 -0
  125. package/dist/backgrounds/stylized-dark.d.ts.map +1 -0
  126. package/dist/backgrounds/stylized-dark.js +5 -0
  127. package/dist/backgrounds/stylized-dark.js.map +1 -0
  128. package/dist/backgrounds/stylized-dark.web.d.ts +3 -0
  129. package/dist/backgrounds/stylized-dark.web.d.ts.map +1 -0
  130. package/dist/backgrounds/stylized-dark.web.js +6 -0
  131. package/dist/backgrounds/stylized-dark.web.js.map +1 -0
  132. package/dist/backgrounds/stylized-dark.webp +0 -0
  133. package/dist/backgrounds/stylized-light.d.ts +3 -0
  134. package/dist/backgrounds/stylized-light.d.ts.map +1 -0
  135. package/dist/backgrounds/stylized-light.js +5 -0
  136. package/dist/backgrounds/stylized-light.js.map +1 -0
  137. package/dist/backgrounds/stylized-light.web.d.ts +3 -0
  138. package/dist/backgrounds/stylized-light.web.d.ts.map +1 -0
  139. package/dist/backgrounds/stylized-light.web.js +6 -0
  140. package/dist/backgrounds/stylized-light.web.js.map +1 -0
  141. package/dist/backgrounds/stylized-light.webp +0 -0
  142. package/dist/index.d.ts +19 -5
  143. package/dist/index.d.ts.map +1 -1
  144. package/dist/index.js +71 -22
  145. package/dist/index.js.map +1 -1
  146. package/dist/index.web.d.ts +25 -3
  147. package/dist/index.web.d.ts.map +1 -1
  148. package/dist/index.web.js +55 -16
  149. package/dist/index.web.js.map +1 -1
  150. package/dist/livekit.d.ts +24 -0
  151. package/dist/livekit.d.ts.map +1 -0
  152. package/dist/livekit.js +57 -0
  153. package/dist/livekit.js.map +1 -0
  154. package/dist/types.d.ts +20 -13
  155. package/dist/types.d.ts.map +1 -1
  156. package/dist/types.js +0 -3
  157. package/dist/types.js.map +1 -1
  158. package/dist/web/blur-kernel.d.ts +6 -0
  159. package/dist/web/blur-kernel.d.ts.map +1 -0
  160. package/dist/web/blur-kernel.js +41 -0
  161. package/dist/web/blur-kernel.js.map +1 -0
  162. package/dist/web/effects/background-image.d.ts.map +1 -1
  163. package/dist/web/effects/background-image.js +100 -33
  164. package/dist/web/effects/background-image.js.map +1 -1
  165. package/dist/web/effects/blur.d.ts.map +1 -1
  166. package/dist/web/effects/blur.js +169 -38
  167. package/dist/web/effects/blur.js.map +1 -1
  168. package/dist/web/effects/transform.d.ts +4 -0
  169. package/dist/web/effects/transform.d.ts.map +1 -0
  170. package/dist/web/effects/transform.js +62 -0
  171. package/dist/web/effects/transform.js.map +1 -0
  172. package/dist/web/insertable-streams.d.ts +11 -1
  173. package/dist/web/insertable-streams.d.ts.map +1 -1
  174. package/dist/web/insertable-streams.js +22 -4
  175. package/dist/web/insertable-streams.js.map +1 -1
  176. package/dist/web/segmenter.d.ts +9 -0
  177. package/dist/web/segmenter.d.ts.map +1 -1
  178. package/dist/web/segmenter.js +32 -0
  179. package/dist/web/segmenter.js.map +1 -1
  180. package/dist/web/shaders.d.ts +1 -3
  181. package/dist/web/shaders.d.ts.map +1 -1
  182. package/dist/web/shaders.generated.d.ts +4 -0
  183. package/dist/web/shaders.generated.d.ts.map +1 -0
  184. package/dist/web/shaders.generated.js +54 -0
  185. package/dist/web/shaders.generated.js.map +1 -0
  186. package/dist/web/shaders.js +29 -103
  187. package/dist/web/shaders.js.map +1 -1
  188. package/dist/web/tuning.d.ts +4 -0
  189. package/dist/web/tuning.d.ts.map +1 -1
  190. package/dist/web/tuning.js +17 -5
  191. package/dist/web/tuning.js.map +1 -1
  192. package/ios/Kaleidoscope.podspec +87 -14
  193. package/ios/KaleidoscopeModule/EffectTuning.swift +47 -7
  194. package/ios/KaleidoscopeModule/KaleidoscopeModule.swift +12 -0
  195. package/ios/KaleidoscopeModule/Registration.swift +78 -13
  196. package/ios/KaleidoscopeModule/effects/BackgroundImageProcessor.swift +293 -0
  197. package/ios/KaleidoscopeModule/effects/BlurProcessor.swift +232 -18
  198. package/ios/KaleidoscopeModule/effects/FrameBridge.swift +46 -0
  199. package/ios/KaleidoscopeModule/effects/TransformProcessor.swift +173 -0
  200. package/ios/KaleidoscopeModule/gpu/Ingest.swift +169 -0
  201. package/ios/KaleidoscopeModule/gpu/MetalRenderer.swift +601 -0
  202. package/ios/KaleidoscopeModule/gpu/Orientation.swift +83 -0
  203. package/ios/KaleidoscopeModule/gpu/ShaderLibrary.swift +100 -0
  204. package/ios/KaleidoscopeModule/gpu/TextureBridge.swift +291 -0
  205. package/ios/KaleidoscopeModule/resources/backgrounds/dark-office.webp +0 -0
  206. package/ios/KaleidoscopeModule/resources/backgrounds/debug-resolutions.webp +0 -0
  207. package/ios/KaleidoscopeModule/resources/backgrounds/home-dark.webp +0 -0
  208. package/ios/KaleidoscopeModule/resources/backgrounds/home-light.webp +0 -0
  209. package/ios/KaleidoscopeModule/resources/backgrounds/light-office.webp +0 -0
  210. package/ios/KaleidoscopeModule/resources/backgrounds/nature-dark.webp +0 -0
  211. package/ios/KaleidoscopeModule/resources/backgrounds/nature-light.webp +0 -0
  212. package/ios/KaleidoscopeModule/resources/backgrounds/simiancraft-dark.webp +0 -0
  213. package/ios/KaleidoscopeModule/resources/backgrounds/simiancraft-light.webp +0 -0
  214. package/ios/KaleidoscopeModule/resources/backgrounds/stylized-dark.webp +0 -0
  215. package/ios/KaleidoscopeModule/resources/backgrounds/stylized-light.webp +0 -0
  216. package/ios/KaleidoscopeModule/resources/selfie_segmenter.tflite +0 -0
  217. package/ios/KaleidoscopeModule/segmentation/MaskTuning.swift +23 -0
  218. package/ios/KaleidoscopeModule/segmentation/Segmenter.swift +444 -25
  219. package/ios/KaleidoscopeModule/shaders/SHADERS.txt +6 -0
  220. package/ios/KaleidoscopeModule/shaders/blur.metalsrc +72 -0
  221. package/ios/KaleidoscopeModule/shaders/composite.metalsrc +22 -0
  222. package/ios/KaleidoscopeModule/shaders/nebula.metalsrc +72 -0
  223. package/ios/KaleidoscopeModule/shaders/passthrough.metalsrc +20 -0
  224. package/ios/KaleidoscopeModule/shaders/simianlights.metalsrc +72 -0
  225. package/ios/KaleidoscopeModule/shaders/transform.metalsrc +22 -0
  226. package/package.json +120 -11
  227. package/src/backgrounds/README.md +83 -0
  228. package/src/backgrounds/assets.d.ts +7 -0
  229. package/src/backgrounds/dark-office.ts +6 -0
  230. package/src/backgrounds/dark-office.web.ts +9 -0
  231. package/src/backgrounds/dark-office.webp +0 -0
  232. package/src/backgrounds/debug-resolutions.ts +7 -0
  233. package/src/backgrounds/debug-resolutions.web.ts +9 -0
  234. package/src/backgrounds/debug-resolutions.webp +0 -0
  235. package/src/backgrounds/home-dark.ts +6 -0
  236. package/src/backgrounds/home-dark.web.ts +7 -0
  237. package/src/backgrounds/home-dark.webp +0 -0
  238. package/src/backgrounds/home-light.ts +6 -0
  239. package/src/backgrounds/home-light.web.ts +7 -0
  240. package/src/backgrounds/home-light.webp +0 -0
  241. package/src/backgrounds/index.ts +7 -0
  242. package/src/backgrounds/light-office.ts +6 -0
  243. package/src/backgrounds/light-office.web.ts +9 -0
  244. package/src/backgrounds/light-office.webp +0 -0
  245. package/src/backgrounds/nature-dark.ts +6 -0
  246. package/src/backgrounds/nature-dark.web.ts +7 -0
  247. package/src/backgrounds/nature-dark.webp +0 -0
  248. package/src/backgrounds/nature-light.ts +6 -0
  249. package/src/backgrounds/nature-light.web.ts +7 -0
  250. package/src/backgrounds/nature-light.webp +0 -0
  251. package/src/backgrounds/preset-source.types.ts +5 -0
  252. package/src/backgrounds/presets.ts +36 -0
  253. package/src/backgrounds/simiancraft-dark.ts +6 -0
  254. package/src/backgrounds/simiancraft-dark.web.ts +7 -0
  255. package/src/backgrounds/simiancraft-dark.webp +0 -0
  256. package/src/backgrounds/simiancraft-light.ts +6 -0
  257. package/src/backgrounds/simiancraft-light.web.ts +7 -0
  258. package/src/backgrounds/simiancraft-light.webp +0 -0
  259. package/src/backgrounds/stylized-dark.ts +6 -0
  260. package/src/backgrounds/stylized-dark.web.ts +7 -0
  261. package/src/backgrounds/stylized-dark.webp +0 -0
  262. package/src/backgrounds/stylized-light.ts +6 -0
  263. package/src/backgrounds/stylized-light.web.ts +7 -0
  264. package/src/backgrounds/stylized-light.webp +0 -0
  265. package/src/index.ts +94 -29
  266. package/src/index.web.ts +69 -19
  267. package/src/livekit.ts +65 -0
  268. package/src/types.ts +23 -14
  269. package/src/web/blur-kernel.ts +44 -0
  270. package/src/web/effects/background-image.ts +121 -34
  271. package/src/web/effects/blur.ts +214 -40
  272. package/src/web/effects/transform.ts +69 -0
  273. package/src/web/insertable-streams.ts +33 -5
  274. package/src/web/segmenter.ts +34 -0
  275. package/src/web/shaders.generated.ts +56 -0
  276. package/src/web/shaders.ts +29 -106
  277. package/src/web/tuning.ts +19 -5
  278. package/android/src/main/assets/backgrounds/office-1.png +0 -0
  279. package/android/src/main/assets/backgrounds/office-2.png +0 -0
  280. package/android/src/main/java/com/simiancraft/kaleidoscope/effects/MirrorFactory.kt +0 -57
  281. package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/GpuEffectFactory.kt +0 -14
  282. package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/GpuEffectProcessor.kt +0 -198
  283. package/dist/backgrounds.d.ts +0 -3
  284. package/dist/backgrounds.d.ts.map +0 -1
  285. package/dist/backgrounds.js +0 -21
  286. package/dist/backgrounds.js.map +0 -1
  287. package/dist/web/effects/mirror.d.ts +0 -3
  288. package/dist/web/effects/mirror.d.ts.map +0 -1
  289. package/dist/web/effects/mirror.js +0 -31
  290. package/dist/web/effects/mirror.js.map +0 -1
  291. package/dist/web/effects/passthrough.d.ts +0 -3
  292. package/dist/web/effects/passthrough.d.ts.map +0 -1
  293. package/dist/web/effects/passthrough.js +0 -15
  294. package/dist/web/effects/passthrough.js.map +0 -1
  295. package/ios/KaleidoscopeModule/effects/MirrorProcessor.swift +0 -17
  296. package/plugin/build/withKaleidoscope.d.ts +0 -3
  297. package/plugin/build/withKaleidoscope.js +0 -14
  298. package/plugin/build/withKaleidoscope.js.map +0 -1
  299. package/src/backgrounds.ts +0 -23
  300. package/src/web/effects/mirror.ts +0 -37
  301. package/src/web/effects/passthrough.ts +0 -17
@@ -0,0 +1,158 @@
1
+ // One-frame GPU pipeline + fence handoff, shared by all GLES effect factories.
2
+ //
3
+ // THE PROBLEM IT SOLVES (R3 frame-pipelining):
4
+ // The effect factories used to call glFinish() at the end of process(),
5
+ // a full CPU<->GPU sync that blocks the capture thread until every draw call
6
+ // for THIS frame has completed on the GPU, serializing CPU and GPU. At 720p
7
+ // with a multi-pass blur that stall is the dominant cost on the capture
8
+ // thread.
9
+ //
10
+ // THE FIX:
11
+ // Defer the handoff by exactly one frame. Each process() call:
12
+ // 1. Renders frame N into a fresh output texture (caller's job).
13
+ // 2. Hands that texture + a GL fence to enqueue().
14
+ // 3. enqueue() returns frame N-1's texture, whose fence it first waits on.
15
+ // Because frame N-1's GPU work had a whole frame interval to finish, the
16
+ // wait is almost always a no-op (GL_ALREADY_SIGNALED); the capture thread
17
+ // no longer blocks on the just-submitted frame's GPU work.
18
+ // The very first frame has no predecessor, so enqueue() returns null and the
19
+ // caller forwards the original frame for exactly one frame.
20
+ //
21
+ // One frame of latency is acceptable for this use case (matches the existing
22
+ // one-frame latency on mask updates; see Mask.kt).
23
+ //
24
+ // FENCE LIFECYCLE:
25
+ // One fence per in-flight frame. At steady state the pipeline holds exactly
26
+ // one PendingFrame (one texture, one fence). enqueue() waits on and deletes
27
+ // the predecessor's fence, then stores the successor's. dispose() drains the
28
+ // last pending frame (deleting its texture + fence) for processors that ever
29
+ // get a teardown hook.
30
+ //
31
+ // glFenceSync / glClientWaitSync / glDeleteSync and the GL_SYNC_* /
32
+ // GL_ALREADY_SIGNALED / GL_CONDITION_SATISFIED constants are all core GLES 3.0
33
+ // (android.opengl.GLES30), so this needs no extension probing.
34
+
35
+ package com.simiancraft.kaleidoscope.gpu
36
+
37
+ import android.opengl.GLES30
38
+ import android.util.Log
39
+
40
+ internal class FramePipeline {
41
+ /**
42
+ * A rendered-but-not-yet-handed-off output texture and the fence that
43
+ * signals when its GPU work has completed. The caller has already detached
44
+ * and freed the FBO; only the color texture survives here.
45
+ */
46
+ private class PendingFrame(
47
+ val textureId: Int,
48
+ val width: Int,
49
+ val height: Int,
50
+ val rotation: Int,
51
+ val timestampNs: Long,
52
+ val fence: Long,
53
+ // Wall-clock at the moment this frame's GPU work was submitted (the
54
+ // enqueue() call that fenced it). Used only when debug timing is on, to
55
+ // report GPU-completion latency a frame late without adding a blocking
56
+ // wait of its own.
57
+ val submitNanos: Long,
58
+ )
59
+
60
+ private var pending: PendingFrame? = null
61
+
62
+ /**
63
+ * Insert a sync fence into the GL command stream right after the caller's
64
+ * final draw, then enqueue the freshly rendered texture and return the
65
+ * PREVIOUS frame's texture (now GPU-complete) for handoff downstream.
66
+ *
67
+ * Must be called on the GL thread after the output FBO has been detached
68
+ * and deleted (so only `textureId` remains live).
69
+ *
70
+ * @return a [Ready] describing the previous frame to wrap in a
71
+ * TextureBufferImpl, or null on the first frame (caller forwards original).
72
+ */
73
+ fun enqueue(
74
+ textureId: Int,
75
+ width: Int,
76
+ height: Int,
77
+ rotation: Int,
78
+ timestampNs: Long,
79
+ debugTiming: Boolean = false,
80
+ timingLabel: String = "",
81
+ ): Ready? {
82
+ // Fence the current frame's GPU work. glFlush guarantees the fence is
83
+ // actually in the command queue so a later glClientWaitSync without the
84
+ // flush-commands bit can still observe it.
85
+ val fence = GLES30.glFenceSync(GLES30.GL_SYNC_GPU_COMMANDS_COMPLETE, 0)
86
+ GLES30.glFlush()
87
+
88
+ val submitNanos = if (debugTiming) System.nanoTime() else 0L
89
+ val prev = pending
90
+ pending = PendingFrame(textureId, width, height, rotation, timestampNs, fence, submitNanos)
91
+
92
+ if (prev == null) return null
93
+
94
+ // Wait on the PREVIOUS frame's fence. It has had a full frame interval to
95
+ // complete, so this is almost always GL_ALREADY_SIGNALED. The timeout is a
96
+ // backstop against a stalled GPU; we still hand the texture off afterward
97
+ // because the consumer's EGL context shares ours and would otherwise read
98
+ // a torn texture. Pass the flush-commands bit defensively in case the
99
+ // predecessor's flush has not yet been issued on this context.
100
+ val waitResult = GLES30.glClientWaitSync(
101
+ prev.fence,
102
+ GLES30.GL_SYNC_FLUSH_COMMANDS_BIT,
103
+ FENCE_TIMEOUT_NS,
104
+ )
105
+ if (waitResult == GLES30.GL_TIMEOUT_EXPIRED || waitResult == GLES30.GL_WAIT_FAILED) {
106
+ Log.w(TAG, "fence wait returned 0x${waitResult.toString(16)}; handing off anyway")
107
+ }
108
+ GLES30.glDeleteSync(prev.fence)
109
+
110
+ if (debugTiming && prev.submitNanos != 0L) {
111
+ // CPU-observed latency from the predecessor's GPU submit to the point
112
+ // its fence resolved (now). EXT_disjoint_timer_query (GL_TIME_ELAPSED_EXT
113
+ // + glGetQueryObjectui64v) has no android.opengl Java binding, so this
114
+ // wall-clock delta is the fallback; it measures submit->complete latency
115
+ // rather than pure on-GPU time, but reads a frame late exactly as a timer
116
+ // query would and adds no extra blocking wait.
117
+ val elapsedMs = (System.nanoTime() - prev.submitNanos) / 1_000_000.0
118
+ Log.d(PERF_TAG, "$timingLabel gpu submit->complete ~%.2f ms (nanoTime fallback)".format(elapsedMs))
119
+ }
120
+
121
+ return Ready(prev.textureId, prev.width, prev.height, prev.rotation, prev.timestampNs)
122
+ }
123
+
124
+ /**
125
+ * Free the last un-handed-off frame's texture and fence. Call on the GL
126
+ * thread from a processor teardown path. (No VideoFrameProcessor teardown
127
+ * hook exists today, mirroring Mask.release; provided for correctness.)
128
+ */
129
+ fun dispose() {
130
+ val p = pending ?: return
131
+ pending = null
132
+ try {
133
+ GLES30.glDeleteSync(p.fence)
134
+ GLES30.glDeleteTextures(1, intArrayOf(p.textureId), 0)
135
+ } catch (t: Throwable) {
136
+ Log.w(TAG, "dispose encountered an error; resources may leak", t)
137
+ }
138
+ }
139
+
140
+ /** The previous frame's texture + metadata, ready to wrap downstream. */
141
+ class Ready(
142
+ val textureId: Int,
143
+ val width: Int,
144
+ val height: Int,
145
+ val rotation: Int,
146
+ val timestampNs: Long,
147
+ )
148
+
149
+ companion object {
150
+ private const val TAG = "Kaleidoscope.Pipeline"
151
+ private const val PERF_TAG = "Perf"
152
+
153
+ // 2 ms. A signaled fence returns immediately; this only bounds the
154
+ // pathological case where the predecessor's GPU work has not finished a
155
+ // full frame later. glClientWaitSync takes nanoseconds.
156
+ private const val FENCE_TIMEOUT_NS = 2_000_000L
157
+ }
158
+ }
@@ -48,6 +48,11 @@ internal class GlProgram(vertexSource: String, fragmentSource: String) {
48
48
  GLES30.glUniform2f(uniformLocation(name), x, y)
49
49
  }
50
50
 
51
+ /** Upload a column-major 2x2 (4 floats) to a mat2 uniform. */
52
+ fun setMat2(name: String, columnMajor: FloatArray) {
53
+ GLES30.glUniformMatrix2fv(uniformLocation(name), 1, false, columnMajor, 0)
54
+ }
55
+
51
56
  fun delete() {
52
57
  GLES30.glDeleteProgram(id)
53
58
  }
@@ -0,0 +1,123 @@
1
+ // Ingest: the ONE place camera orientation is normalized.
2
+ //
3
+ // ===== Why this exists (read before "fixing" any effect's orientation) =====
4
+ //
5
+ // The camera hands us an OES external texture plus a 3x3 `transformMatrix`. That
6
+ // matrix bakes in sensor-correction, the selfie mirror, and crop, but it does
7
+ // NOT apply the display rotation: on a portrait phone the OES->2D passthrough,
8
+ // fed only `transformMatrix`, still lands a RAW LANDSCAPE frame in the FBO
9
+ // (device-confirmed 2026-05-24). Every effect then had to re-correct that
10
+ // rotation itself (transform ops conjugated in buffer space, the background
11
+ // composited pre-rotated). That per-effect cascade is what this module removes.
12
+ //
13
+ // `composedTexMatrix` folds the display rotation INTO the texture matrix, so the
14
+ // single OES->2D pass that BlurFactory, BackgroundImageFactory, and
15
+ // TransformFactory each run now lands a DISPLAY-UPRIGHT frame in the FBO. The
16
+ // FBO is sized with `displayWidth` / `displayHeight` (buffer dims swapped on a
17
+ // 90/270 frame), so the upright image fills it without clamping.
18
+ //
19
+ // After ingest the "original 2D" FBO is canonical: every downstream pass
20
+ // (transform mat2, background cover-fit, blur, the mask downsample/MediaPipe) reads
21
+ // an already-upright square [0,1] UV space and applies NO further orientation
22
+ // correction. Each factory therefore returns the frame with rotation 0; the
23
+ // pixels are already where the encoder/renderer expects an upright frame.
24
+ //
25
+ // ===== The composition =====
26
+ //
27
+ // The OES frag samples `uv = (uTexMatrix * vec4(vUv, 0, 1)).xy`, where `vUv`
28
+ // runs [0,1] across the destination (now display-upright) quad. We compose
29
+ // M' = transformMatrix * Rot(theta about 0.5)
30
+ // so a destination display-UV is first rotated back into the pre-rotation space
31
+ // `transformMatrix` was authored against, then mapped to buffer UV. theta is
32
+ // `frame.rotation` scaled by ROTATION_DIRECTION.
33
+ //
34
+ // ===== Calibration (device verifies; may need one literal edit) =====
35
+ //
36
+ // ROTATION_DIRECTION below is the single sign that decides whether the display
37
+ // rotation is applied as +R or -R. If the person/background comes out rotated
38
+ // the wrong way (e.g. 90 off, or upside down on a 180 device), flip this sign.
39
+ // Nothing else in the orientation story should ever need touching.
40
+
41
+ package com.simiancraft.kaleidoscope.gpu
42
+
43
+ internal object Ingest {
44
+ // The ONE orientation calibration knob. +1f applies the display rotation as
45
+ // -frame.rotation about center (the geometric inverse, which is what maps a
46
+ // display-upright destination UV back into the camera's pre-rotation UV
47
+ // space); -1f applies +frame.rotation. Flip this sign if a portrait-device
48
+ // screenshot shows the whole frame rotated the wrong way. This is the only
49
+ // place orientation direction is decided.
50
+ private const val ROTATION_DIRECTION = 1f
51
+
52
+ /** Display width for a buffer of [bufferWidth] x [bufferHeight] at [frameRotation]. */
53
+ fun displayWidth(bufferWidth: Int, bufferHeight: Int, frameRotation: Int): Int =
54
+ if (swaps(frameRotation)) bufferHeight else bufferWidth
55
+
56
+ /** Display height for a buffer of [bufferWidth] x [bufferHeight] at [frameRotation]. */
57
+ fun displayHeight(bufferWidth: Int, bufferHeight: Int, frameRotation: Int): Int =
58
+ if (swaps(frameRotation)) bufferWidth else bufferHeight
59
+
60
+ /** Does this frame rotation swap buffer dims into display dims? */
61
+ private fun swaps(frameRotation: Int): Boolean {
62
+ val r = normalize(frameRotation)
63
+ return r == 90 || r == 270
64
+ }
65
+
66
+ /**
67
+ * Column-major 4x4 for the OES->2D pass's `uTexMatrix`, composing the camera
68
+ * [transformMatrix] with the display rotation derived from [frameRotation].
69
+ * Feed the result straight to glUniformMatrix4fv (already flat, length 16).
70
+ *
71
+ * M' = transformMatrix4 * Rot(theta about 0.5), where
72
+ * theta = ROTATION_DIRECTION * frameRotation degrees.
73
+ */
74
+ fun composedTexMatrix(
75
+ transformMatrix: android.graphics.Matrix,
76
+ frameRotation: Int,
77
+ ): FloatArray {
78
+ val base = Egl.matrixToGl(transformMatrix)
79
+ val rot = rotationAboutCenter(ROTATION_DIRECTION * normalize(frameRotation).toFloat())
80
+ return multiplyColumnMajor4(base, rot)
81
+ }
82
+
83
+ /**
84
+ * Column-major 4x4 that rotates the xy UV plane by [degrees] about (0.5, 0.5),
85
+ * leaving z and w untouched. Translate-to-center, rotate, translate-back.
86
+ */
87
+ private fun rotationAboutCenter(degrees: Float): FloatArray {
88
+ val rad = Math.toRadians(degrees.toDouble())
89
+ val c = Math.cos(rad).toFloat()
90
+ val s = Math.sin(rad).toFloat()
91
+ // 2D rotation about 0.5 in (x, y):
92
+ // x' = c*(x-0.5) - s*(y-0.5) + 0.5
93
+ // y' = s*(x-0.5) + c*(y-0.5) + 0.5
94
+ // As an affine 4x4 (column-major): linear block in the upper-left 2x2,
95
+ // translation in the last column.
96
+ val tx = 0.5f - 0.5f * c + 0.5f * s
97
+ val ty = 0.5f - 0.5f * s - 0.5f * c
98
+ return floatArrayOf(
99
+ c, s, 0f, 0f,
100
+ -s, c, 0f, 0f,
101
+ 0f, 0f, 1f, 0f,
102
+ tx, ty, 0f, 1f,
103
+ )
104
+ }
105
+
106
+ /** a * b, both column-major 4x4 flat arrays (length 16). Returns a*b. */
107
+ private fun multiplyColumnMajor4(a: FloatArray, b: FloatArray): FloatArray {
108
+ val out = FloatArray(16)
109
+ for (col in 0 until 4) {
110
+ for (row in 0 until 4) {
111
+ var sum = 0f
112
+ for (k in 0 until 4) {
113
+ // column-major index: element(row, k) = a[k*4 + row]
114
+ sum += a[k * 4 + row] * b[col * 4 + k]
115
+ }
116
+ out[col * 4 + row] = sum
117
+ }
118
+ }
119
+ return out
120
+ }
121
+
122
+ private fun normalize(deg: Int): Int = ((deg % 360) + 360) % 360
123
+ }
@@ -0,0 +1,58 @@
1
+ // Orientation: maps a screen-space reorientation op to a column-major 2x2 (the
2
+ // `uUvTransform` mat2 transform.frag multiplies into UV about the 0.5 center).
3
+ // All four transform ops (flip-x, flip-y, rotate-cw, rotate-ccw) call mat2For;
4
+ // none re-derive anything.
5
+ //
6
+ // ===== Pure screen space (read before "fixing") =====
7
+ //
8
+ // Camera orientation is normalized ONCE upstream, in Ingest: the OES->2D
9
+ // passthrough folds the display rotation into the texture matrix, so by the
10
+ // time TRANSFORM_FRAG samples the "original 2D" FBO it is already
11
+ // DISPLAY-UPRIGHT. The op matrices are therefore pure SCREEN SPACE and do NOT
12
+ // depend on frame.rotation:
13
+ // flip-x (screen-horizontal mirror, head stays up) -> negate U
14
+ // flip-y (screen-vertical flip, upside down) -> negate V
15
+ // rotate-cw / rotate-ccw -> swap axes (+ a sign)
16
+ //
17
+ // There is no per-effect rotation/flip compensation anywhere; if a screenshot
18
+ // shows the WHOLE frame rotated wrong, that is an ingest problem — flip
19
+ // Ingest.ROTATION_DIRECTION, do not add a correction here.
20
+ //
21
+ // mat2 column-major convention: floatArrayOf(a, b, c, d) builds the GLSL mat2
22
+ // whose columns are (a,b) and (c,d), i.e. M * v = (a*v.x + c*v.y, b*v.x +
23
+ // d*v.y). UV is taken about 0.5, so a sign flip on a column negates that output
24
+ // axis; swapping the columns' nonzero entries transposes (rotates) the axes.
25
+
26
+ package com.simiancraft.kaleidoscope.gpu
27
+
28
+ // Public (not internal) because TransformFactory is a public registered factory
29
+ // and takes an Op in its constructor; an internal Op would leak through a public
30
+ // signature.
31
+ object Orientation {
32
+ /** Screen-space reorientation operations the transform effect exposes. */
33
+ enum class Op { FLIP_X, FLIP_Y, ROTATE_CW, ROTATE_CCW }
34
+
35
+ /** Does the op swap output dimensions (w x h -> h x w)? True for rotations. */
36
+ fun swapsDimensions(op: Op): Boolean = op == Op.ROTATE_CW || op == Op.ROTATE_CCW
37
+
38
+ /**
39
+ * Column-major 2x2 for glUniformMatrix2fv. The input frame is already
40
+ * display-upright (see Ingest), so this is rotation-independent.
41
+ *
42
+ * flip-x: negate U -> column 0 = (-1, 0), column 1 = (0, 1).
43
+ * flip-y: negate V -> column 0 = ( 1, 0), column 1 = (0, -1).
44
+ * rotate-cw / rotate-ccw: screen 90-degree rotations. Device-confirmed on the
45
+ * clean sampled space (both flips correct on both platforms, so the matrix->
46
+ * screen map is axis-aligned and orientation-preserving): columns (0,-1),(1,0)
47
+ * rendered COUNTER-clockwise, so clockwise is the inverse, columns (0,1),(-1,0).
48
+ */
49
+ fun mat2For(op: Op): FloatArray =
50
+ when (op) {
51
+ Op.FLIP_X -> floatArrayOf(-1f, 0f, 0f, 1f)
52
+ Op.FLIP_Y -> floatArrayOf(1f, 0f, 0f, -1f)
53
+ // Device-confirmed: columns (0,-1),(1,0) rendered CCW on the clean space,
54
+ // so clockwise is its inverse, columns (0,1),(-1,0).
55
+ Op.ROTATE_CW -> floatArrayOf(0f, 1f, -1f, 0f)
56
+ Op.ROTATE_CCW -> floatArrayOf(0f, -1f, 1f, 0f)
57
+ }
58
+ }
@@ -15,15 +15,10 @@ internal object Shaders {
15
15
  // 1 -> (2,0) -> ( 1,-1), uv (1,0)
16
16
  // 2 -> (0,2) -> (-1, 1), uv (0,1)
17
17
  // 3 -> (2,2) -> ( 1, 1), uv (1,1)
18
- const val PASSTHROUGH_VERT = """#version 300 es
19
- precision highp float;
20
- out vec2 vUv;
21
- void main() {
22
- vec2 p = vec2(float((gl_VertexID & 1) << 1), float(gl_VertexID & 2));
23
- vUv = p * 0.5;
24
- gl_Position = vec4(p - 1.0, 0.0, 1.0);
25
- }
26
- """
18
+ // Canonical source: shaders/passthrough.vert. Generated into
19
+ // ShadersGenerated by `bun run build:shaders`; delegated here so call sites
20
+ // keep using Shaders.PASSTHROUGH_VERT.
21
+ val PASSTHROUGH_VERT = ShadersGenerated.PASSTHROUGH_VERT
27
22
 
28
23
  // Sample the OES external camera texture and emit as 2D RGBA. The first
29
24
  // pass of every effect runs this so subsequent passes can use sampler2D.
@@ -56,24 +51,10 @@ void main() {
56
51
  // uAxis is (1/width, 0) for the horizontal pass and (0, 1/height) for the
57
52
  // vertical pass. uOffsets[0] is the center tap (zero); offsets are in
58
53
  // pixel units, so the shader multiplies by uAxis to convert to UV space.
59
- const val BLUR_FRAG = """#version 300 es
60
- precision mediump float;
61
- uniform sampler2D uTex;
62
- uniform vec2 uAxis;
63
- uniform float uWeights[9];
64
- uniform float uOffsets[9];
65
- in highp vec2 vUv;
66
- out vec4 oColor;
67
- void main() {
68
- vec4 color = texture(uTex, vUv) * uWeights[0];
69
- for (int i = 1; i < 9; i++) {
70
- vec2 off = uAxis * uOffsets[i];
71
- color += texture(uTex, vUv + off) * uWeights[i];
72
- color += texture(uTex, vUv - off) * uWeights[i];
73
- }
74
- oColor = color;
75
- }
76
- """
54
+ // Canonical source: shaders/blur.frag. Generated into ShadersGenerated by
55
+ // `bun run build:shaders`; delegated here. BlurFactory computes the 9 weights
56
+ // and offsets from sigma on the CPU and uploads the arrays each program use.
57
+ val BLUR_FRAG = ShadersGenerated.BLUR_FRAG
77
58
 
78
59
  // Composite: mix(background, original, mask). One shader, byte-identical
79
60
  // to src/web/shaders.ts's COMPOSITE_FRAG_SRC.
@@ -102,28 +83,19 @@ void main() {
102
83
  // blurred copy. For background-image, the caller computes them to perform
103
84
  // a cover-fit center crop so an arbitrarily-shaped image fills the output
104
85
  // without distortion.
105
- const val COMPOSITE_FRAG = """#version 300 es
106
- precision mediump float;
107
- uniform sampler2D uOriginal;
108
- uniform sampler2D uBackground;
109
- uniform sampler2D uMask;
110
- uniform vec2 uBgUvScale;
111
- uniform vec2 uBgUvOffset;
112
- uniform vec2 uMaskUvScale;
113
- uniform vec2 uMaskUvOffset;
114
- uniform float uMaskLo;
115
- uniform float uMaskHi;
116
- in highp vec2 vUv;
117
- out vec4 oColor;
118
- void main() {
119
- vec2 maskUv = vUv * uMaskUvScale + uMaskUvOffset;
120
- float raw = texture(uMask, maskUv).r;
121
- float safeHi = max(uMaskHi, uMaskLo + 0.001);
122
- float m = smoothstep(uMaskLo, safeHi, raw);
123
- vec3 orig = texture(uOriginal, vUv).rgb;
124
- vec2 bgUv = clamp(vUv * uBgUvScale + uBgUvOffset, 0.0, 1.0);
125
- vec3 bg = texture(uBackground, bgUv).rgb;
126
- oColor = vec4(mix(bg, orig, m), 1.0);
127
- }
128
- """
86
+ // Canonical source: shaders/composite.frag. Generated into ShadersGenerated
87
+ // by `bun run build:shaders`; delegated here. The per-platform mask/bg
88
+ // orientation differences live in the uniforms the host sets (see above),
89
+ // not in the shader, so the same generated body serves every runtime.
90
+ val COMPOSITE_FRAG = ShadersGenerated.COMPOSITE_FRAG
91
+
92
+ // Transform: a single-pass geometric reorientation (axis flip or 90-degree
93
+ // rotation) driven by the uUvTransform mat2 the host computes from a pure
94
+ // screen-space op via Orientation.mat2For. One shader serves all four ops
95
+ // (flip-x, flip-y, rotate-cw, rotate-ccw); the input is already
96
+ // display-upright (camera orientation is normalized once in Ingest), so no
97
+ // rotation correction happens here.
98
+ // Canonical source: shaders/transform.frag. Generated into ShadersGenerated
99
+ // by `bun run build:shaders`; delegated here.
100
+ val TRANSFORM_FRAG = ShadersGenerated.TRANSFORM_FRAG
129
101
  }
@@ -0,0 +1,76 @@
1
+ // @generated by scripts/build-shaders.ts from shaders/. DO NOT EDIT.
2
+ // Run `bun run build:shaders` to regenerate.
3
+ //
4
+ // Cross-runtime shared shaders. Platform-local shaders (OES external texture,
5
+ // 2D passthrough) stay hand-written in Shaders.kt, which delegates the consts
6
+ // below.
7
+
8
+ package com.simiancraft.kaleidoscope.gpu
9
+
10
+ internal object ShadersGenerated {
11
+ const val PASSTHROUGH_VERT = """#version 300 es
12
+ precision highp float;
13
+ out highp vec2 vUv;
14
+ void main() {
15
+ vec2 p = vec2(float((gl_VertexID & 1) << 1), float(gl_VertexID & 2));
16
+ vUv = p * 0.5;
17
+ gl_Position = vec4(p - 1.0, 0.0, 1.0);
18
+ }
19
+ """
20
+
21
+ const val COMPOSITE_FRAG = """#version 300 es
22
+ precision mediump float;
23
+ uniform sampler2D uOriginal;
24
+ uniform sampler2D uBackground;
25
+ uniform sampler2D uMask;
26
+ uniform vec2 uBgUvScale;
27
+ uniform vec2 uBgUvOffset;
28
+ uniform vec2 uMaskUvScale;
29
+ uniform vec2 uMaskUvOffset;
30
+ uniform float uMaskLo;
31
+ uniform float uMaskHi;
32
+ in highp vec2 vUv;
33
+ out vec4 oColor;
34
+ void main() {
35
+ vec2 maskUv = vUv * uMaskUvScale + uMaskUvOffset;
36
+ float raw = texture(uMask, maskUv).r;
37
+ float safeHi = max(uMaskHi, uMaskLo + 0.001);
38
+ float m = smoothstep(uMaskLo, safeHi, raw);
39
+ vec3 orig = texture(uOriginal, vUv).rgb;
40
+ vec2 bgUv = clamp(vUv * uBgUvScale + uBgUvOffset, 0.0, 1.0);
41
+ vec3 bg = texture(uBackground, bgUv).rgb;
42
+ oColor = vec4(mix(bg, orig, m), 1.0);
43
+ }
44
+ """
45
+
46
+ const val BLUR_FRAG = """#version 300 es
47
+ precision mediump float;
48
+ uniform sampler2D uTex;
49
+ uniform vec2 uAxis;
50
+ uniform float uWeights[5];
51
+ uniform float uOffsets[5];
52
+ in highp vec2 vUv;
53
+ out vec4 oColor;
54
+ void main() {
55
+ vec4 color = texture(uTex, vUv) * uWeights[0];
56
+ for (int i = 1; i < 5; i++) {
57
+ vec2 off = uAxis * uOffsets[i];
58
+ color += texture(uTex, vUv + off) * uWeights[i];
59
+ color += texture(uTex, vUv - off) * uWeights[i];
60
+ }
61
+ oColor = color;
62
+ }
63
+ """
64
+
65
+ const val TRANSFORM_FRAG = """#version 300 es
66
+ precision mediump float;
67
+ uniform sampler2D uTex;
68
+ uniform mat2 uUvTransform;
69
+ in highp vec2 vUv;
70
+ out vec4 oColor;
71
+ void main() {
72
+ vec2 uv = uUvTransform * (vUv - 0.5) + 0.5;
73
+ oColor = texture(uTex, uv);
74
+ }
75
+ """
76
+ }