Files
cuframes/docs/architecture.md
gx fbe1d18c39 docs: troubleshooting guide + production notes
- docs/troubleshooting.md — 13 секций с реальными grабельками которые мы
  прошли: cudaIpcOpenEventHandle invalid device context (pid namespace),
  s6-overlay vs pid share, scale_cuda missing (cuda-llvm + stdbit.h glibc 2.36),
  libcuframes not found install paths, ffbuild/ missing source, GMP no working
  compiler (long-long reliability), zlib.net deprecated URL, RTSP/RTP UDP
  docker NAT, gitea actions Node version
- docs/architecture.md — Appendix A "Production deployment notes" с реальными
  observations после 24h+ run: что подтвердилось, что доработали, что не учли
- docs/requirements.md — production deployment matrix + Docker namespace
  requirements таблица (cross-container CUDA IPC требует 5 условий)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 00:37:13 +01:00

507 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CUDA IPC Frame Bus — open-source FFmpeg-плагин
**Working name:** `cuframes` (рабочий, finalize при release)
**License:** LGPL-2.1+ (compatibility с FFmpeg)
**Статус:** дизайн-спецификация v0.1
**Дата:** 2026-05-14
## TL;DR
FFmpeg-плагин и C/C++ библиотека для zero-copy sharing декодированных
видеокадров между процессами через CUDA IPC. Один процесс делает decode на
GPU, кадр остаётся в VRAM, любое количество других процессов читает его без
копирования. Distributed как open-source.
Use case 0 (наш): Frigate decode'ит видео, `cctv-companion` consume'ит те же
кадры для mosaic-композиции на TV. Без двойного decode.
Use case N (общий): любая комбинация AI-pipeline + NVR + аналитика на одной
машине с NVIDIA GPU.
## 1. Контекст и мотивация
Сейчас типичный setup:
```
Camera → ffmpeg #1 (NVR, decode + record)
→ ffmpeg #2 (AI-detector, decode + inference)
→ ffmpeg #3 (custom analytics, decode + ...)
Каждый decode = NVDEC operation + VRAM. Inter-process передача
требует cudaMemcpy в host RAM и обратно. На 16 cameras × 25 fps ×
3 consumers = 75% VRAM-bandwidth впустую.
```
Существующие решения:
| | Что | Почему не подходит |
|---|---|---|
| NVIDIA DeepStream | Production-grade SDK, в т.ч. shared GPU buffers | Закрытое, vendor lock-in, требует переписи pipeline |
| GStreamer NVMM | NVIDIA Multimedia memory plugin | Inter-process только через DMA-buf, сложная интеграция |
| Custom CUDA IPC | Прямой `cudaIpcGetMemHandle` | Требует написать с нуля у каждого; нет интеграции с FFmpeg |
| Frigate +1 ffmpeg | Каждый сервис свой decode | Дублирование, что и побудило проект |
Пустая ниша: **простой FFmpeg-filter** который добавляется в любой
ffmpeg-pipeline и публикует декодированные кадры через CUDA IPC. Без
рерайта пайплайна, без vendor SDK, без специальной библиотеки на стороне
producer'а — просто extra-flag к ffmpeg.
## 1.bis Prior art / competitive analysis
Research проведён 2026-05-14, документирует что **ниша свободна**.
### Существующие технологии (building blocks, не plug-and-play)
| Проект | License | Что | Почему не аналог |
|---|---|---|---|
| **NVIDIA CUDA IPC API** | Proprietary headers, free use | Core `cudaIpcGetMemHandle`/`cudaIpcOpenMemHandle` | Сырой API, не плагин для FFmpeg — наш фундамент |
| **NVIDIA DeepStream** | Closed-source SDK | `NvDsBufSurface` shared-buffers + ML pipeline | Vendor lock-in, требует переписать pipeline в DeepStream API. Не FFmpeg-filter. |
| **NVIDIA VPF** ([github](https://github.com/NVIDIA/VideoProcessingFramework)) | Apache 2.0 | Python bindings вокруг NvCodec | **Single-process только**, inter-process не делает |
| **PyNvVideoCodec** | NVIDIA-proprietary, free | Python decoder с PyTorch zero-copy interop | Single-process |
| **dora-rs CUDA IPC** ([dora-rs.ai](https://dora-rs.ai/docs/guides/Development/Cuda/)) | Apache 2.0 | `torch_to_ipc_buffer()` / `ipc_buffer_to_ipc_handle()` для dataflow framework | Robotics-focused, работает на уровне torch tensors. Не FFmpeg-плагин. **Можем переиспользовать low-level подходы**. |
| **GStreamer NVMM** | LGPL | NVIDIA Multimedia Memory plugins | Inter-thread (intra-process); inter-process через DMA-buf только на Jetson |
| **BabitMF** (Bytedance) | Apache 2.0 | GPU filtering framework | Single-process intra-pipeline, не FFmpeg-filter |
| **Frigate go2rtc relay** | MIT | Re-stream compressed bytes между consumers | Не zero-copy — каждый consumer декодит снова |
### Подтверждение спроса в community
Threads в `blakeblackshear/frigate`, ровно описывающие нашу проблему:
- **#17033** — «Recommended GPU for Video Decoding with ~80 Cameras» — обсуждение saturation NVDEC engines при наивном double-decode
- **#20191** — «As soon as I add a second camera, nothing works» — пользователи натыкаются на decode-related issues
- **#21559** — «System with 25 cameras — which GPU» — все ответы про "купите больше железа", решения через shared decode нет
- **#21865** — «Combining Nvidia GPU and integrated GPU» — workaround через split decoders, что лишний раз доказывает что shared decode был бы полезен
### Проверка FFmpeg upstream
- FFmpeg-devel mailing list 2024-2026: **ноль** patches содержащих CUDA IPC inter-process filter
- FFmpeg master tree (8.0.1): CUDA-фильтры (`scale_cuda`, `bilateral_cuda`, `bwdif_cuda`, `libvmaf_cuda`, `overlay_cuda`) — все intra-process
- Search GitHub `vf_cuda_ipc | cudaipc | cuda_ipc_export`: zero forks/patches с этой функциональностью
### Вывод
**Ниша свободна.** Подтверждённый спрос (Frigate community), стабильные building-blocks (NVIDIA CUDA IPC API + dora-rs reference). Нет risk'а дублирования усилий. Sources проверены 2026-05-14.
## 2. Архитектура
### 2.1 Компоненты репозитория
```
cuframes/ ← OSS repo
├── filter/
│ └── vf_cuda_ipc_export.c FFmpeg filter (C, ~300 строк)
├── libcuframes/ producer/consumer ядро
│ ├── include/cuframes.h публичный C API
│ ├── src/
│ │ ├── producer.c ring buffer + Unix socket server
│ │ ├── consumer.c subscribe + cudaIpcOpenMemHandle
│ │ └── protocol.c wire format, версионирование
│ └── tests/
│ ├── pingpong.c smoke test: 1 producer ↔ 1 consumer
│ ├── multi_consumer.c 1 → N
│ ├── stress.c 24h, leak detection
│ └── bench.c latency, throughput
├── bindings/
│ ├── cpp/ C++ RAII wrapper
│ └── python/ pybind11 binding (numpy/torch interop)
├── examples/
│ ├── ffmpeg-basic/ минимальный FFmpeg → consumer
│ ├── opencv-consumer/ cv::cuda::GpuMat из shared frame
│ ├── pytorch-consumer/ torch.Tensor zero-copy
│ ├── frigate-integration/ Frigate config + Docker compose
│ └── mosaic-compositor/ наш cctv-companion (как demo)
├── docker/
│ ├── ffmpeg-cuframes/ FFmpeg с встроенным filter
│ └── ffmpeg-cuframes-frigate/ Drop-in replacement для Frigate's ffmpeg
├── docs/
│ ├── architecture.md эта спека после редактирования для OSS
│ ├── protocol.md wire format
│ ├── quickstart.md 5-минутный пример
│ ├── building-ffmpeg.md как пересобрать FFmpeg с filter
│ ├── frigate-howto.md конкретно под Frigate
│ ├── benchmarks.md numbers
│ └── faq.md
├── scripts/
│ ├── build-ffmpeg-patched.sh автоматизация upstream build
│ ├── package-deb.sh .deb для Ubuntu
│ └── ci/
└── .github/
└── workflows/ CI: build matrix Ubuntu 22.04/24.04, CUDA 12.x
```
### 2.2 Wire protocol (краткое)
Producer:
- Аллоцирует ring buffer (default 4 slots) в VRAM через `cudaMalloc`
- На каждый decoded frame — пишет в next slot, делает `cudaStreamSynchronize`
- Публикует `seq` атомарно
- Сигналит consumers через eventfd
Consumer:
- Connect к `/run/cuframes/<key>.sock`, handshake (negotiate format, ring size)
- Mmap shared memory header `/dev/shm/cuframes.<key>`
- Wait eventfd → read latest slot → `cudaIpcOpenMemHandle` → CUDA pointer
- Использует frame zero-copy → ACK через atomic bit в bitmap
Backpressure: drop oldest (newest-wins). Multi-consumer независимы — медленный
не блокирует быстрых.
Полный формат — в `docs/protocol.md`. Здесь — концепт.
### 2.3 FFmpeg filter API
```
-vf "[any pipeline before],cuda_ipc_export=key=NAME[:ring=N][:sync_event=1],[pipeline after]"
```
Опции:
- `key` *(required)* — имя shared resource. Должно совпадать у producer и consumer.
- `ring` — глубина ring buffer (default 4)
- `sync_event` — использовать `cudaIpcEventHandle_t` вместо `cudaStreamSynchronize`
(zero-copy sync, но experimental)
Filter:
- Принимает frame в CUDA hwframe-context
- Side-effect: публикует через `libcuframes`
- Passthrough — frame идёт дальше в pipeline без модификации (refcount++)
Пример:
```bash
ffmpeg -hwaccel cuda -hwaccel_output_format cuda \
-i rtsp://camera/stream \
-vf "scale_cuda=1920:1080,cuda_ipc_export=key=cam1" \
-c:v copy -f segment recording.mp4
```
В этой команде frame и публикуется через CUDA IPC, и пишется в архив. Один
decode для всего.
### 2.4 Consumer API
#### C
```c
#include <cuframes.h>
cuframes_subscriber_t* sub = cuframes_subscribe("cam1", "my-app");
cuframes_frame_t frame;
while (cuframes_next(sub, &frame, /*timeout_ms=*/100) == 0) {
// frame.cuda_ptr — device pointer, zero-copy
// frame.width, height, pitch_y, pitch_uv, format
// frame.pts_ns
do_inference_on_cuda(frame.cuda_ptr, frame.width, frame.height);
cuframes_release(&frame); // ACK обратно producer'у
}
cuframes_unsubscribe(sub);
```
#### C++ (RAII)
```cpp
#include <cuframes.hpp>
cuframes::Subscriber sub("cam1", "my-app");
while (auto frame = sub.next(std::chrono::milliseconds(100))) {
// frame->cuda_ptr — zero-copy
cv::cuda::GpuMat mat(frame->height, frame->width, CV_8UC2, // NV12
frame->cuda_ptr, frame->pitch_y);
// ... use mat
// frame destructor → ACK
}
```
#### Python (numpy/torch interop)
```python
import cuframes
with cuframes.Subscriber("cam1", "my-app") as sub:
for frame in sub:
# frame.tensor: torch.Tensor on CUDA, zero-copy
# frame.dlpack: __dlpack__ для numpy/torch/jax interop
output = model(frame.tensor)
```
### 2.5 Distribution и установка
#### Уровень 1 — FFmpeg patch
Минимальный патч к FFmpeg `libavfilter/`:
- `vf_cuda_ipc_export.c` (новый файл)
- `Makefile`: добавить filter
- `allfilters.c`: регистрация
Это **один commit** к FFmpeg-репо. Возможен upstream PR (надеемся примут;
прецедент есть — Nvidia donated `scale_cuda` и др.).
#### Уровень 2 — `libcuframes` runtime
Stand-alone shared library. Зависимости:
- `libcuda.so.1` (NVIDIA driver, и так у всех)
- `cudart` (CUDA runtime)
- Linux только (POSIX SHM, eventfd, AF_UNIX)
#### Уровень 3 — упаковка
- **Debian/Ubuntu**: `.deb` через `dpkg-buildpackage`
- **Arch**: AUR package
- **Docker**: pre-built image `ghcr.io/<org>/cuframes-ffmpeg:N` — drop-in
замена `/usr/lib/ffmpeg/N/bin/ffmpeg` в Frigate image
- **Frigate-specific image**: `ghcr.io/<org>/cuframes-frigate:0.17`
Frigate stable-tensorrt с pre-installed cuframes ffmpeg
## 3. Use cases для документации
### 3.1 Наш кейс — `cctv-companion` (mosaic)
```
Frigate (ffmpeg с cuda_ipc_export filter):
Camera 1..16 → decode CUDA → publish key=cam_N
↘ свой Frigate pipeline (detect, record)
cctv-companion:
- подписан на cam_1..cam_16 через cuframes
- подписан на frigate/events через MQTT
- decision logic: какую сетку показать (full/2x2/4x4)
- composite через CUDA-ядра (CUB / OpenCV CUDA / custom kernels)
- encode → RTSP → TV
```
### 3.2 Multi-AI на одном decode
```
ffmpeg decode → publish key=cam_main
Subscriber A: YOLOv8 object detection
Subscriber B: face recognition (ArcFace)
Subscriber C: license plate OCR
Subscriber D: motion-triggered cloud upload
```
Все читают **тот же** decoded frame. Один NVDEC operation вместо четырёх.
### 3.3 Замена RAM-based mmstreams
Существующие custom-решения часто строят бypass через FIFO/pipe в RAM с
encoding между процессами. cuframes делает это zero-copy на CUDA.
## 4. План работ
### Phase 0 — Spike (3 дня)
- [ ] Прототип producer + consumer без FFmpeg: голый CUDA IPC + Unix socket
- [ ] Замерить bandwidth, latency на RTX 5090
- [ ] Verify cross-container (docker → docker) и host → docker
- [ ] Решить: sync через `cudaStreamSynchronize` или CUDA IPC events
- **Deliverable:** `tools/spike/pingpong.cpp` + измерения
### Phase 1 — libcuframes (1 неделя)
- [ ] Producer C API
- [ ] Consumer C API
- [ ] Wire-protocol implementation
- [ ] Unit tests (pingpong, multi-consumer, crash-recovery)
- [ ] Stress test rig (24h-runner)
- **Deliverable:** Self-contained library + tests passing
### Phase 2 — FFmpeg filter (1-2 недели)
- [ ] `vf_cuda_ipc_export.c` в стиле FFmpeg
- [ ] Patch script для применения к FFmpeg N.x (4.4, 5.x, 6.x, 7.x)
- [ ] Build на CUDA 12.0+ matrix
- [ ] Integration test: `ffmpeg → cuframes filter → consumer` end-to-end
- **Deliverable:** Patched FFmpeg builds (Ubuntu 22.04, 24.04, in Docker)
### Phase 3 — Bindings (1 неделя)
- [ ] C++ RAII wrapper
- [ ] Python binding через pybind11
- [ ] PyTorch tensor interop (DLPack)
- **Deliverable:** `pip install cuframes` (через `pyproject.toml`)
### Phase 4 — Docker / packaging (3-5 дней)
- [ ] `ghcr.io/<org>/cuframes-ffmpeg:N.x-cuda12.X`
- [ ] `ghcr.io/<org>/cuframes-frigate:0.17` (Frigate с patched ffmpeg)
- [ ] `.deb` packages для Ubuntu 22.04/24.04
- [ ] CI matrix: build всех вариантов на каждый push
- **Deliverable:** `docker pull` ready-to-use
### Phase 5 — Naш use case: cctv-companion (1 неделя)
- [ ] Минимальный C++ daemon
- [ ] Subscribe на N камер
- [ ] MQTT subscriber на Frigate events
- [ ] Smart-grid logic (existing в проекте, переиспользовать)
- [ ] Mosaic compositor на CUDA (cv::cuda::warpAffine для placement)
- [ ] Output через `nvenc` → RTSP сервер для TV
- **Deliverable:** Working setup на R9 со всеми 16 камерами
### Phase 6 — OSS launch (3-5 дней)
- [ ] README с GIF (visual что это даёт)
- [ ] Quickstart (5 минут от `docker pull` до working demo)
- [ ] Benchmarks page с numbers
- [ ] HackerNews / r/selfhosted / r/homeassistant анонс
- [ ] Submit upstream PR в FFmpeg
**Итого: ~6-8 недель** для одного разработчика, **3-4 недели** для двоих.
## 5. Acceptance criteria для v1.0
- [ ] FFmpeg-filter работает на 4.4, 5.x, 6.x, 7.x (Frigate diversity)
- [ ] Latency producer→consumer ≤ 1 ms (p99)
- [ ] 16 cameras × 4 consumers × 25 fps без drops 24h
- [ ] Frigate (stable-tensorrt) drop-in replacement работает
- [ ] PyTorch consumer: `frame.tensor` без копий на CUDA
- [ ] Docker images < 4 GB сжатые
- [ ] CI green на Ubuntu 22.04, 24.04
- [ ] `pip install cuframes` works на Python 3.10/3.11/3.12
- [ ] README с benchmark vs naive double-decode
## 6. Open questions для discussion
1. **Name.** `cuframes` рабочий, варианты: `vramshare`, `cudapipe`,
`nvframe-bus`, `cuda-frame-bridge`, `framesink`. Финализируем перед public.
2. **License.** LGPL-2.1+ для совместимости с FFmpeg LGPL и spirit (linkable
с MIT/Apache). Sourcing — на LGPL.
3. **Upstream FFmpeg PR vs custom build.** Best-effort вклад в FFmpeg, но не
блокер — distrubution через own Docker images работает независимо.
4. **AMD/ROCm support.** Out of scope для v1. После v1 — если будет
community-interest, добавить через HIP IPC.
5. **Multi-GPU.** Producer и consumer на разных CUDA devices — CUDA IPC
ограничивает same device. В v1 — ограничение, документировать.
6. **Sync semantics.****РЕШЕНО** (architectural review 2026-05-15 + spike-v2):
Default — **CUDA IPC events** (`cudaIpcEventHandle_t`). Rationale:
- NVIDIA Programming Guide §3.2.8 явно требует cross-process sync на стороне
application; `cudaStreamSynchronize` producer'а гарантий не даёт contractually.
- Spike-v2 measurements (`docs/measurements/spike-v2/`) показали: на нашем
single-host single-GPU setup stream-only sync **работает** (0 torn frames
на 1500 frames × 4 scenarios), но это **happens-to-work**, не contract.
- Events добавляют mean +20µs overhead но дают **predictable tail latency**
(max 5.2ms vs 14.7ms для FHD@60fps) и future-proof для multi-GPU.
- API option `--sync=stream` оставлен как fallback для legacy / debugging.
7. **Где хостить.** GitHub `<personal-org>/cuframes`. Возможно перенос в
`frigate-community/cuframes` после adoption.
## 7. Риски
| Риск | Митигация |
|---|---|
| Upstream FFmpeg PR не примут | Distributable через custom builds, не блокирует release |
| CUDA IPC subtle bugs | Phase 0 spike до commit'а; Stress test 24h |
| Frigate не пропустит custom filter | `frigate-cuframes` docker image — pre-patched, готов к использованию |
| Conflict с NVIDIA DeepStream APIs | None expected (разные scope) |
| Community не подхватит | Это secondary — primary value для нас (cctv-companion работает) |
## 8. Зависимости (build-time)
- CUDA Toolkit ≥ 12.0
- FFmpeg sources (target versions 4.4, 5.x, 6.x, 7.x)
- pkg-config, autoconf, make
- Optional: OpenCV CUDA (для C++ examples)
- Optional: pybind11 (для Python binding)
## 9. Связь с этим репозиторием (cctv)
Текущий `cpp/apps/cctv-processor` — full-featured C++ NVR с своим decode +
mosaic + RTSP-server. После v1 cuframes:
- `cuframes` выпускается **как отдельный** opensource-репо
(`/home/claude/projects/cuframes/`)
- В `cctv` создаётся новый `cpp/apps/cctv-companion/` — тонкий клиент к
Frigate + cuframes (не Replaces existing processor, а соседствует)
- Существующий cctv-processor — оставить как baseline / fallback для
setups без Frigate
## 10. Готовность к next step
После approval спеки:
1. `git init /home/claude/projects/cuframes/` — пустой OSS-репо
2. Phase 0 spike — 3 дня PoC
3. После Phase 0 — review результатов, корректировка дизайна (если CUDA IPC
повёл себя не как ожидали)
4. Phase 1+ по плану
---
# Appendix A — Production deployment notes (post-v0.1.0)
Реальные наблюдения после первого production deployment (Frigate + cctv-processor
на RTX 5090, 24h+ uptime). Обновляется по мере накопления опыта.
## Что подтвердилось из изначального дизайна
- **CUDA IPC handshake через cudaIpcEventHandle_t работает стабильно** — нет
ни одного torn frame за 24+ часов на 2 consumer'ах.
- **EXTERNAL ownership** (publisher передаёт свои pre-allocated CUDA pointers)
необходим для FFmpeg-based publisher — иначе нужен extra cudaMemcpy из FFmpeg's
hwframe pool в library-managed pool.
- **Unix socket handshake** ОК — простой, debug'абельный (`socat` для inspect).
- **POSIX shm для header + atomic seq counters** — race-free на reader side.
## Что пришлось доработать в v0.1.0 vs initial design
- **CMake install rules** изначально не предусмотрены. Downstream проекты
делали `cmake --install` → пустой prefix. Fix: `install(TARGETS ...)` +
`install(DIRECTORY include/cuframes ...)`. Лессон — install rules должны
быть в day 1.
- **Variable HINTS в find_library**: пользователи делают install в разные
prefix'ы. HINTS для downstream `find_library(cuframes)` должны включать
`$PREFIX/lib`, `$PREFIX/lib64`, и `build-dir/libcuframes/` для local-dev.
## Что не учли в дизайне (открытые grабли — см. troubleshooting.md)
### Cross-container CUDA IPC требует **shared pid + ipc namespace**
`cudaIpcOpenEventHandle` validates IPC peer через `/proc/<pid>/...`. Если
consumer container не в same PID namespace что publisher — fail с
`invalid device context`.
Это **incompatible** с s6-overlay-based containers (linuxserver.io stack,
Frigate), требующими PID 1 для self. Workaround: только `ipc:` shared,
accept race window (works на Frigate в практике потому что подключается
первым после publisher restart). **Real fix planned v0.2**: socket-based
context validation вместо `/proc` reliance.
### Publisher-side resize нужен для consumers без cuda-llvm
Большинство downstream FFmpeg builds — без `--enable-cuda-llvm` (на платформах
с glibc < 2.38 эта опция не собирается, нужен `stdbit.h`). Без cuda-llvm нет
`scale_cuda` filter. Consumer вынужден CPU-resize либо отключать hwaccel.
**Fix planned v0.2**: publisher принимает `--scale=WxH` и делает GPU resize
до publish. Consumer получает уже scaled frames, scale_cuda не нужен.
### Encoded packet sharing — отсутствует в v0.1
cuframes v0.1 раздаёт **только decoded** NV12. Для `record` use case
(`-c:v copy` mux без decode) consumer всё ещё открывает свой RTSP — лимит
камеры на concurrent streams (4-5 у Dahua) hit'ится.
**v0.2 spec**: parallel encoded-packets ring + `cuframes_packets://`
demuxer. См. [issue #2](https://git.goldix.org/gx/cuframes/issues/2).
## Production setup (gold path)
```
┌─► Frigate (FFmpeg cuframes:// demuxer) → detect
Camera RTSP ─► publisher ──┤
(1× NVDEC) └─► cctv-processor (CuframesSource C++ API) → motion+RTSP-encode→TV
```
| Метрика | Without cuframes (baseline) | С cuframes v0.1 |
|---|---|---|
| NVDEC operations на parking-камеру | 2 (Frigate detect + cctv detect) | **1** (publisher) |
| VRAM extra cost | 0 (каждый своё) | ~3 MB (ring 6×460KB sub-stream) |
| RTSP camera load | 2 streams | **1** stream |
| Uptime (verified) | n/a | 24h+ без drops |
## См. также
- [docs/troubleshooting.md](troubleshooting.md) — конкретные грабли + fixes
- [BENCHMARKS.md](../BENCHMARKS.md) — измерения
- [docs/integrations/frigate.md](integrations/frigate.md) — guide для Frigate
- [ROADMAP.md](../ROADMAP.md) — v0.2/v0.3/v1.0