From e2603174724e2517a702030e91080d61ae68f434 Mon Sep 17 00:00:00 2001 From: Filip Jeretina <59307111+zrezke@users.noreply.github.com> Date: Tue, 20 Jun 2023 03:46:43 +0200 Subject: [PATCH] Prerelease 0.0.8 alpha.0 (#20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * v0.0.1 of DepthaiViewer, WIP (#1) * node graph scaffolding and a bit of depthai integration * remove import * left, right + depth * fix fps sliders for mono cams * partial refactor, partially added support for device selection. * pc support + currently selected device fixes * reafactor subscriptions to api, doesn't work too well, trouble with syncing the ui with the backend * Partially migrated to websockets for the api * finish moving to websockets for the config api * fix issues after merge, retry websocket connections * fix ws thread not exiting * sleep for 1 sec before trying to reconnect * Ai support + imu support + rotate camera + bug fixes (#9) * ai support + update pipeline on device select * quick fix depth * set subs when selecting device * rename to Depthai Viewer, use changed() for config ui, disable ui when device not selected * added age gender detection * added mobilenet support * pointcloud support * Toggle subs from visible (#4) * very bad implementation of toggling subscriptions based on space view visibility TODO: less cloning and stuff * better implementation and actually seems to work * fix windows not resizing properly when size gets smaller * fix issues with 3d when resizing scene bbox, only force 2d to always have the size of self.scene_bbox * Nuked Subscriptions struct, just use Vec, much nicer, much better aswell, also fixed subscriptions for channels that need some time to actually show up, it all feels a bit hacky tho * Toggle subs from visible (#5) * very bad implementation of toggling subscriptions based on space view visibility TODO: less cloning and stuff * better implementation and actually seems to work * fix windows not resizing properly when size gets smaller * fix issues with 3d when resizing scene bbox, only force 2d to always have the size of self.scene_bbox * Nuked Subscriptions struct, just use Vec, much nicer, much better aswell, also fixed subscriptions for channels that need some time to actually show up, it all feels a bit hacky tho * fix incorrect despawn of entity paths * remove todo comment * Fix ws not sending messages, fix stutter every 2s (#6) * very bad implementation of toggling subscriptions based on space view visibility TODO: less cloning and stuff * better implementation and actually seems to work * fix windows not resizing properly when size gets smaller * fix issues with 3d when resizing scene bbox, only force 2d to always have the size of self.scene_bbox * Nuked Subscriptions struct, just use Vec, much nicer, much better aswell, also fixed subscriptions for channels that need some time to actually show up, it all feels a bit hacky tho * fix incorrect despawn of entity paths * remove todo comment * error handling * fix websocket not sending pipeline, hotfix more or less might keep * change back to unbounded * Imu support (#8) * add imu logging, exclude it from regular space views * fix imu logging * POC imu plotting * Imu accelerometer and gyroscope plotting * add orientation to imu log, make device id a String * fix imu charts layout * imu log add magnetometer * fix right panel ui as much as it makes sense to fix rn * sadly idk how to create Option::None in pyarrow for the magnetometer field * imu logging fixed, magnetometer value is now None when not logged * fix plot scrolling * Merge upstream (#10) * `arrow2_convert` primitive (de)serialization benchmarks (#1742) * arrow2_convert primitive benchmarks * addressing PR comments * Fix logged obb being displayed with half of the requested size (#1749) * benchmarks for common vector ops across `smallvec`/`tinyvec`/std (#1747) * benchmarks for common vector ops * handle N=1 * Tracked 3D cameras lead now to on-hover rays in other space views that show the same camera but don't track it. (#1751) In the same way as a 2D scene causes a on-hover ray in all space views that contain the space camera at which the 2D view "sits". * Improve dealing with raw buffers for texture read/write (#1744) * Replace TextureRowDataInfo with the more versatile Texture2DBufferInfo * comment & naming fixes * `arrow2` erased refcounted clones benchmarks (#1745) * arrow2 erased refcounted clone benchmarks * lint * addressing PR comments * dude * `arrow2` estimated_bytes_size benchmarks (#1743) * arrow2 estimated_bytes_size benchmarks * cleanup * Fix crash when trying to do picking on depth clouds * Readback depth from GPU picking (#1752) * gpu picking in the viewer picks up depth now * WebGL workarounds * Add new ARKitScenes example (#1538) Co-authored-by: Nikolaus West Co-authored-by: Emil Ernerfeldt * Fix log_obb usage (#1761) * Make sure all log_obb uses uses half_size correctly * Remove outdated link from README * Fix docstring of save * Force named arguments of log_scalar * Add link to --memory-limit docs * update ros example * Python SDK: document that we also accept colors in 0-1 floats (#1740) * Python SDK: document that we also accept colors in 0-1 floats * Assume float colors to be in gamma-space, and document that * Update arkitscenes example * Fix bug * typo * py-format * Collapse space-view by default if there is only one child (#1762) * Always create the log_time timeline (#1763) * Columnar timepoints in data tables and during transport (#1767) * columnar timepoints * self review * Fix undo/redo selection shortcut/action changing selection history without changing selection (#1765) * Fix undo/redo selection shortcut/action changing selection history without changing selection Fixes #1172 * typo fix * Don't initialize an SDK session if we are only going to be launching the app (#1768) * Allow torch tensors for log_rigid3 (#1769) * Option to show scene bounding box (#1770) * Include depth clouds in bounding box calculation * Don't wrap text when showing bbox in ui * Handle projective transforms * Nicer selection view: don't wrap second column too early * Add checkbox to show the scene bounding box * Fix a whole lot of crashes, all at once (#1780) * Add typing_extensions to requirements-doc.txt (#1786) * auto_color class-ids if they are present (#1783) * auto_color class-ids if they are present * Update log line in segmentation demo * Avoid tuple structs * Don't run 3rd party bench suites on CI (#1787) * dont run 3rd party bench suites on CI * typo * and other annoyances * Use copilot markers in PR template (#1784) * Use copilot markers in PR template * remove poem Co-authored-by: Clement Rey --------- Co-authored-by: Clement Rey * re_format: barebone support for custom formatting (#1776) * implement barebone support for custom formatting and apply to Tuid * unwrap * rather than [] * use re_tuid * Always send recording_id as part of LogMsg (#1778) * Always send recording_id as part of LogMsg * Rename build_chunk_from_components -> build_data_table_from_components * Don't make RecordingInfo optional * Always default the recording id * Log an error if we hit the initialization issue * Refactor: Add new helper crate `re_log_encoding` (#1772) * CI: Check `rerun` with --no-default features and/or with --features sdk * Create a new helper crate re_transport containing stream_rrd_from_http * Fix warnings * Move file sink to re_transport * wasm compilation fix * Move LogMsg encoding/decoding into re_transport * Fix typo * Fix web build * Fix tests * Remove a lot of unused dependencies with `cargo machete` * Build fix * Clarify * Rename the crate to re_log_encoding * better docstring Co-authored-by: Jeremy Leibs * better readme Co-authored-by: Jeremy Leibs --------- Co-authored-by: Jeremy Leibs * New example code for facebook research segment anything (#1788) * New example code for facebook research segment anything * Add segmentation workaround for users still on 0.4.0 * Images should use class-id as label * Add an alternative tensor-based view * Implement `re_tuid::Tuid::random()` on web (#1796) * Implement `re_tuid::Tuid::random()` on web * Fix bad error message * ci: fix benchmarks (#1799) * workflow: just run --all * datastore: skip bucket permutations etc on CI * i give up, just replace re_log_types by re_log_encoding * Add `minimal_options` example (`RerunArgs`) (#1773) * Allows connecting to remote server through rerun's RerunArgs. Co-authored-by: Clement Rey * Add `pacman` support to `setup_web.sh` (#1797) Co-authored-by: Clement Rey * fix ci (cargo-deny): cargo update -p crossbeam-channel (#1806) * Compile with `panic = "abort"` (#1813) * Compile with `panic = "abort"` This PR sets `panic = "abort"` for both debug and release builds. This cuts down the `rerun` binary size in release builds from 29.9 MB to 22.7 MB - a 25% reduction! ## Details The default panic behavior in Rust is to unwind the stack. This leads to a lot of extra code bloat, and some missed opportunities for optimization. The benefit is that one can let a thread die without crashing the whole application, and one can use `std::panic::catch_unwind` as a kind of try-catch block. We don't make use of these features at all (at least not intentionally), and so are paying a cost for something we don't need. I would also argue that a panic SHOULD lead to a hard crash unless you are building an Erlang-like robust actor system where you use defensive programming to protect against programmer errors (all panics are programmer errors - user errors should use `Result`). * Quiet clippy * Add `rerun --strict`: crash if any warning or error is logged (#1812) * Add `rerun --strict`: crash if any warning or error is logged Part of https://github.com/rerun-io/rerun/issues/1483 * Can't doc-test private functions * Refactor: Remove `TensorTrait` (#1819) * Refactor: Remove `TensorTrait` We don't need it anymore * End-to-end testing of python logging -> store ingestion (#1817) * Sort the arguments to `rerun` * Pass on `LogMsg::Goodbye` just like any other message * Add `rerun --test-receive` * `just py-build --quiet` is now possible * Add scripts/run_python_e2e_test.py * replace `cargo r -p rerun` with `python3 -m rerun` * lint and explain choice of examples * Add to CI * check returncode * Fix e2e test on CI: Don't try to re-build rerun-sdk (#1821) * Use gpu picking for points, streamline/share picking code some more (#1814) * use gpu picking for picking points * gpu based picking no longer works like a fallback but integrates with other picking sources * fix incorrect cursor rounding for picking * refactor picking context to be a pub struct with exposed state * unify ui picking method for 2d & 3d space views * less indentation for picking method * picking rect size is dynamically chosen * fix accidental z scaling in projection correction for picking & make cropped_projection_from_projection easier to read * CI: install pip requirements for Python e2e test * Process 2d points always in batches (#1820) * Fix CI syntax error * New option to disable persistent storage (#1825) * New option to disable persistent storage * New API to reset_time (#1826) * Datastore revamp 1: new indexing model & core datastructures (#1727) * Datastore revamp 2: serialization & formatting (#1735) * Datastore revamp 3: efficient incremental stats (#1739) * Datastore revamp 4: sunset `MsgId` (#1785) * Datastore revamp 5: (#1791) * Datastore revamp 6: sunset `LogMsg` storage + save store to disk (#1795) * Datastore revamp 7: garbage collection (#1801) * Don't assert if inserting a rowid with a matching timepoint does not create a conflict (#1832) * CI: Try installing the correct wheel on CI * CI: try again with the CI * re_query: up to date with latest data types and structures (#1828) * No more raw arrays for primary components * Don't need to carry around component names no more * Cluster keys are now raw-array-less and _not_ optional anymore * that is done indeed * helpers * datastore: incremental metadata registry stats (#1833) * add profile scopes for stats * implement incremental metadata registry stats * lint * future proofing comment * RFC: datastore state of the union & end-to-end batching (#1610) * add batching rfc * Update design/batching.md Co-authored-by: Emil Ernerfeldt * Update design/batching.md Co-authored-by: Emil Ernerfeldt * Update design/batching.md Co-authored-by: Emil Ernerfeldt * Update design/batching.md Co-authored-by: Emil Ernerfeldt * qa component instances vs. instance keys * no sticky * qa are there any special components & non-integer instance keys * give some clue about cell incompatibility * temporary constructs * zstd is already setup * more planning * date and links --------- Co-authored-by: Emil Ernerfeldt * Python CI: use bash as shell * Fix too many points crash (#1822) * Simplify point cloud builder and fix crash on too many points Fixes #1779 * faster point cloud population by not chaining iterators with default values * Use GPU picking for line(like) primitives, fix `interactive` flags (#1829) * line strip builder no longer has user data, exposes picking id instead (not implemented yet) * handle interactive object property when evaluating picking code * take line strip builder directly when building up line draw data * finish implementing picking for lines * remove unused iter_strips_with_vertices * Simplify picking handling now that there are a lot less types. Labels & textured rects are always picked now, fixes #1021 * CI: try to fix mac wheel build * Reduce memory used by staging belts on Web (#1836) In particular this prevents crashing with out of memory on a run-away belt memory usage caused by failure to unmap buffers. A bit concerningly, the fix uses our knowledge of how `wgpu::Device::poll` is broken in the current wgpu version. I took the opportunity to sharpens the definition of `HardwareTier` a bit. * CI: only test the x86_64 wheel on macos * Always flush when we remove a sink (#1830) Whenever we disconnect (or implicitly disconnect by swapping a sink) we should flush the pending messages. Additionally disconnect and flush calls both require releasing the GIL (for the same reason as shutdown previously). * GPU colormapping, first step (#1835) * Add TextureManager2D::get_or_create_with * Small code cleanup * Add code to upload a Tensor to a GPU texture * Add helper method Tensor::image_height_width_depth * Minor code cleanup (multiplicative_tint) * Hook up color textures via the new path * Refactor: introduce ColormappedTexture * Start working on an uint sampler * merge fix * Dumb colormapping of depth textures! * Use turbo for depth maps (and single-channel images :grimace:) * Use grayscale for luminance * ColorMap -> Colormap * Apply annotation context colormaps * Support sint textures too * cleanup * merge fix * Fix RGB images * More cleanup * Better error-handlign and nicer error message * Clean up the SAMPLE_TYPE with constants * Nicer shader interface * Better error handling * Remove dead code * Self-review cleanup * Fix bug in shader when sampling sint textures * Use textureSampleLevel * Apply a gamma to the image (unused as of now) * image_height_width_channels * fix various review comments * Optimize narrow_f64_to_f32s: avoid one allocation * Test and handle all tensor dtypes as images (#1840) * Support i64 and u64 tensors * api_demo: log all image types * Don't even print out the contents of a tensor * Handle unfilterable float textures * fix typo * py-format * Simplify is_float_filterable * Add a helper function pad_and_narrow_and_cast * Reuse existing image * Exclude image_tensors demo from default api_demo * Still run all api demos in e2e test * pyformat * Install the rerun-sdk in CI using --no-index and split out linux wheel build to run first. (#1838) * Install the rerun-sdk by the expected version * Fix comment * typo * Use --no-index when installing the rerun wheel * Use the cargo_version, not the new_version * Split dependency install into its own step * Don't use force-reinstall * Refactor setting of expected_version variable. * Use bash when setting env * Always run the linux job first and use its rrds for the other wheels * GPU tensor colormapping (#1841) * Refactor: introduce struct SliceSelection * Refactor: use SliceSelection inside of ViewTensorState * MVP of tensor colormapping on GPU * Remove old ui code * Support 64-bit tensors by narrowing to f32 * Allow more colormap options * Clippy * Report range errors instead of ignoring them * Sort colormaps * Shorten function name * Create module gpu_bridge * Move some code around * Simnplify API * Create ViewBuilder::new * Fix missing colon in lint.py * fix typos and formatting * Disable texture filtering options for tensors for now * Update docstrings * Add profile scopes * ViewBuilder cleanup * Make ViewBuilder::setup non-Option * Remove Result from thing that cannot fail * Fix colormap numbering * review cleanup * pass in debug_name * Unify the `range` function * typo * Show previews of colormaps when selecting them (#1846) * Make infallible version of get_or_create_texture * Show colormap previews in UI * Spelling * Implement billinear filtering of textures (#1850) * Implement opt-in billinear filtering of textures * bilinear * MVP Support for inline-rendering of Rerun within jupyter notebooks (#1798) (#1834) (#1844) * Introduce the ability to push an rrd binary via iframe.contentWindow.postMessage * New API to output the current buffered messages as a cell in jupyter * Example notebook with the cube demo * Track that we need to send another recording msg after draining the backlog * Dynamically resolve the app location based on git commit. Allow override to use self-hosted assets * Add some crude timeout logic in case the iframe fails to load * Don't persist app state in notebooks * Introduce new MemoryRecording for use with Jupyter notebooks (#1834) * Refactor the relationship between the assorted web / websocket servers (#1844) * Rename RemoteViewerServer to WebViewerSink * CLI arguments for specifying ports * Proper typing for the ports * Disable wheel tests for x86_64-apple-darwin (#1853) * Fix typos in notebook readme (#1852) * Fix the python build when running without web_viewer enabled (#1856) * Error instead of expect inside msg_encode. (#1857) * Restore: New API to reset_time (#1826) (#1854) * Revert "Implement billinear filtering of textures (#1850)" (#1859) This reverts commit d33dab6e7a33f82ab2513058d0f85744e3ce6ef4. * Add Restart command and keyboard shortcut for moving time to start of timeline (#1802) * Add Restart button to timeline UI. This sets the timeline back to zero. * Adds Restart Command & Timeline Command * Adds Ctrl-Shift-Space keyboard modifier * Remove restart button from timeline UI. * Use cmd(Key::LeftArrow) as key combo for restart timeline. * Add TimeControl::restart to restart the current timeline. * Use this from the kb_shortcut function * fix some code nits --------- Co-authored-by: Emil Ernerfeldt * Fix shutdown race condition in `re_sdk_comms` client (#1861) * Wait for encoder to shut down before shutting down the other threads * Remove unused dependencies (#1863) * Gpu picking for depth clouds (#1849) * wip * allow for hovering depth clouds via gpu picking * Use `[x, y]: [u32; 2]` as argument --------- Co-authored-by: Emil Ernerfeldt * Remove manual depth projection from car and nyud examples (#1869) * Remove manual depth projection from car and nyud examples * revert change to cube.ipynb * revert changes to cube.ipynb * third times a charm for cube.ipynb * Improve end-to-end testing slightly (#1862) * CI: Run e2e tests with RUST_LOG=debug * Move installing of pip packaged from CI to e2e script * Re-enable bilinear interpolation again (#1860) * Revert "Revert "Implement billinear filtering of textures (#1850)" (#1859)" This reverts commit 625d2bdd241c09ff9d0ae394ba91565fa48455ec. * Split rectangle.wgsl into fragme/vertex parts to work around naga bug * Use GPU colormapping when showing images in the GUI (#1865) * Cleanup: move Default close to the struct definition * Simplify code: use if-let-else-return * Simplify code: no need for Arc * Add EntityDataUi so that the Tensor ui function knows entity path * Better naming: selection -> item * Simplify code: no optional tensor stats * Less use of anyhow * Use GPU colormapping when showing tensors in GUI * Link to issue * Optimize pad_to_four_elements for debug builds * Refactor: simpler arguments to show_zoomed_image_region_area_outline * Fix missing meter argument * Refactor: break up long function * Less use of Arc * Pipe annotation context to the hover preview * Simplify `AnnotationMap::find` * Use new GPU colormapper for the hover-zoom-in tooltip * Refactor * Add helper function for turning a Tensor into an image::DynamicImage * Fix warning on web builds * Add helper function `Tensor::could_be_dynamic_image` * Implement click-to-copy and click-to-save for tensors without egui * Convert histogram to the new system * Remove the TensorImageCache * Fix TODO formatting * bug fixes and cleanups * Rename some stuff * Build-fix * Simplify some code * Turn off benchmakrs comment on each PR (#1872) * Refactor: remove `GpuTexture2DHandle::invalid` (#1866) * Refactor TexturedRect * Remove GpuTexture2DHandle::invalid * `GpuTexture2DHandle` is always valid * spacing * Update enumflags2 to non-yanked version (#1874) * Update enumflags2 to non-yanked version ``` ❯ cargo update -p enumflags2 Updating crates.io index Updating enumflags2 v0.7.5 -> v0.7.7 Updating enumflags2_derive v0.7.4 -> v0.7.7 Updating proc-macro2 v1.0.47 -> v1.0.56 Updating quote v1.0.21 -> v1.0.26 Adding syn v2.0.15 ``` Unfortunately this adds the syn v2 dependency for some platforms * Updating dependencies is a valid label * cargo deny: check more platforms * fix stuff broken from merging upstream --------- Co-authored-by: Clement Rey Co-authored-by: benjamin de charmoy Co-authored-by: Andreas Reich Co-authored-by: Emil Ernerfeldt Co-authored-by: Pablo Vela Co-authored-by: Nikolaus West Co-authored-by: Jeremy Leibs Co-authored-by: h3mosphere <129932586+h3mosphere@users.noreply.github.com> Co-authored-by: Urho Laukkarinen * expose depth config, get available sensor resolutions for the selected device * move removing entities to a place where the removal will always be tried, not just when expanding space view header * added depth alignment * added stream enabled buttons * Depth cloud textures are now cached frame-to-frame (#1913) * Depth cloud textures are now cached frame-to-frame Simplified logic a bit by enforcing F32 texture conversion (there was a u16 path for native only) * doc fix * naming consistency, format check, remove unnecessary scaling * improve depth cloud texture check * fixes after merging * Smooth out scroll wheel input for camera zooming (#1920) * Always spawn instead of fork in multiprocessing example (#1922) * Add `--num-frames` arg to canny (webcam) example (#1923) * fix formatting issues * rerun format fix * fix spelling * lint check fixes * mypy * some more lint fixes * some more fixes for python lint checks * Collect extra egui features into the main Cargo.toml (#1926) * just rs-run-all * `just py-run-all-{native|web|rrd}` (#1927) * make all python examples handle unknown arguments gracefully * just py-run-all-{native|web|rrd} * bump version * comment out clang * Join threads at end of multi-threading example (#1934) * Add argument parsing to the rerun_demo (#1925) * More robust wait for exit condition during .serve() (#1939) * More robust wait for exit condition during .serve() * lint * Use zipfile python library instead of `unzip` command in arkitscene (#1936) * Use zipfile python library instead of `unzip` command in arkitscene Windows doesn't have unzip! * Nit: import sort order --------- Co-authored-by: Nikolaus West * Fix annotation images sometimes drawn in the background. (#1933) This caused fairly ugly rendering whenever that happened. Also cleaned up redundant image/textured_rect defintions in spatial scene buildup * Fix backslashes in arkitscene rigid transformation path (#1938) * Fix backslashes in arkitscene rigid transformation path Should be fixed properly by https://github.com/rerun-io/rerun/issues/1937 * Use PosixPath instead of .replace("\\", "/") --------- Co-authored-by: Nikolaus West * Fix hover/select highlights when picking single points in a scene with multiple point clouds (#1942) Batch vertex offset for single highlights wasn't correctly computed. Different parts of the code made different assumptions what offsets referred to * Fix hovering depth clouds (#1943) We didn't add to `scene.primitives.image`. Instead of adding to this list, it is instead now no longer needed for picking since we can very easily query for tensor again. * change python workflow for testing purposes, remove windows and macos wheels * add back one macos so the matrix is valid * 2.5GB before GC kick in on web (#1944) * change name to depthai-viewer * change pip install/uninstalls from rerun-sdk to depthai-viewer * change all occurances of rerun-sdk to depthai-viewer * change windows runner to windows-latest for now when using my personal gh * Release `0.5.0` (#1919) * changelog * 0.5.0-alpha.0 * more changelog * re_format: fix implicit recursive feature flags * publish_crates.sh: fix crate ordering * Join threads at end of multi-threading example (#1934) * Add argument parsing to the rerun_demo (#1925) * More robust wait for exit condition during .serve() (#1939) * More robust wait for exit condition during .serve() * lint * Use zipfile python library instead of `unzip` command in arkitscene (#1936) * Use zipfile python library instead of `unzip` command in arkitscene Windows doesn't have unzip! * Nit: import sort order --------- Co-authored-by: Nikolaus West * Fix annotation images sometimes drawn in the background. (#1933) This caused fairly ugly rendering whenever that happened. Also cleaned up redundant image/textured_rect defintions in spatial scene buildup * Fix backslashes in arkitscene rigid transformation path (#1938) * Fix backslashes in arkitscene rigid transformation path Should be fixed properly by https://github.com/rerun-io/rerun/issues/1937 * Use PosixPath instead of .replace("\\", "/") --------- Co-authored-by: Nikolaus West * changelog * Fix hover/select highlights when picking single points in a scene with multiple point clouds (#1942) Batch vertex offset for single highlights wasn't correctly computed. Different parts of the code made different assumptions what offsets referred to * changelog * Fix hovering depth clouds (#1943) We didn't add to `scene.primitives.image`. Instead of adding to this list, it is instead now no longer needed for picking since we can very easily query for tensor again. * changelog * 2.5GB before GC kick in on web (#1944) * changelog * 0.5.0 --------- Co-authored-by: Jeremy Leibs Co-authored-by: Andreas Reich Co-authored-by: Nikolaus West * Fix imu plots scrolling past their container * fix bottom/top panel sizing after showing the spinner on config setting * Lint error names in `map_err` (#1948) * Lint: Properly name errors in `map_err` * Use correct names for errors * fix config and stats tabs not being able to be viewed at the same time, renamed stats to IMU * New dispatch-only workflow for running the lint-job (#1950) * Fix secret in dispatch_lint.yml * Only maintain a single manual-dispatch job for testing workflows * apply button, have to make it look a bit nicer, but will do that when I fix scrolling in device configuration panel * Bump hyper version due to RUSTSEC-2023-0034 (#1951) * Add other build parameterizations to manual_dispatch.yml * Use proper if gates on the manual_dispatch.yml jobs * Add ability to save cache to manual_disaptch.yml * Standard case of inputs * Add manual step for packaging to 'manual_dispatch.yml' * add back panels when the underlying subscription reappears * Fix crash for missing class ids causing zero sized texture (#1947) * Fix crash for missing class ids causing zero sized texture Two things fixed actually: * texture manager now checks for zero sized texture, this ripples out in a lot more error handling * class id texture texture handles not having any classes gracefully * Use Display for all errors * typo * Better naming of error * Better docs and names * Fix off-by-one error * some use of `context`, change which error is implicitly converted on texture manager2d --------- Co-authored-by: Emil Ernerfeldt * fixes LR stream subscriptions (maybe breaks panels reappearing after sub is gone) (#15) * make config ui scrollable, fix padding * Move clippy_wasm/clippy.toml to under scripts (#1949) * Move clippy_wasm/clippy.toml to under scripts This is just to clean up the root a bit, and to move it closer to where it is actually used. * Fix comment typo --------- Co-authored-by: Andreas Reich * change crate version to 0.6.0-alpha.0 (#1952) * New workflow_dispatch for building wheels for a PR * initial light mode, luxonis depthai viewer * Rename build_wheels_for_pr.yml -> manual_build_wheels_for_pr.yml * Fix run-wasm crash on trying to wait for server (#1959) This ruined my dev experience for re_renderer examples a bit. Not sure what made the previous hack stop working, might be a timing issue. It ended up crashing the `cargo_run_wasm` web server * Update egui to latest and wgpu to 0.16 (#1958) * update to wgpu 0.16 and egui using this version * shader fixup for type aliases and rectangle shader * shader signed/unsigned shenanigans * more signed/unsigned issues * fix texture component count * fix picking layer depth readback crash on web * patch wgpu * better texture size estimate * fix patches * Handle leaking of prerelease into alpha version (#1953) * Make device config panel remember it's height throughout loading * hide time panel, make ai model dropdown wider * Fix incorrect memory usage stats for destroyed on-creation-mapped buffers (#1963) We actually don't have anywhere where we discard this kind of buffer yet, but if we would the stats would be wrong (noticed while doing quick & dirty experiments on the staging belt) * New manual workflow for running benches * Introduce new reusable workflow jobs and cleanup manual trigger (#1954) There are 8 reusable workflow "components" that we can use to build different scenarios: reusable_checks.yml - These are all the checks that run to ensure the code is formatted, reusable_bench.yml - This job runs the benchmarks to check for performance regressions. reusable_deploy_docs- This job deploys the python and rust documentation to https://ref.rerun.io reusable_build_and_test_wheels.yml - This job builds the wheels, runs the end-to-end test, and produces a sample RRD. The artifacts are accessible via GitHub artifacts, but not otherwise uploaded anywhere. reusable_upload_wheels.yml- This job uploads the wheels to google cloud reusable_build_web.yml - This job builds the wasm artifacts for the web. reusable_upload_web.yml - This job uploads the web assets to google cloud. By default this only uploads to: app.rerun.io/commit// reusable_pr_summary.yml - This job updates the PR summary with the results of the CI run. Example summary can be found at: https://storage.googleapis.com/rerun-builds/pull_request/1954/index.html This also introduces a manual_dispatch.yml helper as a convenience for testing these workflows and their different parameterizations. * New manual workflow for adhoc web builds * Use new CI workflows for pull-request and merge to main (#1955) on_pull_request.yml includes the following pieces: - reusable_checks.yml -- Run all of the lints, code-formatting, tests, etc. - reusable_build_and_test_wheels.yml -- Configured in a "minimal" mode with SDK includes end-to-end test and produces an rrd. - reusable_build_web.yml -- Verifies we can build the wasm - reusable_upload_web.yml -- Uploads the RRD and Wasm to app.rerun.io to confirm the demo works as well as support notebook testing. - reusable_pr_summary.yml -- Create a manifest page with a link to the on_push_main.yml includes the following pieces: - reusable_checks.yml -- Run all of the lints, code-formatting, tests, etc. - reusable_bench.yml -- Run the benchmarks - reusable_build_and_test_wheels.yml -- Builds wheels for all platforms - reusable_upload_wheel.yml -- Uploads the all the wheels to gcloud - reusable_build_web.yml -- Builds the wasm bundle - reusable_upload_web.yml -- Uploads the RRD and Wasm to app.rerun.io - reusable_pip_index.yml -- Generates a pip index page which can be used to install packages with, e.g. * Fix name of on_push_main.yml * Fix usage of long commit in generate_prerelease_pip_index.py * Try making pull-request workflows non-concurrent (#1970) * Try making pull-request workflows non-concurrent * Concurrency groups for push_main as well * Each sub-workflow needs its own name or they fight * Another attempt to make jobs non-concurrent on a per-PR basis (#1974) * Another attempt to make jobs non-concurrent on a per-PR basis * Move concurrency into the reusable job * Jobs with duplicated instances still need separate concurrency keys based on platform * Round to nearest color_index when doing color mapping (#1969) * Full (experimental) WebGPU support (#1965) * always build with unstable web sys apis * Make shader Tint friendly * expose webgl feature flag on re_renderer & re_viewer * fix bug link on negative hexadecimal * hardware tier is now created from wgpu adapter * sort out build flags for webgpu & document building webviewer * introduce shader text replacement workarounds to workaround current chrome issue * latest egui master * typo fix * doc fixes, use if cfg! instead of attribute cfg * move backend to rerun * If there's a `{{ pr-build-summary }}` in the PR description, update it. (#1971) * If there's a `{{ pr-build-summary }}` in the PR description, update it. * Add comment to the PR template * Add pull-requests permission to pr_summary job * Run the cube notebook on PR (#1972) * Run the cube notebook on PR * Add notebooks to the build summary * Use the new concurrency model * reformat py files * reformat * fix pylint errors * Add ability to manually run a web build to upload to an adhoc name (#1966) * Add ability to manually run a web build to upload to an adhoc name * Pass through ADHOC_NAME * Add a concurrency criteria for the new adhoc job * Make input description more explicit * change entity paths * merged wip albedo colormap into latest depth_cloud * remove pointclouds generated in sdk * fix compiler error, trying to compile frame.close for wasm * Default to albedo texture for depth cloud, added support for mono albedo textures * restart backend on failure, added oak_cam.device.close seems to really close the cam * py lint fix * don't run notebooks * remove run-notebook dependency --------- Co-authored-by: Clement Rey Co-authored-by: benjamin de charmoy Co-authored-by: Andreas Reich Co-authored-by: Emil Ernerfeldt Co-authored-by: Pablo Vela Co-authored-by: Nikolaus West Co-authored-by: Jeremy Leibs Co-authored-by: h3mosphere <129932586+h3mosphere@users.noreply.github.com> Co-authored-by: Urho Laukkarinen Co-authored-by: Nikolaus West * Fix rerun lints * rename binary, always start with memory-limit, pin depthai dependencies, fix errors after changing dependency versions * remove compiler warnings * fix memory leak when app is in background * Custom viewport UI (#4) * custom blueprint panel, show logical space views for depthai-viewer users, added settings clog on top of space view tab to configure what is visible * small fixes, clear entity_paths every time to avoid displaying an unavailable entity_path in space view options ui * custom left panel to add or remove space view instances, created a new default viewport layout. Improved behaviour when a panel re appears after user selected to hide it, then if stream stops and starts again the panel will be spawned back in correctly. * improve auto layout to not split when only 3D or 2D view is available * MJPEG encode image frames if connected to a PoE device. Only add magnetometer to imu sensors list if the device has a BNO IMU. Lower the memory limit to 100MB * Styling (#6) * initial styling impl * make buttons that should be small, small * Runtime depth config and fix device selection ui * comment * Proper runtime depth config updates * switch to Yolo v8 * add comma to label for non open zoo models * split 2d + 3d cam view vertically instead of horizontally * Tabify all panels (except for blueprint) (#7) * initial styling impl * make buttons that should be small, small * Runtime depth config and fix device selection ui * comment * Proper runtime depth config updates * switch to Yolo v8 * add comma to label for non open zoo models * split 2d + 3d cam view vertically instead of horizontally * Make the UI more configurable by converting the right panel into tabs. TODO: UX while laying out the panels. When a new space view appears only update the viewport layout, try to keep the user configured fixed function panels as they were. Just handle it in a way that is intuitive * remove bottom panel, switch to RAW imu sensors * XLink statistics initial implementation * initial xlink throughput statistics impllementation, have to glow it up a bit and maybe clean up the code * Xlink and rerun rename (#9) * Rename rerun py library to depthai_viewer * bug fixes and started working on a smart auto layout that operates on an existing tree, to preserve ui as much as possible, while also creating good layouts * auto layout * Fix maximize not working and add maximize for Stats tab * delete profiling stuff that shouldn't have been commited * mostly fix smart layout, TODO: detect when you can group mono and color 3d + 2d views into a 4 way split * add docstring for update_tree * WIP auto layout can_create_mono_quad checker, not at all finished yet * pass lint checks and bugfixes * forgot to sort imports * Fix mypy lint (TODO: Proper typing, especially in the comms from back to store to ws) other types are pretty solid * pylints and fix Queue typehinting * fix doc build * try to pass pylints with py.typed * ignore misc mypy errors * forgot to run black formatter * switch back to old ci * change version to 0.0.1 * format pyproject.toml * add back check_version to version_util.py * switch to normal runners * try cache-apt-pkgs-action@1.2.4 * sync mono camera settings * fix web build * Update scripts/version_util.py * Some fixes (#11) * v0.0.1 of DepthaiViewer, WIP (#1) * node graph scaffolding and a bit of depthai integration * remove import * left, right + depth * fix fps sliders for mono cams * partial refactor, partially added support for device selection. * pc support + currently selected device fixes * reafactor subscriptions to api, doesn't work too well, trouble with syncing the ui with the backend * Partially migrated to websockets for the api * finish moving to websockets for the config api * fix issues after merge, retry websocket connections * fix ws thread not exiting * sleep for 1 sec before trying to reconnect * Ai support + imu support + rotate camera + bug fixes (#9) * ai support + update pipeline on device select * quick fix depth * set subs when selecting device * rename to Depthai Viewer, use changed() for config ui, disable ui when device not selected * added age gender detection * added mobilenet support * pointcloud support * Toggle subs from visible (#4) * very bad implementation of toggling subscriptions based on space view visibility TODO: less cloning and stuff * better implementation and actually seems to work * fix windows not resizing properly when size gets smaller * fix issues with 3d when resizing scene bbox, only force 2d to always have the size of self.scene_bbox * Nuked Subscriptions struct, just use Vec, much nicer, much better aswell, also fixed subscriptions for channels that need some time to actually show up, it all feels a bit hacky tho * Toggle subs from visible (#5) * very bad implementation of toggling subscriptions based on space view visibility TODO: less cloning and stuff * better implementation and actually seems to work * fix windows not resizing properly when size gets smaller * fix issues with 3d when resizing scene bbox, only force 2d to always have the size of self.scene_bbox * Nuked Subscriptions struct, just use Vec, much nicer, much better aswell, also fixed subscriptions for channels that need some time to actually show up, it all feels a bit hacky tho * fix incorrect despawn of entity paths * remove todo comment * Fix ws not sending messages, fix stutter every 2s (#6) * very bad implementation of toggling subscriptions based on space view visibility TODO: less cloning and stuff * better implementation and actually seems to work * fix windows not resizing properly when size gets smaller * fix issues with 3d when resizing scene bbox, only force 2d to always have the size of self.scene_bbox * Nuked Subscriptions struct, just use Vec, much nicer, much better aswell, also fixed subscriptions for channels that need some time to actually show up, it all feels a bit hacky tho * fix incorrect despawn of entity paths * remove todo comment * error handling * fix websocket not sending pipeline, hotfix more or less might keep * change back to unbounded * Imu support (#8) * add imu logging, exclude it from regular space views * fix imu logging * POC imu plotting * Imu accelerometer and gyroscope plotting * add orientation to imu log, make device id a String * fix imu charts layout * imu log add magnetometer * fix right panel ui as much as it makes sense to fix rn * sadly idk how to create Option::None in pyarrow for the magnetometer field * imu logging fixed, magnetometer value is now None when not logged * fix plot scrolling * Merge upstream (#10) * `arrow2_convert` primitive (de)serialization benchmarks (#1742) * arrow2_convert primitive benchmarks * addressing PR comments * Fix logged obb being displayed with half of the requested size (#1749) * benchmarks for common vector ops across `smallvec`/`tinyvec`/std (#1747) * benchmarks for common vector ops * handle N=1 * Tracked 3D cameras lead now to on-hover rays in other space views that show the same camera but don't track it. (#1751) In the same way as a 2D scene causes a on-hover ray in all space views that contain the space camera at which the 2D view "sits". * Improve dealing with raw buffers for texture read/write (#1744) * Replace TextureRowDataInfo with the more versatile Texture2DBufferInfo * comment & naming fixes * `arrow2` erased refcounted clones benchmarks (#1745) * arrow2 erased refcounted clone benchmarks * lint * addressing PR comments * dude * `arrow2` estimated_bytes_size benchmarks (#1743) * arrow2 estimated_bytes_size benchmarks * cleanup * Fix crash when trying to do picking on depth clouds * Readback depth from GPU picking (#1752) * gpu picking in the viewer picks up depth now * WebGL workarounds * Add new ARKitScenes example (#1538) Co-authored-by: Nikolaus West Co-authored-by: Emil Ernerfeldt * Fix log_obb usage (#1761) * Make sure all log_obb uses uses half_size correctly * Remove outdated link from README * Fix docstring of save * Force named arguments of log_scalar * Add link to --memory-limit docs * update ros example * Python SDK: document that we also accept colors in 0-1 floats (#1740) * Python SDK: document that we also accept colors in 0-1 floats * Assume float colors to be in gamma-space, and document that * Update arkitscenes example * Fix bug * typo * py-format * Collapse space-view by default if there is only one child (#1762) * Always create the log_time timeline (#1763) * Columnar timepoints in data tables and during transport (#1767) * columnar timepoints * self review * Fix undo/redo selection shortcut/action changing selection history without changing selection (#1765) * Fix undo/redo selection shortcut/action changing selection history without changing selection Fixes #1172 * typo fix * Don't initialize an SDK session if we are only going to be launching the app (#1768) * Allow torch tensors for log_rigid3 (#1769) * Option to show scene bounding box (#1770) * Include depth clouds in bounding box calculation * Don't wrap text when showing bbox in ui * Handle projective transforms * Nicer selection view: don't wrap second column too early * Add checkbox to show the scene bounding box * Fix a whole lot of crashes, all at once (#1780) * Add typing_extensions to requirements-doc.txt (#1786) * auto_color class-ids if they are present (#1783) * auto_color class-ids if they are present * Update log line in segmentation demo * Avoid tuple structs * Don't run 3rd party bench suites on CI (#1787) * dont run 3rd party bench suites on CI * typo * and other annoyances * Use copilot markers in PR template (#1784) * Use copilot markers in PR template * remove poem Co-authored-by: Clement Rey --------- Co-authored-by: Clement Rey * re_format: barebone support for custom formatting (#1776) * implement barebone support for custom formatting and apply to Tuid * unwrap * rather than [] * use re_tuid * Always send recording_id as part of LogMsg (#1778) * Always send recording_id as part of LogMsg * Rename build_chunk_from_components -> build_data_table_from_components * Don't make RecordingInfo optional * Always default the recording id * Log an error if we hit the initialization issue * Refactor: Add new helper crate `re_log_encoding` (#1772) * CI: Check `rerun` with --no-default features and/or with --features sdk * Create a new helper crate re_transport containing stream_rrd_from_http * Fix warnings * Move file sink to re_transport * wasm compilation fix * Move LogMsg encoding/decoding into re_transport * Fix typo * Fix web build * Fix tests * Remove a lot of unused dependencies with `cargo machete` * Build fix * Clarify * Rename the crate to re_log_encoding * better docstring Co-authored-by: Jeremy Leibs * better readme Co-authored-by: Jeremy Leibs --------- Co-authored-by: Jeremy Leibs * New example code for facebook research segment anything (#1788) * New example code for facebook research segment anything * Add segmentation workaround for users still on 0.4.0 * Images should use class-id as label * Add an alternative tensor-based view * Implement `re_tuid::Tuid::random()` on web (#1796) * Implement `re_tuid::Tuid::random()` on web * Fix bad error message * ci: fix benchmarks (#1799) * workflow: just run --all * datastore: skip bucket permutations etc on CI * i give up, just replace re_log_types by re_log_encoding * Add `minimal_options` example (`RerunArgs`) (#1773) * Allows connecting to remote server through rerun's RerunArgs. Co-authored-by: Clement Rey * Add `pacman` support to `setup_web.sh` (#1797) Co-authored-by: Clement Rey * fix ci (cargo-deny): cargo update -p crossbeam-channel (#1806) * Compile with `panic = "abort"` (#1813) * Compile with `panic = "abort"` This PR sets `panic = "abort"` for both debug and release builds. This cuts down the `rerun` binary size in release builds from 29.9 MB to 22.7 MB - a 25% reduction! ## Details The default panic behavior in Rust is to unwind the stack. This leads to a lot of extra code bloat, and some missed opportunities for optimization. The benefit is that one can let a thread die without crashing the whole application, and one can use `std::panic::catch_unwind` as a kind of try-catch block. We don't make use of these features at all (at least not intentionally), and so are paying a cost for something we don't need. I would also argue that a panic SHOULD lead to a hard crash unless you are building an Erlang-like robust actor system where you use defensive programming to protect against programmer errors (all panics are programmer errors - user errors should use `Result`). * Quiet clippy * Add `rerun --strict`: crash if any warning or error is logged (#1812) * Add `rerun --strict`: crash if any warning or error is logged Part of https://github.com/rerun-io/rerun/issues/1483 * Can't doc-test private functions * Refactor: Remove `TensorTrait` (#1819) * Refactor: Remove `TensorTrait` We don't need it anymore * End-to-end testing of python logging -> store ingestion (#1817) * Sort the arguments to `rerun` * Pass on `LogMsg::Goodbye` just like any other message * Add `rerun --test-receive` * `just py-build --quiet` is now possible * Add scripts/run_python_e2e_test.py * replace `cargo r -p rerun` with `python3 -m rerun` * lint and explain choice of examples * Add to CI * check returncode * Fix e2e test on CI: Don't try to re-build rerun-sdk (#1821) * Use gpu picking for points, streamline/share picking code some more (#1814) * use gpu picking for picking points * gpu based picking no longer works like a fallback but integrates with other picking sources * fix incorrect cursor rounding for picking * refactor picking context to be a pub struct with exposed state * unify ui picking method for 2d & 3d space views * less indentation for picking method * picking rect size is dynamically chosen * fix accidental z scaling in projection correction for picking & make cropped_projection_from_projection easier to read * CI: install pip requirements for Python e2e test * Process 2d points always in batches (#1820) * Fix CI syntax error * New option to disable persistent storage (#1825) * New option to disable persistent storage * New API to reset_time (#1826) * Datastore revamp 1: new indexing model & core datastructures (#1727) * Datastore revamp 2: serialization & formatting (#1735) * Datastore revamp 3: efficient incremental stats (#1739) * Datastore revamp 4: sunset `MsgId` (#1785) * Datastore revamp 5: (#1791) * Datastore revamp 6: sunset `LogMsg` storage + save store to disk (#1795) * Datastore revamp 7: garbage collection (#1801) * Don't assert if inserting a rowid with a matching timepoint does not create a conflict (#1832) * CI: Try installing the correct wheel on CI * CI: try again with the CI * re_query: up to date with latest data types and structures (#1828) * No more raw arrays for primary components * Don't need to carry around component names no more * Cluster keys are now raw-array-less and _not_ optional anymore * that is done indeed * helpers * datastore: incremental metadata registry stats (#1833) * add profile scopes for stats * implement incremental metadata registry stats * lint * future proofing comment * RFC: datastore state of the union & end-to-end batching (#1610) * add batching rfc * Update design/batching.md Co-authored-by: Emil Ernerfeldt * Update design/batching.md Co-authored-by: Emil Ernerfeldt * Update design/batching.md Co-authored-by: Emil Ernerfeldt * Update design/batching.md Co-authored-by: Emil Ernerfeldt * qa component instances vs. instance keys * no sticky * qa are there any special components & non-integer instance keys * give some clue about cell incompatibility * temporary constructs * zstd is already setup * more planning * date and links --------- Co-authored-by: Emil Ernerfeldt * Python CI: use bash as shell * Fix too many points crash (#1822) * Simplify point cloud builder and fix crash on too many points Fixes #1779 * faster point cloud population by not chaining iterators with default values * Use GPU picking for line(like) primitives, fix `interactive` flags (#1829) * line strip builder no longer has user data, exposes picking id instead (not implemented yet) * handle interactive object property when evaluating picking code * take line strip builder directly when building up line draw data * finish implementing picking for lines * remove unused iter_strips_with_vertices * Simplify picking handling now that there are a lot less types. Labels & textured rects are always picked now, fixes #1021 * CI: try to fix mac wheel build * Reduce memory used by staging belts on Web (#1836) In particular this prevents crashing with out of memory on a run-away belt memory usage caused by failure to unmap buffers. A bit concerningly, the fix uses our knowledge of how `wgpu::Device::poll` is broken in the current wgpu version. I took the opportunity to sharpens the definition of `HardwareTier` a bit. * CI: only test the x86_64 wheel on macos * Always flush when we remove a sink (#1830) Whenever we disconnect (or implicitly disconnect by swapping a sink) we should flush the pending messages. Additionally disconnect and flush calls both require releasing the GIL (for the same reason as shutdown previously). * GPU colormapping, first step (#1835) * Add TextureManager2D::get_or_create_with * Small code cleanup * Add code to upload a Tensor to a GPU texture * Add helper method Tensor::image_height_width_depth * Minor code cleanup (multiplicative_tint) * Hook up color textures via the new path * Refactor: introduce ColormappedTexture * Start working on an uint sampler * merge fix * Dumb colormapping of depth textures! * Use turbo for depth maps (and single-channel images :grimace:) * Use grayscale for luminance * ColorMap -> Colormap * Apply annotation context colormaps * Support sint textures too * cleanup * merge fix * Fix RGB images * More cleanup * Better error-handlign and nicer error message * Clean up the SAMPLE_TYPE with constants * Nicer shader interface * Better error handling * Remove dead code * Self-review cleanup * Fix bug in shader when sampling sint textures * Use textureSampleLevel * Apply a gamma to the image (unused as of now) * image_height_width_channels * fix various review comments * Optimize narrow_f64_to_f32s: avoid one allocation * Test and handle all tensor dtypes as images (#1840) * Support i64 and u64 tensors * api_demo: log all image types * Don't even print out the contents of a tensor * Handle unfilterable float textures * fix typo * py-format * Simplify is_float_filterable * Add a helper function pad_and_narrow_and_cast * Reuse existing image * Exclude image_tensors demo from default api_demo * Still run all api demos in e2e test * pyformat * Install the rerun-sdk in CI using --no-index and split out linux wheel build to run first. (#1838) * Install the rerun-sdk by the expected version * Fix comment * typo * Use --no-index when installing the rerun wheel * Use the cargo_version, not the new_version * Split dependency install into its own step * Don't use force-reinstall * Refactor setting of expected_version variable. * Use bash when setting env * Always run the linux job first and use its rrds for the other wheels * GPU tensor colormapping (#1841) * Refactor: introduce struct SliceSelection * Refactor: use SliceSelection inside of ViewTensorState * MVP of tensor colormapping on GPU * Remove old ui code * Support 64-bit tensors by narrowing to f32 * Allow more colormap options * Clippy * Report range errors instead of ignoring them * Sort colormaps * Shorten function name * Create module gpu_bridge * Move some code around * Simnplify API * Create ViewBuilder::new * Fix missing colon in lint.py * fix typos and formatting * Disable texture filtering options for tensors for now * Update docstrings * Add profile scopes * ViewBuilder cleanup * Make ViewBuilder::setup non-Option * Remove Result from thing that cannot fail * Fix colormap numbering * review cleanup * pass in debug_name * Unify the `range` function * typo * Show previews of colormaps when selecting them (#1846) * Make infallible version of get_or_create_texture * Show colormap previews in UI * Spelling * Implement billinear filtering of textures (#1850) * Implement opt-in billinear filtering of textures * bilinear * MVP Support for inline-rendering of Rerun within jupyter notebooks (#1798) (#1834) (#1844) * Introduce the ability to push an rrd binary via iframe.contentWindow.postMessage * New API to output the current buffered messages as a cell in jupyter * Example notebook with the cube demo * Track that we need to send another recording msg after draining the backlog * Dynamically resolve the app location based on git commit. Allow override to use self-hosted assets * Add some crude timeout logic in case the iframe fails to load * Don't persist app state in notebooks * Introduce new MemoryRecording for use with Jupyter notebooks (#1834) * Refactor the relationship between the assorted web / websocket servers (#1844) * Rename RemoteViewerServer to WebViewerSink * CLI arguments for specifying ports * Proper typing for the ports * Disable wheel tests for x86_64-apple-darwin (#1853) * Fix typos in notebook readme (#1852) * Fix the python build when running without web_viewer enabled (#1856) * Error instead of expect inside msg_encode. (#1857) * Restore: New API to reset_time (#1826) (#1854) * Revert "Implement billinear filtering of textures (#1850)" (#1859) This reverts commit d33dab6e7a33f82ab2513058d0f85744e3ce6ef4. * Add Restart command and keyboard shortcut for moving time to start of timeline (#1802) * Add Restart button to timeline UI. This sets the timeline back to zero. * Adds Restart Command & Timeline Command * Adds Ctrl-Shift-Space keyboard modifier * Remove restart button from timeline UI. * Use cmd(Key::LeftArrow) as key combo for restart timeline. * Add TimeControl::restart to restart the current timeline. * Use this from the kb_shortcut function * fix some code nits --------- Co-authored-by: Emil Ernerfeldt * Fix shutdown race condition in `re_sdk_comms` client (#1861) * Wait for encoder to shut down before shutting down the other threads * Remove unused dependencies (#1863) * Gpu picking for depth clouds (#1849) * wip * allow for hovering depth clouds via gpu picking * Use `[x, y]: [u32; 2]` as argument --------- Co-authored-by: Emil Ernerfeldt * Remove manual depth projection from car and nyud examples (#1869) * Remove manual depth projection from car and nyud examples * revert change to cube.ipynb * revert changes to cube.ipynb * third times a charm for cube.ipynb * Improve end-to-end testing slightly (#1862) * CI: Run e2e tests with RUST_LOG=debug * Move installing of pip packaged from CI to e2e script * Re-enable bilinear interpolation again (#1860) * Revert "Revert "Implement billinear filtering of textures (#1850)" (#1859)" This reverts commit 625d2bdd241c09ff9d0ae394ba91565fa48455ec. * Split rectangle.wgsl into fragme/vertex parts to work around naga bug * Use GPU colormapping when showing images in the GUI (#1865) * Cleanup: move Default close to the struct definition * Simplify code: use if-let-else-return * Simplify code: no need for Arc * Add EntityDataUi so that the Tensor ui function knows entity path * Better naming: selection -> item * Simplify code: no optional tensor stats * Less use of anyhow * Use GPU colormapping when showing tensors in GUI * Link to issue * Optimize pad_to_four_elements for debug builds * Refactor: simpler arguments to show_zoomed_image_region_area_outline * Fix missing meter argument * Refactor: break up long function * Less use of Arc * Pipe annotation context to the hover preview * Simplify `AnnotationMap::find` * Use new GPU colormapper for the hover-zoom-in tooltip * Refactor * Add helper function for turning a Tensor into an image::DynamicImage * Fix warning on web builds * Add helper function `Tensor::could_be_dynamic_image` * Implement click-to-copy and click-to-save for tensors without egui * Convert histogram to the new system * Remove the TensorImageCache * Fix TODO formatting * bug fixes and cleanups * Rename some stuff * Build-fix * Simplify some code * Turn off benchmakrs comment on each PR (#1872) * Refactor: remove `GpuTexture2DHandle::invalid` (#1866) * Refactor TexturedRect * Remove GpuTexture2DHandle::invalid * `GpuTexture2DHandle` is always valid * spacing * Update enumflags2 to non-yanked version (#1874) * Update enumflags2 to non-yanked version ``` ❯ cargo update -p enumflags2 Updating crates.io index Updating enumflags2 v0.7.5 -> v0.7.7 Updating enumflags2_derive v0.7.4 -> v0.7.7 Updating proc-macro2 v1.0.47 -> v1.0.56 Updating quote v1.0.21 -> v1.0.26 Adding syn v2.0.15 ``` Unfortunately this adds the syn v2 dependency for some platforms * Updating dependencies is a valid label * cargo deny: check more platforms * fix stuff broken from merging upstream --------- Co-authored-by: Clement Rey Co-authored-by: benjamin de charmoy Co-authored-by: Andreas Reich Co-authored-by: Emil Ernerfeldt Co-authored-by: Pablo Vela Co-authored-by: Nikolaus West Co-authored-by: Jeremy Leibs Co-authored-by: h3mosphere <129932586+h3mosphere@users.noreply.github.com> Co-authored-by: Urho Laukkarinen * expose depth config, get available sensor resolutions for the selected device * move removing entities to a place where the removal will always be tried, not just when expanding space view header * added depth alignment * added stream enabled buttons * Depth cloud textures are now cached frame-to-frame (#1913) * Depth cloud textures are now cached frame-to-frame Simplified logic a bit by enforcing F32 texture conversion (there was a u16 path for native only) * doc fix * naming consistency, format check, remove unnecessary scaling * improve depth cloud texture check * fixes after merging * Smooth out scroll wheel input for camera zooming (#1920) * Always spawn instead of fork in multiprocessing example (#1922) * Add `--num-frames` arg to canny (webcam) example (#1923) * fix formatting issues * rerun format fix * fix spelling * lint check fixes * mypy * some more lint fixes * some more fixes for python lint checks * Collect extra egui features into the main Cargo.toml (#1926) * just rs-run-all * `just py-run-all-{native|web|rrd}` (#1927) * make all python examples handle unknown arguments gracefully * just py-run-all-{native|web|rrd} * bump version * comment out clang * Join threads at end of multi-threading example (#1934) * Add argument parsing to the rerun_demo (#1925) * More robust wait for exit condition during .serve() (#1939) * More robust wait for exit condition during .serve() * lint * Use zipfile python library instead of `unzip` command in arkitscene (#1936) * Use zipfile python library instead of `unzip` command in arkitscene Windows doesn't have unzip! * Nit: import sort order --------- Co-authored-by: Nikolaus West * Fix annotation images sometimes drawn in the background. (#1933) This caused fairly ugly rendering whenever that happened. Also cleaned up redundant image/textured_rect defintions in spatial scene buildup * Fix backslashes in arkitscene rigid transformation path (#1938) * Fix backslashes in arkitscene rigid transformation path Should be fixed properly by https://github.com/rerun-io/rerun/issues/1937 * Use PosixPath instead of .replace("\\", "/") --------- Co-authored-by: Nikolaus West * Fix hover/select highlights when picking single points in a scene with multiple point clouds (#1942) Batch vertex offset for single highlights wasn't correctly computed. Different parts of the code made different assumptions what offsets referred to * Fix hovering depth clouds (#1943) We didn't add to `scene.primitives.image`. Instead of adding to this list, it is instead now no longer needed for picking since we can very easily query for tensor again. * change python workflow for testing purposes, remove windows and macos wheels * add back one macos so the matrix is valid * 2.5GB before GC kick in on web (#1944) * change name to depthai-viewer * change pip install/uninstalls from rerun-sdk to depthai-viewer * change all occurances of rerun-sdk to depthai-viewer * change windows runner to windows-latest for now when using my personal gh * Release `0.5.0` (#1919) * changelog * 0.5.0-alpha.0 * more changelog * re_format: fix implicit recursive feature flags * publish_crates.sh: fix crate ordering * Join threads at end of multi-threading example (#1934) * Add argument parsing to the rerun_demo (#1925) * More robust wait for exit condition during .serve() (#1939) * More robust wait for exit condition during .serve() * lint * Use zipfile python library instead of `unzip` command in arkitscene (#1936) * Use zipfile python library instead of `unzip` command in arkitscene Windows doesn't have unzip! * Nit: import sort order --------- Co-authored-by: Nikolaus West * Fix annotation images sometimes drawn in the background. (#1933) This caused fairly ugly rendering whenever that happened. Also cleaned up redundant image/textured_rect defintions in spatial scene buildup * Fix backslashes in arkitscene rigid transformation path (#1938) * Fix backslashes in arkitscene rigid transformation path Should be fixed properly by https://github.com/rerun-io/rerun/issues/1937 * Use PosixPath instead of .replace("\\", "/") --------- Co-authored-by: Nikolaus West * changelog * Fix hover/select highlights when picking single points in a scene with multiple point clouds (#1942) Batch vertex offset for single highlights wasn't correctly computed. Different parts of the code made different assumptions what offsets referred to * changelog * Fix hovering depth clouds (#1943) We didn't add to `scene.primitives.image`. Instead of adding to this list, it is instead now no longer needed for picking since we can very easily query for tensor again. * changelog * 2.5GB before GC kick in on web (#1944) * changelog * 0.5.0 --------- Co-authored-by: Jeremy Leibs Co-authored-by: Andreas Reich Co-authored-by: Nikolaus West * Fix imu plots scrolling past their container * fix bottom/top panel sizing after showing the spinner on config setting * Lint error names in `map_err` (#1948) * Lint: Properly name errors in `map_err` * Use correct names for errors * fix config and stats tabs not being able to be viewed at the same time, renamed stats to IMU * New dispatch-only workflow for running the lint-job (#1950) * Fix secret in dispatch_lint.yml * Only maintain a single manual-dispatch job for testing workflows * apply button, have to make it look a bit nicer, but will do that when I fix scrolling in device configuration panel * Bump hyper version due to RUSTSEC-2023-0034 (#1951) * Add other build parameterizations to manual_dispatch.yml * Use proper if gates on the manual_dispatch.yml jobs * Add ability to save cache to manual_disaptch.yml * Standard case of inputs * Add manual step for packaging to 'manual_dispatch.yml' * add back panels when the underlying subscription reappears * Fix crash for missing class ids causing zero sized texture (#1947) * Fix crash for missing class ids causing zero sized texture Two things fixed actually: * texture manager now checks for zero sized texture, this ripples out in a lot more error handling * class id texture texture handles not having any classes gracefully * Use Display for all errors * typo * Better naming of error * Better docs and names * Fix off-by-one error * some use of `context`, change which error is implicitly converted on texture manager2d --------- Co-authored-by: Emil Ernerfeldt * fixes LR stream subscriptions (maybe breaks panels reappearing after sub is gone) (#15) * make config ui scrollable, fix padding * Move clippy_wasm/clippy.toml to under scripts (#1949) * Move clippy_wasm/clippy.toml to under scripts This is just to clean up the root a bit, and to move it closer to where it is actually used. * Fix comment typo --------- Co-authored-by: Andreas Reich * change crate version to 0.6.0-alpha.0 (#1952) * New workflow_dispatch for building wheels for a PR * initial light mode, luxonis depthai viewer * Rename build_wheels_for_pr.yml -> manual_build_wheels_for_pr.yml * Fix run-wasm crash on trying to wait for server (#1959) This ruined my dev experience for re_renderer examples a bit. Not sure what made the previous hack stop working, might be a timing issue. It ended up crashing the `cargo_run_wasm` web server * Update egui to latest and wgpu to 0.16 (#1958) * update to wgpu 0.16 and egui using this version * shader fixup for type aliases and rectangle shader * shader signed/unsigned shenanigans * more signed/unsigned issues * fix texture component count * fix picking layer depth readback crash on web * patch wgpu * better texture size estimate * fix patches * Handle leaking of prerelease into alpha version (#1953) * Make device config panel remember it's height throughout loading * hide time panel, make ai model dropdown wider * Fix incorrect memory usage stats for destroyed on-creation-mapped buffers (#1963) We actually don't have anywhere where we discard this kind of buffer yet, but if we would the stats would be wrong (noticed while doing quick & dirty experiments on the staging belt) * New manual workflow for running benches * Introduce new reusable workflow jobs and cleanup manual trigger (#1954) There are 8 reusable workflow "components" that we can use to build different scenarios: reusable_checks.yml - These are all the checks that run to ensure the code is formatted, reusable_bench.yml - This job runs the benchmarks to check for performance regressions. reusable_deploy_docs- This job deploys the python and rust documentation to https://ref.rerun.io reusable_build_and_test_wheels.yml - This job builds the wheels, runs the end-to-end test, and produces a sample RRD. The artifacts are accessible via GitHub artifacts, but not otherwise uploaded anywhere. reusable_upload_wheels.yml- This job uploads the wheels to google cloud reusable_build_web.yml - This job builds the wasm artifacts for the web. reusable_upload_web.yml - This job uploads the web assets to google cloud. By default this only uploads to: app.rerun.io/commit// reusable_pr_summary.yml - This job updates the PR summary with the results of the CI run. Example summary can be found at: https://storage.googleapis.com/rerun-builds/pull_request/1954/index.html This also introduces a manual_dispatch.yml helper as a convenience for testing these workflows and their different parameterizations. * New manual workflow for adhoc web builds * Use new CI workflows for pull-request and merge to main (#1955) on_pull_request.yml includes the following pieces: - reusable_checks.yml -- Run all of the lints, code-formatting, tests, etc. - reusable_build_and_test_wheels.yml -- Configured in a "minimal" mode with SDK includes end-to-end test and produces an rrd. - reusable_build_web.yml -- Verifies we can build the wasm - reusable_upload_web.yml -- Uploads the RRD and Wasm to app.rerun.io to confirm the demo works as well as support notebook testing. - reusable_pr_summary.yml -- Create a manifest page with a link to the on_push_main.yml includes the following pieces: - reusable_checks.yml -- Run all of the lints, code-formatting, tests, etc. - reusable_bench.yml -- Run the benchmarks - reusable_build_and_test_wheels.yml -- Builds wheels for all platforms - reusable_upload_wheel.yml -- Uploads the all the wheels to gcloud - reusable_build_web.yml -- Builds the wasm bundle - reusable_upload_web.yml -- Uploads the RRD and Wasm to app.rerun.io - reusable_pip_index.yml -- Generates a pip index page which can be used to install packages with, e.g. * Fix name of on_push_main.yml * Fix usage of long commit in generate_prerelease_pip_index.py * Try making pull-request workflows non-concurrent (#1970) * Try making pull-request workflows non-concurrent * Concurrency groups for push_main as well * Each sub-workflow needs its own name or they fight * Another attempt to make jobs non-concurrent on a per-PR basis (#1974) * Another attempt to make jobs non-concurrent on a per-PR basis * Move concurrency into the reusable job * Jobs with duplicated instances still need separate concurrency keys based on platform * Round to nearest color_index when doing color mapping (#1969) * Full (experimental) WebGPU support (#1965) * always build with unstable web sys apis * Make shader Tint friendly * expose webgl feature flag on re_renderer & re_viewer * fix bug link on negative hexadecimal * hardware tier is now created from wgpu adapter * sort out build flags for webgpu & document building webviewer * introduce shader text replacement workarounds to workaround current chrome issue * latest egui master * typo fix * doc fixes, use if cfg! instead of attribute cfg * move backend to rerun * If there's a `{{ pr-build-summary }}` in the PR description, update it. (#1971) * If there's a `{{ pr-build-summary }}` in the PR description, update it. * Add comment to the PR template * Add pull-requests permission to pr_summary job * Run the cube notebook on PR (#1972) * Run the cube notebook on PR * Add notebooks to the build summary * Use the new concurrency model * reformat py files * reformat * fix pylint errors * Add ability to manually run a web build to upload to an adhoc name (#1966) * Add ability to manually run a web build to upload to an adhoc name * Pass through ADHOC_NAME * Add a concurrency criteria for the new adhoc job * Make input description more explicit * change entity paths * merged wip albedo colormap into latest depth_cloud * remove pointclouds generated in sdk * fix compiler error, trying to compile frame.close for wasm * Default to albedo texture for depth cloud, added support for mono albedo textures * restart backend on failure, added oak_cam.device.close seems to really close the cam * py lint fix * don't run notebooks * remove run-notebook dependency --------- Co-authored-by: Clement Rey Co-authored-by: benjamin de charmoy Co-authored-by: Andreas Reich Co-authored-by: Emil Ernerfeldt Co-authored-by: Pablo Vela Co-authored-by: Nikolaus West Co-authored-by: Jeremy Leibs Co-authored-by: h3mosphere <129932586+h3mosphere@users.noreply.github.com> Co-authored-by: Urho Laukkarinen Co-authored-by: Nikolaus West * Fix rerun lints * rename binary, always start with memory-limit, pin depthai dependencies, fix errors after changing dependency versions * remove compiler warnings * fix memory leak when app is in background * Custom viewport UI (#4) * custom blueprint panel, show logical space views for depthai-viewer users, added settings clog on top of space view tab to configure what is visible * small fixes, clear entity_paths every time to avoid displaying an unavailable entity_path in space view options ui * custom left panel to add or remove space view instances, created a new default viewport layout. Improved behaviour when a panel re appears after user selected to hide it, then if stream stops and starts again the panel will be spawned back in correctly. * improve auto layout to not split when only 3D or 2D view is available * MJPEG encode image frames if connected to a PoE device. Only add magnetometer to imu sensors list if the device has a BNO IMU. Lower the memory limit to 100MB * Styling (#6) * initial styling impl * make buttons that should be small, small * Runtime depth config and fix device selection ui * comment * Proper runtime depth config updates * switch to Yolo v8 * add comma to label for non open zoo models * split 2d + 3d cam view vertically instead of horizontally * Tabify all panels (except for blueprint) (#7) * initial styling impl * make buttons that should be small, small * Runtime depth config and fix device selection ui * comment * Proper runtime depth config updates * switch to Yolo v8 * add comma to label for non open zoo models * split 2d + 3d cam view vertically instead of horizontally * Make the UI more configurable by converting the right panel into tabs. TODO: UX while laying out the panels. When a new space view appears only update the viewport layout, try to keep the user configured fixed function panels as they were. Just handle it in a way that is intuitive * remove bottom panel, switch to RAW imu sensors * XLink statistics initial implementation * initial xlink throughput statistics impllementation, have to glow it up a bit and maybe clean up the code * Xlink and rerun rename (#9) * Rename rerun py library to depthai_viewer * bug fixes and started working on a smart auto layout that operates on an existing tree, to preserve ui as much as possible, while also creating good layouts * auto layout * Fix maximize not working and add maximize for Stats tab * delete profiling stuff that shouldn't have been commited * mostly fix smart layout, TODO: detect when you can group mono and color 3d + 2d views into a 4 way split * add docstring for update_tree * WIP auto layout can_create_mono_quad checker, not at all finished yet * pass lint checks and bugfixes * forgot to sort imports * Fix mypy lint (TODO: Proper typing, especially in the comms from back to store to ws) other types are pretty solid * pylints and fix Queue typehinting * fix doc build * try to pass pylints with py.typed * ignore misc mypy errors * forgot to run black formatter * switch back to old ci * sync mono camera settings * fix web build * Update scripts/version_util.py --------- Co-authored-by: Clement Rey Co-authored-by: benjamin de charmoy Co-authored-by: Andreas Reich Co-authored-by: Emil Ernerfeldt Co-authored-by: Pablo Vela Co-authored-by: Nikolaus West Co-authored-by: Jeremy Leibs Co-authored-by: h3mosphere <129932586+h3mosphere@users.noreply.github.com> Co-authored-by: Urho Laukkarinen Co-authored-by: Nikolaus West * install depthai_viewer instead of rerun-sdk for pytest * Updated design and improvements * get rid of rerun_sdk and rerun_bindings, now just depthai_viewer and depthai_viewer_bindings get installed, so you don't clash with an existing rerun install * hopefuly fix ci * hopefuly fix ci * a bunch of fixes and improvements * disable smart layout updater for now * bugfixes * Update README.md * Patches * WIP moving away from relying on board sockets. This will enable us to support all devices! * fix merge error * LR fixes, and other stuff * a few fixes, viewer seems pretty stable on any device * auto layout * Make blueprint work * bigfixes for adding and removing space views, ai model board socket change bugfix. * bump version, fixes, Install deps in venv and use the deps from venv instead of global. * Fix for windows (and linux) * fixes * Changelog * ci checks * clear by cutoff only when not targeting wasm * Use sysconfig instead of site for getting site-packages, change app icon * No more (ignored) exceptions on startup * Fix pylints, bump depthai_sdk to latest develop * disable depth if no legit stereo pairs are available * Bump version * remove unused variable * Fix duplicate resolutions, improve first startup virtual environment creation * Fix xlink plots a bit * disable playback commands * focus on camera by default * Changelog * bump version * Improved first time startup stability * Prioritize camera images over depth image when creating albedo texture * Changed logos and get depthai_sdk from the artifactory * changelog * forgot to run pylints * Improved tensor logging performance * Don't show 2D image if the 3d view has a pointcloud * fix memory leak, loose a bit of performance because of tobytes * NV12 support! (#18) * WIP: Support sending encoded images to reduce data transfer sizes. * Support logging of NV12 encoded images, support for mapping depth_cloud onto NV12 image, todo: Fix tooltip tensor view... * Almost correct display of NV12 in every view, tooltip is still incorrect * Fixed picking! Tensor::get_nv12_pixel is wrong - todo along with correct NV12 -> RGB decoding coefficients in the shader * Fully working NV12 decoding in the shader! * Backend refactor * Better layout * Nicer behavior on device select * Fix Depth and Image spawning in seperate tabs * refactor, use sdk create_queue to get better performance * add sdk dependency * moved back to callbacks * sync cameras and depth, decode jpeg on the backend using turbojpeg (still slow) * prerelease build to test on multiple platforms * rerun lints * mypy lints * mypy lints * imports * match alpha and beta version tags * remove openh264 for now --------- Co-authored-by: Clement Rey Co-authored-by: benjamin de charmoy Co-authored-by: Andreas Reich Co-authored-by: Emil Ernerfeldt Co-authored-by: Pablo Vela Co-authored-by: Nikolaus West Co-authored-by: Jeremy Leibs Co-authored-by: h3mosphere <129932586+h3mosphere@users.noreply.github.com> Co-authored-by: Urho Laukkarinen Co-authored-by: Nikolaus West --- .vscode/settings.json | 3 +- CHANGELOG.md | 22 + Cargo.lock | 82 +- Cargo.toml | 54 +- crates/re_arrow_store/src/store_gc.rs | 28 +- crates/re_build_info/src/lib.rs | 2 +- crates/re_data_store/src/log_db.rs | 15 +- crates/re_log_types/Cargo.toml | 2 +- .../src/component_types/tensor.rs | 391 ++++++---- .../src/component_types/xlink_stats.rs | 5 +- crates/re_log_types/src/lib.rs | 5 +- crates/re_memory/src/memory_limit.rs | 4 +- crates/re_renderer/shader/decodings.wgsl | 19 + crates/re_renderer/shader/depth_cloud.wgsl | 84 ++- crates/re_renderer/shader/rectangle.wgsl | 5 + crates/re_renderer/shader/rectangle_fs.wgsl | 3 + crates/re_renderer/shader/rectangle_vs.wgsl | 7 +- .../re_renderer/src/renderer/depth_cloud.rs | 442 ++++++----- crates/re_renderer/src/renderer/mod.rs | 4 +- crates/re_renderer/src/renderer/rectangles.rs | 222 +++--- crates/re_renderer/src/workspace_shaders.rs | 6 + crates/re_sdk/src/lib.rs | 3 +- crates/re_ui/data/icons/app_icon_mac.png | Bin 69290 -> 4402 bytes crates/re_ui/data/icons/app_icon_windows.png | Bin 30514 -> 4402 bytes crates/re_ui/data/icons/rerun_menu.png | Bin 11051 -> 2256 bytes crates/re_ui/data/logo_dark_mode.png | Bin 4402 -> 2256 bytes crates/re_ui/data/logo_light_mode.png | Bin 4402 -> 2256 bytes crates/re_ui/src/command.rs | 42 +- crates/re_ui/src/lib.rs | 1 + crates/re_viewer/Cargo.toml | 1 + crates/re_viewer/src/app.rs | 104 +-- crates/re_viewer/src/depthai/api.rs | 72 +- crates/re_viewer/src/depthai/depthai.rs | 651 ++++++++-------- crates/re_viewer/src/depthai/ws.rs | 126 ++-- crates/re_viewer/src/gpu_bridge/mod.rs | 90 ++- .../re_viewer/src/gpu_bridge/tensor_to_gpu.rs | 272 +++---- crates/re_viewer/src/lib.rs | 7 +- crates/re_viewer/src/misc/time_control_ui.rs | 11 +- crates/re_viewer/src/ui/auto_layout.rs | 560 +++++++------- crates/re_viewer/src/ui/data_ui/image.rs | 410 +++++----- .../re_viewer/src/ui/device_settings_panel.rs | 702 ++++++++---------- crates/re_viewer/src/ui/mod.rs | 6 +- crates/re_viewer/src/ui/selection_panel.rs | 26 - crates/re_viewer/src/ui/space_view.rs | 100 +-- .../re_viewer/src/ui/space_view_heuristics.rs | 16 +- crates/re_viewer/src/ui/stats_panel.rs | 25 +- crates/re_viewer/src/ui/view_bar_chart/ui.rs | 57 +- .../src/ui/view_spatial/scene/picking.rs | 7 +- .../view_spatial/scene/scene_part/images.rs | 378 +++++----- crates/re_viewer/src/ui/view_spatial/ui.rs | 513 +++++++------ crates/re_viewer/src/ui/view_spatial/ui_3d.rs | 115 ++- .../src/ui/view_tensor/tensor_slice_to_gpu.rs | 6 +- crates/re_viewer/src/ui/view_tensor/ui.rs | 17 +- crates/re_viewer/src/ui/viewport.rs | 165 ++-- crates/re_viewer/src/viewer_analytics.rs | 6 +- crates/rerun/src/main.rs | 1 - crates/rerun/src/run.rs | 7 +- rerun_py/README.md | 3 +- rerun_py/depthai_viewer/__init__.py | 42 +- rerun_py/depthai_viewer/__main__.py | 95 ++- .../_backend/classification_labels.py | 86 +-- .../depthai_viewer/_backend/config_api.py | 142 ++-- rerun_py/depthai_viewer/_backend/device.py | 386 ++++++++++ .../_backend/device_configuration.py | 325 +++++--- rerun_py/depthai_viewer/_backend/main.py | 366 ++------- rerun_py/depthai_viewer/_backend/messages.py | 102 +++ .../depthai_viewer/_backend/packet_handler.py | 225 ++++++ .../depthai_viewer/_backend/sdk_callbacks.py | 174 ----- rerun_py/depthai_viewer/_backend/store.py | 66 +- rerun_py/depthai_viewer/components/tensor.py | 20 +- .../depthai_viewer/components/xlink_stats.py | 3 +- rerun_py/depthai_viewer/log/image.py | 45 ++ rerun_py/depthai_viewer/log/tensor.py | 5 +- rerun_py/depthai_viewer/log/xlink_stats.py | 8 +- rerun_py/depthai_viewer/requirements.txt | 10 + rerun_py/pyproject.toml | 12 +- rerun_py/src/python_bridge.rs | 10 +- rerun_py/src/python_session.rs | 7 +- scripts/check_shader.py | 61 ++ scripts/version_util.py | 2 +- 80 files changed, 4508 insertions(+), 3589 deletions(-) create mode 100644 crates/re_renderer/shader/decodings.wgsl create mode 100644 rerun_py/depthai_viewer/_backend/device.py create mode 100644 rerun_py/depthai_viewer/_backend/messages.py create mode 100644 rerun_py/depthai_viewer/_backend/packet_handler.py delete mode 100644 rerun_py/depthai_viewer/_backend/sdk_callbacks.py create mode 100644 rerun_py/depthai_viewer/requirements.txt create mode 100644 scripts/check_shader.py diff --git a/.vscode/settings.json b/.vscode/settings.json index a1a7f0abc714..b9ea6558d528 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -71,5 +71,6 @@ // INCLUDING attempts to publish a new release! "rust-analyzer.cargo.buildScripts.enable": false, "python.analysis.extraPaths": ["rerun_py/"], - "ruff.args": ["--config", "rerun_py/pyproject.toml"] + "ruff.args": ["--config", "rerun_py/pyproject.toml"], + "stm32-for-vscode.openOCDPath": false } diff --git a/CHANGELOG.md b/CHANGELOG.md index 268c14c09115..56cdd155abba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Depthai Viewer changelog +## 0.0.7 + +- Install depthai_sdk from artifactory +- Change logos + +## 0.0.6 + +- App startup bugfixes + +## 0.0.5 + +- App startup bugfixes +- Better default focusing in 3d views + +## 0.0.4 + +- Disable depth settings if intrinsics aren't available. +- App startup bugfixes. + +## 0.0.3 + +- Added support for all devices. ## 0.0.2 diff --git a/Cargo.lock b/Cargo.lock index b0369291c38a..32f70da3af73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,7 +124,7 @@ checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" [[package]] name = "api_demo" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "anyhow", "clap 4.1.4", @@ -1265,9 +1265,19 @@ dependencies = [ "syn 1.0.103", ] +[[package]] +name = "dcv-color-primitives" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6404eb429533f00a9430a015daca9235593068a1080860aa5cfbde6a8d9f7ca8" +dependencies = [ + "paste", + "wasm-bindgen", +] + [[package]] name = "depthai-viewer" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "anyhow", "backtrace", @@ -1382,7 +1392,7 @@ dependencies = [ [[package]] name = "dna" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "depthai-viewer", "itertools", @@ -2822,7 +2832,7 @@ dependencies = [ [[package]] name = "minimal" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "depthai-viewer", ] @@ -2835,7 +2845,7 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "minimal_options" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "anyhow", "clap 4.1.4", @@ -3253,7 +3263,7 @@ dependencies = [ [[package]] name = "objectron" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "anyhow", "clap 4.1.4", @@ -3871,7 +3881,7 @@ checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" [[package]] name = "raw_mesh" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "anyhow", "bytes", @@ -3911,7 +3921,7 @@ dependencies = [ [[package]] name = "re_analytics" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "anyhow", "crossbeam", @@ -3932,7 +3942,7 @@ dependencies = [ [[package]] name = "re_arrow_store" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "ahash 0.8.2", "anyhow", @@ -3959,7 +3969,7 @@ dependencies = [ [[package]] name = "re_build_build_info" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "anyhow", "time 0.3.20", @@ -3967,18 +3977,18 @@ dependencies = [ [[package]] name = "re_build_info" -version = "0.0.2" +version = "0.0.8-alpha.0" [[package]] name = "re_build_web_viewer" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "cargo_metadata", ] [[package]] name = "re_data_store" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "ahash 0.8.2", "criterion", @@ -4001,14 +4011,14 @@ dependencies = [ [[package]] name = "re_error" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "anyhow", ] [[package]] name = "re_format" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "arrow2", "arrow2_convert", @@ -4018,7 +4028,7 @@ dependencies = [ [[package]] name = "re_int_histogram" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "criterion", "insta", @@ -4029,7 +4039,7 @@ dependencies = [ [[package]] name = "re_log" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "env_logger", "js-sys", @@ -4042,7 +4052,7 @@ dependencies = [ [[package]] name = "re_log_encoding" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "criterion", "ehttp", @@ -4067,7 +4077,7 @@ dependencies = [ [[package]] name = "re_log_types" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "ahash 0.8.2", "array-init", @@ -4091,6 +4101,7 @@ dependencies = [ "rand", "re_format", "re_log", + "re_renderer", "re_string_interner", "re_tuid", "rmp-serde", @@ -4105,7 +4116,7 @@ dependencies = [ [[package]] name = "re_memory" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "ahash 0.8.2", "backtrace", @@ -4125,7 +4136,7 @@ dependencies = [ [[package]] name = "re_query" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "arrow2", "criterion", @@ -4143,7 +4154,7 @@ dependencies = [ [[package]] name = "re_renderer" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "ahash 0.8.2", "anyhow", @@ -4196,7 +4207,7 @@ dependencies = [ [[package]] name = "re_sdk" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "arrow2_convert", "document-features", @@ -4216,7 +4227,7 @@ dependencies = [ [[package]] name = "re_sdk_comms" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "ahash 0.8.2", "anyhow", @@ -4232,7 +4243,7 @@ dependencies = [ [[package]] name = "re_smart_channel" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "crossbeam", "instant", @@ -4240,7 +4251,7 @@ dependencies = [ [[package]] name = "re_string_interner" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "ahash 0.8.2", "nohash-hasher", @@ -4251,7 +4262,7 @@ dependencies = [ [[package]] name = "re_tensor_ops" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "ahash 0.8.2", "ndarray", @@ -4261,7 +4272,7 @@ dependencies = [ [[package]] name = "re_tuid" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "arrow2", "arrow2_convert", @@ -4275,7 +4286,7 @@ dependencies = [ [[package]] name = "re_ui" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "eframe", "egui", @@ -4293,7 +4304,7 @@ dependencies = [ [[package]] name = "re_viewer" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "ahash 0.8.2", "anyhow", @@ -4303,6 +4314,7 @@ dependencies = [ "cocoa", "console_error_panic_hook", "crossbeam-channel", + "dcv-color-primitives", "eframe", "egui", "egui-wgpu", @@ -4363,7 +4375,7 @@ dependencies = [ [[package]] name = "re_web_viewer_server" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "cargo_metadata", "ctrlc", @@ -4380,7 +4392,7 @@ dependencies = [ [[package]] name = "re_ws_comms" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "anyhow", "bincode", @@ -4452,7 +4464,7 @@ checksum = "216080ab382b992234dda86873c18d4c48358f5cfcb70fd693d7f6f2131b628b" [[package]] name = "rerun_py" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "arrow2", "depthai-viewer", @@ -4556,7 +4568,7 @@ dependencies = [ [[package]] name = "run_wasm" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "cargo-run-wasm", "pico-args", @@ -5110,7 +5122,7 @@ dependencies = [ [[package]] name = "test_image_memory" -version = "0.0.2" +version = "0.0.8-alpha.0" dependencies = [ "depthai-viewer", "mimalloc", diff --git a/Cargo.toml b/Cargo.toml index 920cb2836f2c..5e1442a43535 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,39 +16,39 @@ include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"] license = "MIT OR Apache-2.0" repository = "https://github.com/rerun-io/rerun" rust-version = "1.67" -version = "0.0.2" +version = "0.0.8-alpha.0" [workspace.dependencies] # When using alpha-release, always use exact version, e.g. `version = "=0.x.y-alpha.z" # This is because we treat alpha-releases as incompatible, but semver doesn't. # In particular: if we compile rerun 0.3.0-alpha.0 we only want it to use # re_log_types 0.3.0-alpha.0, NOT 0.3.0-alpha.4 even though it is newer and semver-compatible. -re_sdk_comms = { path = "crates/re_sdk_comms", version = "0.0.2" } -re_analytics = { path = "crates/re_analytics", version = "0.0.2" } -re_arrow_store = { path = "crates/re_arrow_store", version = "0.0.2" } -re_build_build_info = { path = "crates/re_build_build_info", version = "0.0.2" } -re_build_info = { path = "crates/re_build_info", version = "0.0.2" } -re_build_web_viewer = { path = "crates/re_build_web_viewer", version = "0.0.2" } -re_data_store = { path = "crates/re_data_store", version = "0.0.2" } -re_error = { path = "crates/re_error", version = "0.0.2" } -re_format = { path = "crates/re_format", version = "0.0.2" } -re_int_histogram = { path = "crates/re_int_histogram", version = "0.0.2" } -re_log = { path = "crates/re_log", version = "0.0.2" } -re_log_encoding = { path = "crates/re_log_encoding", version = "0.0.2" } -re_log_types = { path = "crates/re_log_types", version = "0.0.2" } -re_memory = { path = "crates/re_memory", version = "0.0.2" } -re_query = { path = "crates/re_query", version = "0.0.2" } -re_renderer = { path = "crates/re_renderer", version = "0.0.2", default-features = false } -re_sdk = { path = "crates/re_sdk", version = "0.0.2" } -re_smart_channel = { path = "crates/re_smart_channel", version = "0.0.2" } -re_string_interner = { path = "crates/re_string_interner", version = "0.0.2" } -re_tensor_ops = { path = "crates/re_tensor_ops", version = "0.0.2" } -re_tuid = { path = "crates/re_tuid", version = "0.0.2" } -re_ui = { path = "crates/re_ui", version = "0.0.2" } -re_viewer = { path = "crates/re_viewer", version = "0.0.2", default-features = false } -re_web_viewer_server = { path = "crates/re_web_viewer_server", version = "0.0.2" } -re_ws_comms = { path = "crates/re_ws_comms", version = "0.0.2" } -depthai-viewer = { path = "crates/rerun", version = "0.0.2" } +re_sdk_comms = { path = "crates/re_sdk_comms", version = "0.0.8-alpha.0" } +re_analytics = { path = "crates/re_analytics", version = "0.0.8-alpha.0" } +re_arrow_store = { path = "crates/re_arrow_store", version = "0.0.8-alpha.0" } +re_build_build_info = { path = "crates/re_build_build_info", version = "0.0.8-alpha.0" } +re_build_info = { path = "crates/re_build_info", version = "0.0.8-alpha.0" } +re_build_web_viewer = { path = "crates/re_build_web_viewer", version = "0.0.8-alpha.0" } +re_data_store = { path = "crates/re_data_store", version = "0.0.8-alpha.0" } +re_error = { path = "crates/re_error", version = "0.0.8-alpha.0" } +re_format = { path = "crates/re_format", version = "0.0.8-alpha.0" } +re_int_histogram = { path = "crates/re_int_histogram", version = "0.0.8-alpha.0" } +re_log = { path = "crates/re_log", version = "0.0.8-alpha.0" } +re_log_encoding = { path = "crates/re_log_encoding", version = "0.0.8-alpha.0" } +re_log_types = { path = "crates/re_log_types", version = "0.0.8-alpha.0" } +re_memory = { path = "crates/re_memory", version = "0.0.8-alpha.0" } +re_query = { path = "crates/re_query", version = "0.0.8-alpha.0" } +re_renderer = { path = "crates/re_renderer", version = "0.0.8-alpha.0", default-features = false } +re_sdk = { path = "crates/re_sdk", version = "0.0.8-alpha.0" } +re_smart_channel = { path = "crates/re_smart_channel", version = "0.0.8-alpha.0" } +re_string_interner = { path = "crates/re_string_interner", version = "0.0.8-alpha.0" } +re_tensor_ops = { path = "crates/re_tensor_ops", version = "0.0.8-alpha.0" } +re_tuid = { path = "crates/re_tuid", version = "0.0.8-alpha.0" } +re_ui = { path = "crates/re_ui", version = "0.0.8-alpha.0" } +re_viewer = { path = "crates/re_viewer", version = "0.0.8-alpha.0", default-features = false } +re_web_viewer_server = { path = "crates/re_web_viewer_server", version = "0.0.8-alpha.0" } +re_ws_comms = { path = "crates/re_ws_comms", version = "0.0.8-alpha.0" } +depthai-viewer = { path = "crates/rerun", version = "0.0.8-alpha.0" } ahash = "0.8" anyhow = "1.0" diff --git a/crates/re_arrow_store/src/store_gc.rs b/crates/re_arrow_store/src/store_gc.rs index e418763eac33..638a6b691fb4 100644 --- a/crates/re_arrow_store/src/store_gc.rs +++ b/crates/re_arrow_store/src/store_gc.rs @@ -1,4 +1,5 @@ -use re_log_types::{RowId, SizeBytes as _, TimeInt, TimeRange}; +use ahash::HashSetExt; +use re_log_types::{RowId, SizeBytes as _, Time, TimeInt, TimeRange}; use crate::{ store::{IndexedBucketInner, IndexedTable}, @@ -157,6 +158,31 @@ impl DataStore { row_ids } + + pub fn gc_drop_by_cutoff_time(&mut self, cutoff_time: i64) -> ahash::HashSet { + let mut row_ids = ahash::HashSet::new(); + + for (_, table) in &mut self.tables.iter_mut() { + let mut row_ids_to_remove = Vec::new(); + { + let (_, bucket) = table.find_bucket(cutoff_time.into()); + for row_id in bucket.inner.write().col_row_id.iter() { + for time in self.metadata_registry.get(row_id).unwrap().times() { + if time.as_i64() < cutoff_time { + row_ids_to_remove.push((*row_id, time)); + if !row_ids.contains(row_id) { + row_ids.insert(*row_id); + } + } + } + } + } + for (row_id, time) in row_ids_to_remove { + table.try_drop_row(row_id, time.as_i64()); + } + } + row_ids + } } impl IndexedTable { diff --git a/crates/re_build_info/src/lib.rs b/crates/re_build_info/src/lib.rs index 484289120cda..954cf78143ae 100644 --- a/crates/re_build_info/src/lib.rs +++ b/crates/re_build_info/src/lib.rs @@ -14,7 +14,7 @@ pub use crate_version::CrateVersion; macro_rules! build_info { () => { $crate::BuildInfo { - crate_name: env!("CARGO_PKG_NAME"), + crate_name: "depthai-viewer", //env!("CARGO_PKG_NAME"), version: $crate::CrateVersion::parse(env!("CARGO_PKG_VERSION")), rustc_version: env!("RE_BUILD_RUSTC_VERSION"), llvm_version: env!("RE_BUILD_LLVM_VERSION"), diff --git a/crates/re_data_store/src/log_db.rs b/crates/re_data_store/src/log_db.rs index 9af248b9c8e0..2308af4cc59f 100644 --- a/crates/re_data_store/src/log_db.rs +++ b/crates/re_data_store/src/log_db.rs @@ -1,12 +1,13 @@ use std::collections::BTreeMap; +use ahash::{HashSet, HashSetExt}; use nohash_hasher::IntMap; use re_arrow_store::{DataStoreConfig, TimeInt}; use re_log_types::{ component_types::InstanceKey, ArrowMsg, BeginRecordingMsg, Component as _, ComponentPath, DataCell, DataRow, DataTable, EntityPath, EntityPathHash, EntityPathOpMsg, LogMsg, PathOp, - RecordingId, RecordingInfo, RowId, TimePoint, Timeline, + RecordingId, RecordingInfo, RowId, Time, TimePoint, Timeline, }; use crate::{Error, TimesPerTimeline}; @@ -277,4 +278,16 @@ impl LogDb { entity_db.purge(&cutoff_times, &drop_row_ids); } + + /// Free up some RAM by forgetting parts of the time that are more than `cutoff` ns in the past. + #[cfg(not(target_arch = "wasm32"))] + pub fn clear_by_cutoff(&mut self, cutoff: i64) { + let cutoff_time = Time::now().nanos_since_epoch() - cutoff; + let oldest = self.entity_db.data_store.oldest_time_per_timeline(); + let row_ids = self + .entity_db + .data_store + .gc_drop_by_cutoff_time(cutoff_time); + self.entity_db.purge(&oldest, &row_ids); + } } diff --git a/crates/re_log_types/Cargo.toml b/crates/re_log_types/Cargo.toml index 3af826321b61..f456ac8ad321 100644 --- a/crates/re_log_types/Cargo.toml +++ b/crates/re_log_types/Cargo.toml @@ -45,6 +45,7 @@ serde = [ [dependencies] # Rerun +re_renderer.workspace = true re_format.workspace = true re_log.workspace = true re_string_interner.workspace = true @@ -80,7 +81,6 @@ time = { workspace = true, default-features = false, features = [ typenum = "1.15" uuid = { version = "1.1", features = ["serde", "v4", "js"] } - # Optional dependencies: ecolor = { workspace = true, optional = true } glam = { workspace = true, optional = true } diff --git a/crates/re_log_types/src/component_types/tensor.rs b/crates/re_log_types/src/component_types/tensor.rs index cf02a3bc2b8e..06c1d8401ca7 100644 --- a/crates/re_log_types/src/component_types/tensor.rs +++ b/crates/re_log_types/src/component_types/tensor.rs @@ -1,14 +1,16 @@ -use arrow2::array::{FixedSizeBinaryArray, MutableFixedSizeBinaryArray}; +use arrow2::array::{ FixedSizeBinaryArray, MutableFixedSizeBinaryArray }; use arrow2::buffer::Buffer; use arrow2_convert::deserialize::ArrowDeserialize; use arrow2_convert::field::ArrowField; -use arrow2_convert::{serialize::ArrowSerialize, ArrowDeserialize, ArrowField, ArrowSerialize}; +use arrow2_convert::{ serialize::ArrowSerialize, ArrowDeserialize, ArrowField, ArrowSerialize }; use crate::Component; -use crate::{TensorDataType, TensorElement}; +use crate::{ TensorDataType, TensorElement }; use super::arrow_convert_shims::BinaryBuffer; +use re_renderer::renderer::TextureEncoding; + // ---------------------------------------------------------------------------- /// A unique id per [`Tensor`]. @@ -57,7 +59,7 @@ impl ArrowSerialize for TensorId { #[inline] fn arrow_serialize( v: &::Type, - array: &mut Self::MutableArrayType, + array: &mut Self::MutableArrayType ) -> arrow2::error::Result<()> { array.try_push(Some(v.0.as_bytes())) } @@ -68,10 +70,9 @@ impl ArrowDeserialize for TensorId { #[inline] fn arrow_deserialize( - v: <&Self::ArrayType as IntoIterator>::Item, + v: <&Self::ArrayType as IntoIterator>::Item ) -> Option<::Type> { - v.and_then(|bytes| uuid::Uuid::from_slice(bytes).ok()) - .map(Self) + v.and_then(|bytes| uuid::Uuid::from_slice(bytes).ok()).map(Self) } } @@ -160,12 +161,22 @@ pub enum TensorData { F32(Buffer), F64(Buffer), JPEG(BinaryBuffer), + NV12(BinaryBuffer), +} + +impl Into> for &TensorData { + fn into(self) -> Option { + match self { + &TensorData::NV12(_) => Some(TextureEncoding::Nv12), + _ => None, + } + } } impl TensorData { pub fn dtype(&self) -> TensorDataType { match self { - Self::U8(_) | Self::JPEG(_) => TensorDataType::U8, + Self::U8(_) | Self::JPEG(_) | Self::NV12(_) => TensorDataType::U8, Self::U16(_) => TensorDataType::U16, Self::U32(_) => TensorDataType::U32, Self::U64(_) => TensorDataType::U64, @@ -180,7 +191,7 @@ impl TensorData { pub fn size_in_bytes(&self) -> usize { match self { - Self::U8(buf) | Self::JPEG(buf) => buf.0.len(), + Self::U8(buf) | Self::JPEG(buf) | Self::NV12(buf) => buf.0.len(), Self::U16(buf) => buf.len(), Self::U32(buf) => buf.len(), Self::U64(buf) => buf.len(), @@ -199,7 +210,7 @@ impl TensorData { pub fn is_compressed_image(&self) -> bool { match self { - Self::U8(_) + | Self::U8(_) | Self::U16(_) | Self::U32(_) | Self::U64(_) @@ -210,7 +221,7 @@ impl TensorData { | Self::F32(_) | Self::F64(_) => false, - Self::JPEG(_) => true, + Self::JPEG(_) | Self::NV12(_) => true, } } } @@ -229,6 +240,7 @@ impl std::fmt::Debug for TensorData { Self::F32(_) => write!(f, "F32({} bytes)", self.size_in_bytes()), Self::F64(_) => write!(f, "F64({} bytes)", self.size_in_bytes()), Self::JPEG(_) => write!(f, "JPEG({} bytes)", self.size_in_bytes()), + Self::NV12(_) => write!(f, "NV12({} bytes)", self.size_in_bytes()), } } } @@ -401,43 +413,77 @@ impl Tensor { self.shape.as_slice() } + /// Calculates the real dimensions of the tensor, taking into account the encoding. + #[inline] + pub fn real_shape(&self) -> Vec { + match &self.data { + &TensorData::NV12(_) => { + let shape = self.shape.as_slice(); + match shape { + [y, x] => { + vec![ + TensorDimension::height((((y.size as f64) * 2.0) / 3.0) as u64), + TensorDimension::width(x.size) + ] + } + _ => panic!("Invalid shape for NV12 encoding: {:?}", shape), + } + } + _ => self.shape().into(), + } + } + #[inline] pub fn num_dim(&self) -> usize { self.shape.len() } /// If this tensor is shaped as an image, return the height, width, and channels/depth of it. + /// Takes into account the encoding pub fn image_height_width_channels(&self) -> Option<[u64; 3]> { - if self.shape.len() == 2 { - Some([self.shape[0].size, self.shape[1].size, 1]) - } else if self.shape.len() == 3 { - let channels = self.shape[2].size; - // gray, rgb, rgba - if matches!(channels, 1 | 3 | 4) { - Some([self.shape[0].size, self.shape[1].size, channels]) - } else { - None + match &self.data { + &TensorData::NV12(_) => { + let shape = self.real_shape(); + if let [y, x] = shape.as_slice() { + Some([y.size, x.size, 1]) + } else { + None + } + } + _ => { + if self.shape.len() == 2 { + Some([self.shape[0].size, self.shape[1].size, 1]) + } else if self.shape.len() == 3 { + let channels = self.shape[2].size; + // gray, rgb, rgba + if matches!(channels, 1 | 3 | 4) { + Some([self.shape[0].size, self.shape[1].size, channels]) + } else { + None + } + } else { + None + } } - } else { - None } } pub fn is_shaped_like_an_image(&self) -> bool { - self.num_dim() == 2 - || self.num_dim() == 3 && { - matches!( - self.shape.last().unwrap().size, - // gray, rgb, rgba - 1 | 3 | 4 - ) - } + self.num_dim() == 2 || + (self.num_dim() == 3 && + ({ + matches!( + self.shape.last().unwrap().size, + // gray, rgb, rgba + 1 | 3 | 4 + ) + })) } #[inline] pub fn is_vector(&self) -> bool { let shape = &self.shape; - shape.len() == 1 || { shape.len() == 2 && (shape[0].size == 1 || shape[1].size == 1) } + shape.len() == 1 || ({ shape.len() == 2 && (shape[0].size == 1 || shape[1].size == 1) }) } #[inline] @@ -452,7 +498,7 @@ impl Tensor { if size <= index { return None; } - offset += *index as usize * stride; + offset += (*index as usize) * stride; stride *= *size as usize; } @@ -467,10 +513,47 @@ impl Tensor { TensorData::I64(buf) => Some(TensorElement::I64(buf[offset])), TensorData::F32(buf) => Some(TensorElement::F32(buf[offset])), TensorData::F64(buf) => Some(TensorElement::F64(buf[offset])), + // Doesn't make sense to get a single value for NV12, use get_nv12_pixel instead. + // You would need to call get once for each channel. + // That would meant that you have to manually supply the channel, so using get_nv12_pixel is easier. + TensorData::NV12(_) => None, TensorData::JPEG(_) => None, // Too expensive to unpack here. } } + pub fn get_nv12_pixel(&self, index: &[u64; 2]) -> Option<[TensorElement; 3]> { + let [row, col] = index; + match self.real_shape().as_slice() { + [h, w] => { + match &self.data { + TensorData::NV12(buf) => { + let uv_offset = (w.size * h.size) as u64; + let y = ((buf[(*row * w.size + *col) as usize] as f64) - 16.0) / 216.0; + let u = + ((buf[(uv_offset + (*row / 2) * w.size + *col) as usize] as f64) - + 128.0) / + 224.0; + let v = + ((buf[((uv_offset + (*row / 2) * w.size + *col) as usize) + 1] as f64) - + 128.0) / + 224.0; + let r = y + 1.402 * v; + let g = y - 0.344 * u + 0.714 * v; + let b = y + 1.772 * u; + + Some([ + TensorElement::U8(f64::clamp(r * 255.0, 0.0, 255.0) as u8), + TensorElement::U8(f64::clamp(g * 255.0, 0.0, 255.0) as u8), + TensorElement::U8(f64::clamp(b * 255.0, 0.0, 255.0) as u8), + ]) + } + _ => None, + } + } + _ => None, + } + } + pub fn dtype(&self) -> TensorDataType { self.data.dtype() } @@ -492,8 +575,7 @@ pub enum TensorCastError { #[error("ndarray type mismatch with tensor storage")] TypeMismatch, - #[error("tensor shape did not match storage length")] - BadTensorShape { + #[error("tensor shape did not match storage length")] BadTensorShape { #[from] source: ndarray::ShapeError, }, @@ -611,20 +693,21 @@ impl<'a> TryFrom<&'a Tensor> for ::ndarray::ArrayViewD<'a, half::f16> { #[cfg(feature = "image")] #[derive(thiserror::Error, Clone, Debug)] pub enum TensorImageLoadError { - #[error(transparent)] - Image(std::sync::Arc), + #[error(transparent)] Image(std::sync::Arc), - #[error("Unsupported JPEG color type: {0:?}. Only RGB Jpegs are supported")] - UnsupportedJpegColorType(image::ColorType), + #[error( + "Unsupported JPEG color type: {0:?}. Only RGB Jpegs are supported" + )] UnsupportedJpegColorType(image::ColorType), - #[error("Unsupported color type: {0:?}. We support 8-bit, 16-bit, and f32 images, and RGB, RGBA, Luminance, and Luminance-Alpha.")] - UnsupportedImageColorType(image::ColorType), + #[error( + "Unsupported color type: {0:?}. We support 8-bit, 16-bit, and f32 images, and RGB, RGBA, Luminance, and Luminance-Alpha." + )] UnsupportedImageColorType(image::ColorType), - #[error("Failed to load file: {0}")] - ReadError(std::sync::Arc), + #[error("Failed to load file: {0}")] ReadError(std::sync::Arc), - #[error("The encoded tensor did not match its metadata {expected:?} != {found:?}")] - InvalidMetaData { + #[error( + "The encoded tensor did not match its metadata {expected:?} != {found:?}" + )] InvalidMetaData { expected: Vec, found: Vec, }, @@ -650,11 +733,11 @@ impl From for TensorImageLoadError { #[cfg(feature = "image")] #[derive(thiserror::Error, Debug)] pub enum TensorImageSaveError { - #[error("Expected image-shaped tensor, got {0:?}")] - ShapeNotAnImage(Vec), + #[error("Expected image-shaped tensor, got {0:?}")] ShapeNotAnImage(Vec), - #[error("Cannot convert tensor with {0} channels and datatype {1} to an image")] - UnsupportedChannelsDtype(u64, TensorDataType), + #[error( + "Cannot convert tensor with {0} channels and datatype {1} to an image" + )] UnsupportedChannelsDtype(u64, TensorDataType), #[error("The tensor data did not match tensor dimensions")] BadData, @@ -666,7 +749,7 @@ impl Tensor { shape: Vec, data: TensorData, meaning: TensorDataMeaning, - meter: Option, + meter: Option ) -> Self { Self { tensor_id, @@ -685,7 +768,7 @@ impl Tensor { /// Requires the `image` feature. #[cfg(not(target_arch = "wasm32"))] pub fn tensor_from_jpeg_file( - image_path: impl AsRef, + image_path: impl AsRef ) -> Result { let jpeg_bytes = std::fs::read(image_path)?; Self::tensor_from_jpeg_bytes(jpeg_bytes) @@ -699,9 +782,7 @@ impl Tensor { let jpeg = image::codecs::jpeg::JpegDecoder::new(std::io::Cursor::new(&jpeg_bytes))?; if jpeg.color_type() != image::ColorType::Rgb8 { // TODO(emilk): support gray-scale jpeg as well - return Err(TensorImageLoadError::UnsupportedJpegColorType( - jpeg.color_type(), - )); + return Err(TensorImageLoadError::UnsupportedJpegColorType(jpeg.color_type())); } let (w, h) = jpeg.dimensions(); @@ -710,7 +791,7 @@ impl Tensor { shape: vec![ TensorDimension::height(h as _), TensorDimension::width(w as _), - TensorDimension::depth(3), + TensorDimension::depth(3) ], data: TensorData::JPEG(jpeg_bytes.into()), meaning: TensorDataMeaning::Unknown, @@ -724,7 +805,7 @@ impl Tensor { /// /// This is a convenience function that calls [`DecodedTensor::from_image`]. pub fn from_image( - image: impl Into, + image: impl Into ) -> Result { Self::from_dynamic_image(image.into()) } @@ -740,21 +821,21 @@ impl Tensor { /// Predicts if [`Self::to_dynamic_image`] is likely to succeed, without doing anything expensive pub fn could_be_dynamic_image(&self) -> bool { - self.is_shaped_like_an_image() - && matches!( + self.is_shaped_like_an_image() && + matches!( self.dtype(), - TensorDataType::U8 - | TensorDataType::U16 - | TensorDataType::F16 - | TensorDataType::F32 - | TensorDataType::F64 + TensorDataType::U8 | + TensorDataType::U16 | + TensorDataType::F16 | + TensorDataType::F32 | + TensorDataType::F64 ) } /// Try to convert an image-like tensor into an [`image::DynamicImage`]. pub fn to_dynamic_image(&self) -> Result { - use ecolor::{gamma_u8_from_linear_f32, linear_u8_from_linear_f32}; - use image::{DynamicImage, GrayImage, RgbImage, RgbaImage}; + use ecolor::{ gamma_u8_from_linear_f32, linear_u8_from_linear_f32 }; + use image::{ DynamicImage, GrayImage, RgbImage, RgbaImage }; type Rgb16Image = image::ImageBuffer, Vec>; type Rgba16Image = image::ImageBuffer, Vec>; @@ -766,87 +847,85 @@ impl Tensor { let w = w as u32; let h = h as u32; - let dyn_img_result = - match (channels, &self.data) { - (1, TensorData::U8(buf)) => { - GrayImage::from_raw(w, h, buf.as_slice().to_vec()).map(DynamicImage::ImageLuma8) - } - (1, TensorData::U16(buf)) => Gray16Image::from_raw(w, h, buf.as_slice().to_vec()) - .map(DynamicImage::ImageLuma16), - // TODO(emilk) f16 - (1, TensorData::F32(buf)) => { - let pixels = buf - .iter() - .map(|pixel| gamma_u8_from_linear_f32(*pixel)) - .collect(); - GrayImage::from_raw(w, h, pixels).map(DynamicImage::ImageLuma8) - } - (1, TensorData::F64(buf)) => { - let pixels = buf - .iter() - .map(|&pixel| gamma_u8_from_linear_f32(pixel as f32)) - .collect(); - GrayImage::from_raw(w, h, pixels).map(DynamicImage::ImageLuma8) - } + let dyn_img_result = match (channels, &self.data) { + (1, TensorData::U8(buf)) => { + GrayImage::from_raw(w, h, buf.as_slice().to_vec()).map(DynamicImage::ImageLuma8) + } + (1, TensorData::U16(buf)) => + Gray16Image::from_raw(w, h, buf.as_slice().to_vec()).map(DynamicImage::ImageLuma16), + // TODO(emilk) f16 + (1, TensorData::F32(buf)) => { + let pixels = buf + .iter() + .map(|pixel| gamma_u8_from_linear_f32(*pixel)) + .collect(); + GrayImage::from_raw(w, h, pixels).map(DynamicImage::ImageLuma8) + } + (1, TensorData::F64(buf)) => { + let pixels = buf + .iter() + .map(|&pixel| gamma_u8_from_linear_f32(pixel as f32)) + .collect(); + GrayImage::from_raw(w, h, pixels).map(DynamicImage::ImageLuma8) + } - (3, TensorData::U8(buf)) => { - RgbImage::from_raw(w, h, buf.as_slice().to_vec()).map(DynamicImage::ImageRgb8) - } - (3, TensorData::U16(buf)) => Rgb16Image::from_raw(w, h, buf.as_slice().to_vec()) - .map(DynamicImage::ImageRgb16), - (3, TensorData::F32(buf)) => { - let pixels = buf.iter().copied().map(gamma_u8_from_linear_f32).collect(); - RgbImage::from_raw(w, h, pixels).map(DynamicImage::ImageRgb8) - } - (3, TensorData::F64(buf)) => { - let pixels = buf - .iter() - .map(|&comp| gamma_u8_from_linear_f32(comp as f32)) - .collect(); - RgbImage::from_raw(w, h, pixels).map(DynamicImage::ImageRgb8) - } + (3, TensorData::U8(buf)) => { + RgbImage::from_raw(w, h, buf.as_slice().to_vec()).map(DynamicImage::ImageRgb8) + } + (3, TensorData::U16(buf)) => + Rgb16Image::from_raw(w, h, buf.as_slice().to_vec()).map(DynamicImage::ImageRgb16), + (3, TensorData::F32(buf)) => { + let pixels = buf.iter().copied().map(gamma_u8_from_linear_f32).collect(); + RgbImage::from_raw(w, h, pixels).map(DynamicImage::ImageRgb8) + } + (3, TensorData::F64(buf)) => { + let pixels = buf + .iter() + .map(|&comp| gamma_u8_from_linear_f32(comp as f32)) + .collect(); + RgbImage::from_raw(w, h, pixels).map(DynamicImage::ImageRgb8) + } - (4, TensorData::U8(buf)) => { - RgbaImage::from_raw(w, h, buf.as_slice().to_vec()).map(DynamicImage::ImageRgba8) - } - (4, TensorData::U16(buf)) => Rgba16Image::from_raw(w, h, buf.as_slice().to_vec()) - .map(DynamicImage::ImageRgba16), - (4, TensorData::F32(buf)) => { - let rgba: &[[f32; 4]] = bytemuck::cast_slice(buf.as_slice()); - let pixels: Vec = rgba - .iter() - .flat_map(|&[r, g, b, a]| { - let r = gamma_u8_from_linear_f32(r); - let g = gamma_u8_from_linear_f32(g); - let b = gamma_u8_from_linear_f32(b); - let a = linear_u8_from_linear_f32(a); - [r, g, b, a] - }) - .collect(); - RgbaImage::from_raw(w, h, pixels).map(DynamicImage::ImageRgba8) - } - (4, TensorData::F64(buf)) => { - let rgba: &[[f64; 4]] = bytemuck::cast_slice(buf.as_slice()); - let pixels: Vec = rgba - .iter() - .flat_map(|&[r, g, b, a]| { - let r = gamma_u8_from_linear_f32(r as _); - let g = gamma_u8_from_linear_f32(g as _); - let b = gamma_u8_from_linear_f32(b as _); - let a = linear_u8_from_linear_f32(a as _); - [r, g, b, a] - }) - .collect(); - RgbaImage::from_raw(w, h, pixels).map(DynamicImage::ImageRgba8) - } + (4, TensorData::U8(buf)) => { + RgbaImage::from_raw(w, h, buf.as_slice().to_vec()).map(DynamicImage::ImageRgba8) + } + (4, TensorData::U16(buf)) => + Rgba16Image::from_raw(w, h, buf.as_slice().to_vec()).map(DynamicImage::ImageRgba16), + (4, TensorData::F32(buf)) => { + let rgba: &[[f32; 4]] = bytemuck::cast_slice(buf.as_slice()); + let pixels: Vec = rgba + .iter() + .flat_map(|&[r, g, b, a]| { + let r = gamma_u8_from_linear_f32(r); + let g = gamma_u8_from_linear_f32(g); + let b = gamma_u8_from_linear_f32(b); + let a = linear_u8_from_linear_f32(a); + [r, g, b, a] + }) + .collect(); + RgbaImage::from_raw(w, h, pixels).map(DynamicImage::ImageRgba8) + } + (4, TensorData::F64(buf)) => { + let rgba: &[[f64; 4]] = bytemuck::cast_slice(buf.as_slice()); + let pixels: Vec = rgba + .iter() + .flat_map(|&[r, g, b, a]| { + let r = gamma_u8_from_linear_f32(r as _); + let g = gamma_u8_from_linear_f32(g as _); + let b = gamma_u8_from_linear_f32(b as _); + let a = linear_u8_from_linear_f32(a as _); + [r, g, b, a] + }) + .collect(); + RgbaImage::from_raw(w, h, pixels).map(DynamicImage::ImageRgba8) + } - (_, _) => { - return Err(TensorImageSaveError::UnsupportedChannelsDtype( - channels, - self.data.dtype(), - )) - } - }; + (_, _) => { + return Err( + TensorImageSaveError::UnsupportedChannelsDtype(channels, self.data.dtype()) + ); + } + }; dyn_img_result.ok_or(TensorImageSaveError::BadData) } @@ -877,7 +956,7 @@ impl TryFrom for DecodedTensor { fn try_from(tensor: Tensor) -> Result { match &tensor.data { - TensorData::U8(_) + | TensorData::U8(_) | TensorData::U16(_) | TensorData::U32(_) | TensorData::U64(_) @@ -886,7 +965,8 @@ impl TryFrom for DecodedTensor { | TensorData::I32(_) | TensorData::I64(_) | TensorData::F32(_) - | TensorData::F64(_) => Ok(Self(tensor)), + | TensorData::F64(_) + | TensorData::NV12(_) => Ok(Self(tensor)), TensorData::JPEG(_) => Err(tensor), } @@ -899,7 +979,7 @@ impl DecodedTensor { /// /// Requires the `image` feature. pub fn from_image( - image: impl Into, + image: impl Into ) -> Result { Self::from_dynamic_image(image.into()) } @@ -908,7 +988,7 @@ impl DecodedTensor { /// /// Requires the `image` feature. pub fn from_dynamic_image( - image: image::DynamicImage, + image: image::DynamicImage ) -> Result { let (w, h) = (image.width(), image.height()); @@ -943,9 +1023,7 @@ impl DecodedTensor { } _ => { // It is very annoying that DynamicImage is #[non_exhaustive] - return Err(TensorImageLoadError::UnsupportedImageColorType( - image.color(), - )); + return Err(TensorImageLoadError::UnsupportedImageColorType(image.color())); } }; let tensor = Tensor { @@ -953,7 +1031,7 @@ impl DecodedTensor { shape: vec![ TensorDimension::height(h as _), TensorDimension::width(w as _), - TensorDimension::depth(depth), + TensorDimension::depth(depth) ], data, meaning: TensorDataMeaning::Unknown, @@ -964,9 +1042,9 @@ impl DecodedTensor { pub fn try_decode(maybe_encoded_tensor: Tensor) -> Result { crate::profile_function!(); - + // NV12 get's decoded in the shader, so we don't need to do anything here. match &maybe_encoded_tensor.data { - TensorData::U8(_) + | TensorData::U8(_) | TensorData::U16(_) | TensorData::U32(_) | TensorData::U64(_) @@ -975,7 +1053,8 @@ impl DecodedTensor { | TensorData::I32(_) | TensorData::I64(_) | TensorData::F32(_) - | TensorData::F64(_) => Ok(Self(maybe_encoded_tensor)), + | TensorData::F64(_) + | TensorData::NV12(_) => Ok(Self(maybe_encoded_tensor)), TensorData::JPEG(buf) => { use image::io::Reader as ImageReader; @@ -1037,7 +1116,7 @@ fn test_ndarray() { TensorDimension { size: 2, name: None, - }, + } ], data: TensorData::U16(vec![1, 2, 3, 4].into()), meaning: TensorDataMeaning::Unknown, @@ -1053,7 +1132,7 @@ fn test_ndarray() { #[test] fn test_arrow() { - use arrow2_convert::{deserialize::TryIntoCollection, serialize::TryIntoArrow}; + use arrow2_convert::{ deserialize::TryIntoCollection, serialize::TryIntoArrow }; let tensors_in = vec![ Tensor { @@ -1075,7 +1154,7 @@ fn test_arrow() { data: TensorData::F32(vec![1.23, 2.45].into()), meaning: TensorDataMeaning::Unknown, meter: None, - }, + } ]; let array: Box = tensors_in.iter().try_into_arrow().unwrap(); diff --git a/crates/re_log_types/src/component_types/xlink_stats.rs b/crates/re_log_types/src/component_types/xlink_stats.rs index 82b5f3b5522c..8b6da1b10d2e 100644 --- a/crates/re_log_types/src/component_types/xlink_stats.rs +++ b/crates/re_log_types/src/component_types/xlink_stats.rs @@ -5,13 +5,16 @@ use arrow2_convert::{field::I128, ArrowDeserialize, ArrowField, ArrowSerialize}; // TODO(filip): Convert to use i128 /// Stats about the XLink connection throughput -#[derive(Clone, Debug, PartialEq, ArrowField, ArrowSerialize, ArrowDeserialize)] +#[derive(Clone, Copy, Debug, PartialEq, ArrowField, ArrowSerialize, ArrowDeserialize)] pub struct XlinkStats { /// Bytes read from the XLink by the host (PC) pub bytes_read: i64, /// Bytes written to the XLink by the host (PC) pub bytes_written: i64, + + /// Time in s from epoch when the stats were collected + pub timestamp: f64, } impl XlinkStats { diff --git a/crates/re_log_types/src/lib.rs b/crates/re_log_types/src/lib.rs index af76f0b2c4a8..b23bc29b4ab0 100644 --- a/crates/re_log_types/src/lib.rs +++ b/crates/re_log_types/src/lib.rs @@ -273,6 +273,7 @@ impl std::fmt::Display for PythonVersion { } type SysExePath = String; +type VenvSitePackages = String; #[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -280,7 +281,7 @@ pub enum RecordingSource { Unknown, /// The official Rerun Python Logging SDK - PythonSdk(PythonVersion, SysExePath), + PythonSdk(PythonVersion, SysExePath, VenvSitePackages), /// The official Rerun Rust Logging SDK RustSdk { @@ -296,7 +297,7 @@ impl std::fmt::Display for RecordingSource { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Unknown => "Unknown".fmt(f), - Self::PythonSdk(version, _) => write!(f, "Python {version} SDK"), + Self::PythonSdk(version, _, _) => write!(f, "Python {version} SDK"), Self::RustSdk { rustc_version: rust_version, llvm_version: _, diff --git a/crates/re_memory/src/memory_limit.rs b/crates/re_memory/src/memory_limit.rs index ec59cb66e48f..1c54aabbe008 100644 --- a/crates/re_memory/src/memory_limit.rs +++ b/crates/re_memory/src/memory_limit.rs @@ -5,14 +5,14 @@ pub struct MemoryLimit { /// This is primarily compared to what is reported by [`crate::AccountingAllocator`] ('counted'). /// We limit based on this instead of `resident` (RSS) because `counted` is what we have immediate /// control over, while RSS depends on what our allocator (MiMalloc) decides to do. - /// Default is Some(100MB) + /// Default is Some(500MB) pub limit: Option, } impl Default for MemoryLimit { fn default() -> Self { Self { - limit: re_format::parse_bytes("100MB"), + limit: re_format::parse_bytes("500MB"), } } } diff --git a/crates/re_renderer/shader/decodings.wgsl b/crates/re_renderer/shader/decodings.wgsl new file mode 100644 index 000000000000..4e774a135f9a --- /dev/null +++ b/crates/re_renderer/shader/decodings.wgsl @@ -0,0 +1,19 @@ +#import <./types.wgsl> + +fn decode_nv12(texture: texture_2d, in_tex_coords: Vec2) -> Vec4 { + let texture_dim = Vec2(textureDimensions(texture).xy); + let uv_offset = u32(floor(texture_dim.y / 1.5)); + let uv_row = u32(floor(in_tex_coords.y * texture_dim.y) / 2.0); + var uv_col = u32(floor(in_tex_coords.x * texture_dim.x / 2.0)) * 2u; + + let coords = UVec2(in_tex_coords * Vec2(texture_dim.x, texture_dim.y)); + let y = (f32(textureLoad(texture, coords, 0).r) - 16.0) / 219.0; + let u = (f32(textureLoad(texture, UVec2(u32(uv_col), uv_offset + uv_row), 0).r) - 128.0) / 224.0; + let v = (f32(textureLoad(texture, UVec2((u32(uv_col) + 1u), uv_offset + uv_row), 0).r) - 128.0) / 224.0; + + // Get RGB values and apply reverse gamma correction since we are rendering to sRGB framebuffer + let r = pow(y + 1.402 * v, 2.2); + let g = pow(y - (0.344 * u + 0.714 * v), 2.2); + let b = pow(y + 1.772 * u, 2.2); + return Vec4(r, g, b, 1.0); +} diff --git a/crates/re_renderer/shader/depth_cloud.wgsl b/crates/re_renderer/shader/depth_cloud.wgsl index db39dd6d20d2..fbd49c85bb4f 100644 --- a/crates/re_renderer/shader/depth_cloud.wgsl +++ b/crates/re_renderer/shader/depth_cloud.wgsl @@ -10,6 +10,16 @@ #import <./utils/size.wgsl> #import <./utils/sphere_quad.wgsl> #import <./utils/srgb.wgsl> +#import <./decodings.wgsl> + +const SAMPLE_TYPE_FLOAT_FILTER = 1u; +const SAMPLE_TYPE_FLOAT_NOFILTER = 2u; +const SAMPLE_TYPE_SINT_NOFILTER = 3u; +const SAMPLE_TYPE_UINT_NOFILTER = 4u; +// ------------------- +// Encoded textures +// ------------------- +const SAMPLE_TYPE_NV12 = 5u; // --- @@ -43,8 +53,11 @@ struct DepthCloudInfo { /// Configures color mapping mode, see `colormap.wgsl`. colormap: u32, - /// Is the albedo texture rgb or mono - albedo_color_space: u32, + /// Configures the sample type for the albedo texture. + albedo_sample_type: u32, + + /// Uint or filterable float. + depth_sample_type: u32, /// Changes between the opaque and outline draw-phases. radius_boost_in_ui_points: f32, @@ -57,11 +70,27 @@ const ALBEDO_COLOR_MONO: u32 = 1u; var depth_cloud_info: DepthCloudInfo; @group(1) @binding(1) -var depth_texture: texture_2d; +var depth_texture_float: texture_2d; -/// Only sampled if `DepthCloudInfo::colormap == ALBEDO_TEXTURE`. @group(1) @binding(2) -var albedo_texture: texture_2d; +var depth_texture_uint: texture_2d; + +// ----------------------------------------------- +// Different kinds of albedo textures +// Only sampled when colormap == ALBEDO_TEXTURE +// ----------------------------------------------- +@group(1) @binding(3) +var albedo_texture_float_nofilter: texture_2d; + +@group(1) @binding(4) +var albedo_texture_sint: texture_2d; + +@group(1) @binding(5) +var albedo_texture_uint: texture_2d; + +@group(1) @binding(6) +var albedo_texture_float_filterable: texture_2d; + struct VertexOut { @builtin(position) @@ -93,11 +122,19 @@ struct PointData { // Backprojects the depth texture using the intrinsics passed in the uniform buffer. fn compute_point_data(quad_idx: u32) -> PointData { - let wh = textureDimensions(depth_texture); - let texcoords = UVec2(quad_idx % wh.x, quad_idx / wh.x); - - // TODO(cmc): expose knobs to linearize/normalize/flip/cam-to-plane depth. - let world_space_depth = depth_cloud_info.world_depth_from_texture_value * textureLoad(depth_texture, texcoords, 0).x; + var wh: UVec2; + var texcoords: UVec2; + var world_space_depth: f32 = depth_cloud_info.world_depth_from_texture_value; + if depth_cloud_info.depth_sample_type == SAMPLE_TYPE_FLOAT_FILTER { + wh = textureDimensions(depth_texture_float); + // TODO(cmc): expose knobs to linearize/normalize/flip/cam-to-plane depth. + texcoords = UVec2(quad_idx % wh.x, quad_idx / wh.x); + world_space_depth = world_space_depth * textureLoad(depth_texture_float, texcoords, 0).x; + } else { + wh = textureDimensions(depth_texture_uint); + texcoords = UVec2(quad_idx % wh.x, quad_idx / wh.x); + world_space_depth = world_space_depth * f32(textureLoad(depth_texture_uint, texcoords, 0).x); + } var data: PointData; if 0.0 < world_space_depth && world_space_depth < f32max { @@ -106,15 +143,24 @@ fn compute_point_data(quad_idx: u32) -> PointData { var color: Vec4; if depth_cloud_info.colormap == ALBEDO_TEXTURE { - color = textureSampleLevel( - albedo_texture, - trilinear_sampler, - Vec2(texcoords) / Vec2(textureDimensions(albedo_texture)), - 0.0 - ); - if depth_cloud_info.albedo_color_space == ALBEDO_COLOR_MONO { - color = Vec4(linear_from_srgb(Vec3(color.r)), 1.0); - } + if depth_cloud_info.albedo_sample_type == SAMPLE_TYPE_NV12 { + color = decode_nv12(albedo_texture_uint, Vec2(f32(texcoords.x), f32(texcoords.y)) / Vec2(f32(wh.x), f32(wh.x))); + } else { // TODO(filip): Support all sample types like in rectangle_fs.wgsl + if depth_cloud_info.depth_sample_type == SAMPLE_TYPE_FLOAT_FILTER { + color = textureSampleLevel( + depth_texture_float, + trilinear_sampler, + Vec2(texcoords) / Vec2(textureDimensions(depth_texture_float)), + 0.0 + ); + } else { + color = Vec4(textureLoad( + depth_texture_uint, + texcoords, + 0 + )) / 255.0; + } + } } else { color = Vec4(colormap_srgb(depth_cloud_info.colormap, world_space_depth), 1.0); } diff --git a/crates/re_renderer/shader/rectangle.wgsl b/crates/re_renderer/shader/rectangle.wgsl index afd0de119ca2..8459532fd753 100644 --- a/crates/re_renderer/shader/rectangle.wgsl +++ b/crates/re_renderer/shader/rectangle.wgsl @@ -10,6 +10,11 @@ const SAMPLE_TYPE_FLOAT_FILTER = 1u; const SAMPLE_TYPE_FLOAT_NOFILTER = 2u; const SAMPLE_TYPE_SINT_NOFILTER = 3u; const SAMPLE_TYPE_UINT_NOFILTER = 4u; +// ------------------ +// Encoded textures +// ------------------ +const SAMPLE_TYPE_NV12 = 5u; + // How do we do colormapping? const COLOR_MAPPER_OFF = 1u; diff --git a/crates/re_renderer/shader/rectangle_fs.wgsl b/crates/re_renderer/shader/rectangle_fs.wgsl index 62f65952fe21..8cb214285d93 100644 --- a/crates/re_renderer/shader/rectangle_fs.wgsl +++ b/crates/re_renderer/shader/rectangle_fs.wgsl @@ -1,4 +1,5 @@ #import <./rectangle.wgsl> +#import <./decodings.wgsl> fn is_magnifying(pixel_coord: Vec2) -> bool { return fwidth(pixel_coord.x) < 1.0; @@ -64,6 +65,8 @@ fn fs_main(in: VertexOut) -> @location(0) Vec4 { let bottom = mix(v01, v11, fract(coord.x)); sampled_value = mix(top, bottom, fract(coord.y)); } + } else if rect_info.sample_type == SAMPLE_TYPE_NV12 { + sampled_value = decode_nv12(texture_uint, in.texcoord); } else { return ERROR_RGBA; // unknown sample type } diff --git a/crates/re_renderer/shader/rectangle_vs.wgsl b/crates/re_renderer/shader/rectangle_vs.wgsl index e0758c17c23a..3d30c441ba0a 100644 --- a/crates/re_renderer/shader/rectangle_vs.wgsl +++ b/crates/re_renderer/shader/rectangle_vs.wgsl @@ -4,11 +4,14 @@ fn vs_main(@builtin(vertex_index) v_idx: u32) -> VertexOut { let texcoord = Vec2(f32(v_idx / 2u), f32(v_idx % 2u)); let pos = texcoord.x * rect_info.extent_u + texcoord.y * rect_info.extent_v + - rect_info.top_left_corner_position; + rect_info.top_left_corner_position; var out: VertexOut; out.position = apply_depth_offset(frame.projection_from_world * Vec4(pos, 1.0), rect_info.depth_offset); + // out.texcoord = (texcoord.x * rect_info.extent_u + texcoord.y * rect_info.extent_v).xy; out.texcoord = texcoord; - + if rect_info.sample_type == SAMPLE_TYPE_NV12 { + out.texcoord.y *= (2.0 / 3.0); + } return out; } diff --git a/crates/re_renderer/src/renderer/depth_cloud.rs b/crates/re_renderer/src/renderer/depth_cloud.rs index 676d37e45391..3d2bce3114f6 100644 --- a/crates/re_renderer/src/renderer/depth_cloud.rs +++ b/crates/re_renderer/src/renderer/depth_cloud.rs @@ -16,34 +16,60 @@ use smallvec::smallvec; use crate::{ allocator::create_and_fill_uniform_buffer_batch, - draw_phases::{DrawPhase, OutlineMaskProcessor}, + draw_phases::{ DrawPhase, OutlineMaskProcessor }, include_shader_module, - resource_managers::{GpuTexture2D, ResourceManagerError}, + resource_managers::{ GpuTexture2D, ResourceManagerError }, view_builder::ViewBuilder, wgpu_resources::{ - BindGroupDesc, BindGroupEntry, BindGroupLayoutDesc, GpuBindGroup, GpuBindGroupLayoutHandle, - GpuRenderPipelineHandle, GpuTexture, PipelineLayoutDesc, RenderPipelineDesc, TextureDesc, + BindGroupDesc, + BindGroupEntry, + BindGroupLayoutDesc, + GpuBindGroup, + GpuBindGroupLayoutHandle, + GpuRenderPipelineHandle, + GpuTexture, + PipelineLayoutDesc, + RenderPipelineDesc, + TextureDesc, }, - Colormap, OutlineMaskPreference, PickingLayerObjectId, PickingLayerProcessor, + Colormap, + OutlineMaskPreference, + PickingLayerObjectId, + PickingLayerProcessor, + texture_info, }; use super::{ - DrawData, FileResolver, FileSystem, RenderContext, Renderer, SharedRendererData, + DrawData, + FileResolver, + FileSystem, + RenderContext, + Renderer, + SharedRendererData, WgpuResourcePools, + ColormappedTexture, }; // --- - -#[derive(Debug, Clone, Copy)] -enum AlbedoColorSpace { - Rgb, - Mono, -} - mod gpu_data { - use crate::{wgpu_buffer_types, PickingLayerObjectId}; + use crate::{ + wgpu_buffer_types::{ self, U32RowPadded }, + PickingLayerObjectId, + renderer::TextureEncoding, + texture_info, + }; + + // Keep in sync with mirror in depth_cloud.wgsl - use super::{AlbedoColorSpace, DepthCloudAlbedoData}; + // Which texture to read from? + const SAMPLE_TYPE_FLOAT_FILTER: u32 = 1; + const SAMPLE_TYPE_FLOAT_NOFILTER: u32 = 2; + const SAMPLE_TYPE_SINT_NOFILTER: u32 = 3; + const SAMPLE_TYPE_UINT_NOFILTER: u32 = 4; + // ------------------ + // Encoded textures + // ------------------ + const SAMPLE_TYPE_NV12: u32 = 5; /// Keep in sync with mirror in `depth_cloud.wgsl.` #[repr(C, align(256))] @@ -69,19 +95,23 @@ mod gpu_data { /// Which colormap should be used. pub colormap: u32, - /// Is the albedo texture rgb or mono - pub albedo_color_space: wgpu_buffer_types::U32RowPadded, + /// Which texture sample to use + pub albedo_sample_type: U32RowPadded, + + /// Which texture sample to use + pub depth_sample_type: U32RowPadded, /// Changes over different draw-phases. pub radius_boost_in_ui_points: wgpu_buffer_types::F32RowPadded, - pub end_padding: [wgpu_buffer_types::PaddingRow; 16 - 4 - 3 - 1 - 1 - 1 - 1], + pub end_padding: [wgpu_buffer_types::PaddingRow; 16 - 4 - 3 - 1 - 1 - 1 - 1 - 1], } impl DepthCloudInfoUBO { pub fn from_depth_cloud( radius_boost_in_ui_points: f32, depth_cloud: &super::DepthCloud, + device_features: wgpu::Features ) -> Self { let super::DepthCloud { world_from_obj, @@ -90,14 +120,62 @@ mod gpu_data { point_radius_from_world_depth, max_depth_in_world, depth_dimensions: _, - depth_texture: _, + depth_texture, colormap, outline_mask_id, picking_object_id, - albedo_dimensions: _, - albedo_data: _, + albedo_texture, } = depth_cloud; + let albedo_sample_type = match albedo_texture { + Some(colormapped_texture) => { + match colormapped_texture.texture.format().sample_type(None) { + Some(wgpu::TextureSampleType::Float { .. }) => + match colormapped_texture.encoding { + Some(TextureEncoding::Nv12) => SAMPLE_TYPE_NV12, + _ => { + if + texture_info::is_float_filterable( + colormapped_texture.texture.format(), + device_features + ) + { + SAMPLE_TYPE_FLOAT_FILTER + } else { + SAMPLE_TYPE_FLOAT_NOFILTER + } + } + } + Some(wgpu::TextureSampleType::Uint) => { + match colormapped_texture.encoding { + Some(TextureEncoding::Nv12) => SAMPLE_TYPE_NV12, + _ => SAMPLE_TYPE_UINT_NOFILTER, + } + } + Some(wgpu::TextureSampleType::Sint) => SAMPLE_TYPE_SINT_NOFILTER, + _ => 0, + } + } + _ => { 0 } + }; + + let depth_sample_type = match depth_texture.texture.format().sample_type(None) { + Some(wgpu::TextureSampleType::Float { .. }) => { + if + texture_info::is_float_filterable( + depth_texture.texture.format(), + device_features + ) + { + SAMPLE_TYPE_FLOAT_FILTER + } else { + SAMPLE_TYPE_FLOAT_NOFILTER + } + } + Some(wgpu::TextureSampleType::Uint) => SAMPLE_TYPE_UINT_NOFILTER, + _ => panic!("Depth texture must be float or uint"), + }; + Self { world_from_obj: (*world_from_obj).into(), depth_camera_intrinsics: (*depth_camera_intrinsics).into(), @@ -106,36 +184,16 @@ mod gpu_data { point_radius_from_world_depth: *point_radius_from_world_depth, max_depth_in_world: *max_depth_in_world, colormap: *colormap as u32, - albedo_color_space: (depth_cloud - .albedo_data - .as_ref() - .map(|albedo_data| match albedo_data { - DepthCloudAlbedoData::Mono8(_) => AlbedoColorSpace::Mono, - _ => AlbedoColorSpace::Rgb, - }) - .unwrap_or(AlbedoColorSpace::Rgb) as u32) - .into(), radius_boost_in_ui_points: radius_boost_in_ui_points.into(), picking_layer_object_id: *picking_object_id, + albedo_sample_type: albedo_sample_type.into(), + depth_sample_type: depth_sample_type.into(), end_padding: Default::default(), } } } } -/// The raw data for the (optional) albedo texture. -// -// TODO(cmc): support more albedo data types. -// TODO(cmc): arrow buffers for u8... -#[derive(Debug, Clone)] -pub enum DepthCloudAlbedoData { - Rgb8(Vec), - Rgb8Srgb(Vec), - Rgba8(Vec), - Rgba8Srgb(Vec), - Mono8(Vec), -} - pub struct DepthCloud { /// The extrinsics of the camera used for the projection. pub world_from_obj: glam::Mat4, @@ -160,7 +218,7 @@ pub struct DepthCloud { /// The actual data for the depth texture. /// /// Only textures with sample type `Float` are supported. - pub depth_texture: GpuTexture2D, + pub depth_texture: ColormappedTexture, /// Configures color mapping mode. pub colormap: Colormap, @@ -171,15 +229,7 @@ pub struct DepthCloud { /// Picking object id that applies for the entire depth cloud. pub picking_object_id: PickingLayerObjectId, - /// The dimensions of the (optional) albedo texture in pixels. - /// - /// Irrelevant if [`Self::albedo_data`] isn't set. - pub albedo_dimensions: glam::UVec2, - - /// The actual data for the (optional) albedo texture. - /// - /// If set, takes precedence over [`Self::colormap`]. - pub albedo_data: Option, + pub albedo_texture: Option, } impl DepthCloud { @@ -237,35 +287,33 @@ impl DrawData for DepthCloudDrawData { #[derive(thiserror::Error, Debug)] pub enum DepthCloudDrawDataError { - #[error("Depth texture format was {0:?}, only formats with sample type float are supported")] - InvalidDepthTextureFormat(wgpu::TextureFormat), + #[error( + "Depth texture format was {0:?}, only formats with sample type float are supported" + )] InvalidDepthTextureFormat(wgpu::TextureFormat), - #[error(transparent)] - ResourceManagerError(#[from] ResourceManagerError), + #[error("Invalid albedo texture format {0:?}")] InvalidAlbedoTextureFormat(wgpu::TextureFormat), + + #[error(transparent)] ResourceManagerError(#[from] ResourceManagerError), } impl DepthCloudDrawData { pub fn new( ctx: &mut RenderContext, - depth_clouds: &DepthClouds, + depth_clouds: &DepthClouds ) -> Result { crate::profile_function!(); - let DepthClouds { - clouds: depth_clouds, - radius_boost_in_ui_points_for_outlines, - } = depth_clouds; + let DepthClouds { clouds: depth_clouds, radius_boost_in_ui_points_for_outlines } = + depth_clouds; - let bg_layout = ctx - .renderers + let bg_layout = ctx.renderers .write() .get_or_create::<_, DepthCloudRenderer>( &ctx.shared_renderer_data, &mut ctx.gpu_resources, &ctx.device, - &mut ctx.resolver, - ) - .bind_group_layout; + &mut ctx.resolver + ).bind_group_layout; if depth_clouds.is_empty() { return Ok(DepthCloudDrawData { @@ -276,91 +324,84 @@ impl DepthCloudDrawData { let depth_cloud_ubo_binding_outlines = create_and_fill_uniform_buffer_batch( ctx, "depth_cloud_ubos".into(), - depth_clouds.iter().map(|dc| { - gpu_data::DepthCloudInfoUBO::from_depth_cloud( - *radius_boost_in_ui_points_for_outlines, - dc, - ) - }), + depth_clouds + .iter() + .map(|dc| { + gpu_data::DepthCloudInfoUBO::from_depth_cloud( + *radius_boost_in_ui_points_for_outlines, + dc, + ctx.device.features() + ) + }) ); let depth_cloud_ubo_binding_opaque = create_and_fill_uniform_buffer_batch( ctx, "depth_cloud_ubos".into(), depth_clouds .iter() - .map(|dc| gpu_data::DepthCloudInfoUBO::from_depth_cloud(0.0, dc)), + .map(|dc| + gpu_data::DepthCloudInfoUBO::from_depth_cloud(0.0, dc, ctx.device.features()) + ) ); let mut instances = Vec::with_capacity(depth_clouds.len()); + let mut albedo_texture_float_filterable = + ctx.texture_manager_2d.zeroed_texture_float().handle; + let mut albedo_texture_float_nofilter = + ctx.texture_manager_2d.zeroed_texture_float().handle; + let mut albedo_texture_sint = ctx.texture_manager_2d.zeroed_texture_sint().handle; + let mut albedo_texture_uint = ctx.texture_manager_2d.zeroed_texture_uint().handle; + + let mut depth_texture_float = ctx.texture_manager_2d.zeroed_texture_float().handle; + let mut depth_texture_uint = ctx.texture_manager_2d.zeroed_texture_uint().handle; + for (depth_cloud, ubo_outlines, ubo_opaque) in itertools::izip!( depth_clouds, depth_cloud_ubo_binding_outlines, depth_cloud_ubo_binding_opaque ) { - if !matches!( - depth_cloud.depth_texture.format().sample_type(None), - Some(wgpu::TextureSampleType::Float { filterable: _ }) - ) { - return Err(DepthCloudDrawDataError::InvalidDepthTextureFormat( - depth_cloud.depth_texture.format(), - )); + let depth_texture = &depth_cloud.depth_texture.texture; + let depth_texture_format = depth_texture.creation_desc.format; + match depth_texture_format.sample_type(None) { + Some(wgpu::TextureSampleType::Float { .. }) => { + depth_texture_float = depth_texture.handle; + } + Some(wgpu::TextureSampleType::Uint) => { + depth_texture_uint = depth_texture.handle; + } + _ => { + return Err( + DepthCloudDrawDataError::InvalidDepthTextureFormat(depth_texture_format) + ); + } } - let albedo_texture = depth_cloud - .albedo_data - .as_ref() - .map_or_else(|| { - create_and_upload_texture( - ctx, - (1, 1).into(), - wgpu::TextureFormat::Rgba8Unorm, - [0u8; 4].as_slice(), - ) - }, |data| match data { - DepthCloudAlbedoData::Rgba8(data) => create_and_upload_texture( - ctx, - depth_cloud.albedo_dimensions, - wgpu::TextureFormat::Rgba8Unorm, - data.as_slice(), - ), - DepthCloudAlbedoData::Rgba8Srgb(data) => create_and_upload_texture( - ctx, - depth_cloud.albedo_dimensions, - wgpu::TextureFormat::Rgba8UnormSrgb, - data.as_slice(), - ), - DepthCloudAlbedoData::Rgb8(data) => { - let data = data - .chunks(3) - .into_iter() - .flat_map(|c| [c[0], c[1], c[2], 255]) - .collect_vec(); - create_and_upload_texture( - ctx, - depth_cloud.albedo_dimensions, - wgpu::TextureFormat::Rgba8Unorm, - data.as_slice(), - ) + if + let Some(albedo_texture) = depth_cloud.albedo_texture + .as_ref() + .and_then(|t| Some(&t.texture)) + { + let texture_format = albedo_texture.creation_desc.format; + match texture_format.sample_type(None) { + Some(wgpu::TextureSampleType::Float { .. }) => { + if texture_info::is_float_filterable(texture_format, ctx.device.features()) { + albedo_texture_float_filterable = albedo_texture.handle; + } else { + albedo_texture_float_nofilter = albedo_texture.handle; + } } - DepthCloudAlbedoData::Rgb8Srgb(data) => { - let data = data - .chunks(3) - .into_iter() - .flat_map(|c| [c[0], c[1], c[2], 255]) - .collect_vec(); - create_and_upload_texture( - ctx, - depth_cloud.albedo_dimensions, - wgpu::TextureFormat::Rgba8UnormSrgb, - data.as_slice(), - ) + Some(wgpu::TextureSampleType::Sint) => { + albedo_texture_sint = albedo_texture.handle; + } + Some(wgpu::TextureSampleType::Uint) => { + albedo_texture_uint = albedo_texture.handle; + } + _ => { + return Err( + DepthCloudDrawDataError::InvalidAlbedoTextureFormat(texture_format) + ); } - DepthCloudAlbedoData::Mono8(data) => create_and_upload_texture( - ctx, - depth_cloud.albedo_dimensions, - wgpu::TextureFormat::R8Unorm, - data.as_slice(), - ), - }); + } + } let mk_bind_group = |label, ubo: BindGroupEntry| { ctx.gpu_resources.bind_groups.alloc( @@ -370,11 +411,15 @@ impl DepthCloudDrawData { label, entries: smallvec![ ubo, - BindGroupEntry::DefaultTextureView(depth_cloud.depth_texture.handle), - BindGroupEntry::DefaultTextureView(albedo_texture.handle) + BindGroupEntry::DefaultTextureView(depth_texture_float), + BindGroupEntry::DefaultTextureView(depth_texture_uint), + BindGroupEntry::DefaultTextureView(albedo_texture_float_nofilter), + BindGroupEntry::DefaultTextureView(albedo_texture_sint), + BindGroupEntry::DefaultTextureView(albedo_texture_uint), + BindGroupEntry::DefaultTextureView(albedo_texture_float_filterable) ], layout: bg_layout, - }), + }) ) }; @@ -393,54 +438,6 @@ impl DepthCloudDrawData { } } -fn create_and_upload_texture( - ctx: &RenderContext, - dimensions: glam::UVec2, - format: wgpu::TextureFormat, - data: &[T], -) -> GpuTexture { - crate::profile_function!(); - - let texture_size = wgpu::Extent3d { - width: dimensions.x, - height: dimensions.y, - depth_or_array_layers: 1, - }; - let texture_desc = TextureDesc { - label: "texture".into(), - size: texture_size, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format, - usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, - }; - let texture = ctx.gpu_resources.textures.alloc(&ctx.device, &texture_desc); - - let format_info = texture_desc.format; - let width_blocks = dimensions.x / (format_info.block_dimensions().0 as u32); - - let mut texture_staging = ctx.cpu_write_gpu_read_belt.lock().allocate::( - &ctx.device, - &ctx.gpu_resources.buffers, - data.len(), - ); - texture_staging.extend_from_slice(data); - - texture_staging.copy_to_texture2d( - ctx.active_frame.before_view_builder_encoder.lock().get(), - wgpu::ImageCopyTexture { - texture: &texture.inner.texture, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - glam::UVec2::new(texture_size.width, texture_size.height), - ); - - texture -} - pub struct DepthCloudRenderer { render_pipeline_color: GpuRenderPipelineHandle, render_pipeline_picking_layer: GpuRenderPipelineHandle, @@ -452,18 +449,14 @@ impl Renderer for DepthCloudRenderer { type RendererDrawData = DepthCloudDrawData; fn participated_phases() -> &'static [DrawPhase] { - &[ - DrawPhase::Opaque, - DrawPhase::PickingLayer, - DrawPhase::OutlineMask, - ] + &[DrawPhase::Opaque, DrawPhase::PickingLayer, DrawPhase::OutlineMask] } fn create_renderer( shared_data: &SharedRendererData, pools: &mut WgpuResourcePools, device: &wgpu::Device, - resolver: &mut FileResolver, + resolver: &mut FileResolver ) -> Self { crate::profile_function!(); @@ -478,8 +471,9 @@ impl Renderer for DepthCloudRenderer { ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, - min_binding_size: (std::mem::size_of::() - as u64) + min_binding_size: ( + std::mem::size_of::() as u64 + ) .try_into() .ok(), }, @@ -489,7 +483,7 @@ impl Renderer for DepthCloudRenderer { binding: 1, visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: false }, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, @@ -499,14 +493,54 @@ impl Renderer for DepthCloudRenderer { binding: 2, visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: true }, + sample_type: wgpu::TextureSampleType::Uint, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: false }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, count: None, }, + wgpu::BindGroupLayoutEntry { + binding: 4, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Sint, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 5, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Uint, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 6, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + } ], - }), + }) ); let pipeline_layout = pools.pipeline_layouts.get_or_create( @@ -515,13 +549,13 @@ impl Renderer for DepthCloudRenderer { label: "depth_cloud_rp_layout".into(), entries: vec![shared_data.global_bindings.layout, bind_group_layout], }), - &pools.bind_group_layouts, + &pools.bind_group_layouts ); let shader_module = pools.shader_modules.get_or_create( device, resolver, - &include_shader_module!("../../shader/depth_cloud.wgsl"), + &include_shader_module!("../../shader/depth_cloud.wgsl") ); let render_pipeline_desc_color = RenderPipelineDesc { @@ -549,7 +583,7 @@ impl Renderer for DepthCloudRenderer { device, &render_pipeline_desc_color, &pools.pipeline_layouts, - &pools.shader_modules, + &pools.shader_modules ); let render_pipeline_picking_layer = pools.render_pipelines.get_or_create( device, @@ -562,7 +596,7 @@ impl Renderer for DepthCloudRenderer { ..render_pipeline_desc_color.clone() }), &pools.pipeline_layouts, - &pools.shader_modules, + &pools.shader_modules ); let render_pipeline_outline_mask = pools.render_pipelines.get_or_create( device, @@ -573,12 +607,12 @@ impl Renderer for DepthCloudRenderer { depth_stencil: OutlineMaskProcessor::MASK_DEPTH_STATE, // Alpha to coverage doesn't work with the mask integer target. multisample: OutlineMaskProcessor::mask_default_msaa_state( - shared_data.config.hardware_tier, + shared_data.config.hardware_tier ), ..render_pipeline_desc_color }), &pools.pipeline_layouts, - &pools.shader_modules, + &pools.shader_modules ); DepthCloudRenderer { @@ -594,7 +628,7 @@ impl Renderer for DepthCloudRenderer { pools: &'a WgpuResourcePools, phase: DrawPhase, pass: &mut wgpu::RenderPass<'a>, - draw_data: &'a Self::RendererDrawData, + draw_data: &'a Self::RendererDrawData ) -> anyhow::Result<()> { crate::profile_function!(); if draw_data.instances.is_empty() { diff --git a/crates/re_renderer/src/renderer/mod.rs b/crates/re_renderer/src/renderer/mod.rs index b8b2bc508968..9e900498944c 100644 --- a/crates/re_renderer/src/renderer/mod.rs +++ b/crates/re_renderer/src/renderer/mod.rs @@ -15,7 +15,7 @@ pub use point_cloud::{ mod depth_cloud; pub use self::depth_cloud::{ - DepthCloud, DepthCloudAlbedoData, DepthCloudDrawData, DepthCloudRenderer, DepthClouds, + DepthCloud, DepthCloudDrawData, DepthCloudRenderer, DepthClouds, }; mod test_triangle; @@ -24,7 +24,7 @@ pub use test_triangle::TestTriangleDrawData; mod rectangles; pub use rectangles::{ ColorMapper, ColormappedTexture, RectangleDrawData, RectangleOptions, TextureFilterMag, - TextureFilterMin, TexturedRect, + TextureFilterMin, TexturedRect, TextureEncoding }; mod mesh_renderer; diff --git a/crates/re_renderer/src/renderer/rectangles.rs b/crates/re_renderer/src/renderer/rectangles.rs index 8b380a30a2bd..3a84ab0057ca 100644 --- a/crates/re_renderer/src/renderer/rectangles.rs +++ b/crates/re_renderer/src/renderer/rectangles.rs @@ -10,26 +10,41 @@ //! Since we're not allowed to bind many textures at once (no widespread bindless support!), //! we are forced to have individual bind groups per rectangle and thus a draw call per rectangle. -use itertools::{izip, Itertools as _}; +use itertools::{ izip, Itertools as _ }; use smallvec::smallvec; use crate::{ allocator::create_and_fill_uniform_buffer_batch, depth_offset::DepthOffset, - draw_phases::{DrawPhase, OutlineMaskProcessor}, + draw_phases::{ DrawPhase, OutlineMaskProcessor }, include_shader_module, - resource_managers::{GpuTexture2D, ResourceManagerError}, + resource_managers::{ GpuTexture2D, ResourceManagerError }, texture_info, view_builder::ViewBuilder, wgpu_resources::{ - BindGroupDesc, BindGroupEntry, BindGroupLayoutDesc, GpuBindGroup, GpuBindGroupLayoutHandle, - GpuRenderPipelineHandle, PipelineLayoutDesc, RenderPipelineDesc, SamplerDesc, + BindGroupDesc, + BindGroupEntry, + BindGroupLayoutDesc, + GpuBindGroup, + GpuBindGroupLayoutHandle, + GpuRenderPipelineHandle, + PipelineLayoutDesc, + RenderPipelineDesc, + SamplerDesc, }, - Colormap, OutlineMaskPreference, PickingLayerProcessor, Rgba, + Colormap, + OutlineMaskPreference, + PickingLayerProcessor, + Rgba, }; use super::{ - DrawData, FileResolver, FileSystem, RenderContext, Renderer, SharedRendererData, + DrawData, + FileResolver, + FileSystem, + RenderContext, + Renderer, + SharedRendererData, WgpuResourcePools, }; @@ -49,11 +64,21 @@ pub enum TextureFilterMin { // TODO(andreas): Offer mipmapping here? } +#[derive(Clone, Debug, PartialEq)] +pub enum TextureEncoding { + Mono, + Rgb, + Rgba, + Nv12, +} + /// Describes a texture and how to map it to a color. #[derive(Clone)] pub struct ColormappedTexture { pub texture: GpuTexture2D, + pub encoding: Option, + /// Min/max range of the values in the texture. /// Used to normalize the input values (squash them to the 0-1 range). pub range: [f32; 2], @@ -95,6 +120,22 @@ impl ColormappedTexture { range: [0.0, 1.0], gamma: 1.0, color_mapper: None, + encoding: None, + } + } + + /// Calculate the real texture width and height, + /// taking into account the texture's encoding. + pub fn width_height(&self) -> [u32; 2] { + let texture_dim = self.texture.width_height(); + match &self.encoding { + &Some(TextureEncoding::Nv12) => { + let real_dim = + glam::Vec2::new(texture_dim[0] as f32, texture_dim[1] as f32) * + glam::Vec2::new(1.0, 2.0 / 3.0); + [real_dim.x as u32, real_dim.y as u32] + } + _ => texture_dim, } } } @@ -144,11 +185,9 @@ impl Default for RectangleOptions { #[derive(thiserror::Error, Debug)] pub enum RectangleError { - #[error(transparent)] - ResourceManagerError(#[from] ResourceManagerError), + #[error(transparent)] ResourceManagerError(#[from] ResourceManagerError), - #[error("Texture required special features: {0:?}")] - SpecialFeatures(wgpu::Features), + #[error("Texture required special features: {0:?}")] SpecialFeatures(wgpu::Features), // There's really no need for users to be able to sample depth textures. // We don't get filtering of depth textures any way. @@ -158,20 +197,21 @@ pub enum RectangleError { #[error("Color mapping is being applied to a four-component RGBA texture")] ColormappingRgbaTexture, - #[error("Only 1 and 4 component textures are supported, got {0} components")] - UnsupportedComponentCount(u8), + #[error( + "Only 1 and 4 component textures are supported, got {0} components" + )] UnsupportedComponentCount(u8), #[error("No color mapper was supplied for this 1-component texture")] MissingColorMapper, - #[error("Invalid color map texture format: {0:?}")] - UnsupportedColormapTextureFormat(wgpu::TextureFormat), + #[error("Invalid color map texture format: {0:?}")] UnsupportedColormapTextureFormat( + wgpu::TextureFormat, + ), } mod gpu_data { - use crate::{texture_info, wgpu_buffer_types}; - - use super::{ColorMapper, RectangleError, TexturedRect}; + use super::{ ColorMapper, RectangleError, TextureEncoding, TexturedRect }; + use crate::{ texture_info, wgpu_buffer_types }; // Keep in sync with mirror in rectangle.wgsl @@ -180,6 +220,10 @@ mod gpu_data { const SAMPLE_TYPE_FLOAT_NOFILTER: u32 = 2; const SAMPLE_TYPE_SINT_NOFILTER: u32 = 3; const SAMPLE_TYPE_UINT_NOFILTER: u32 = 4; + // ------------------ + // Encoded textures + // ------------------ + const SAMPLE_TYPE_NV12: u32 = 5; // How do we do colormapping? const COLOR_MAPPER_OFF: u32 = 1; @@ -219,7 +263,7 @@ mod gpu_data { impl UniformBuffer { pub fn from_textured_rect( rectangle: &super::TexturedRect, - device_features: wgpu::Features, + device_features: wgpu::Features ) -> Result { let texture_format = rectangle.colormapped_texture.texture.format(); @@ -236,6 +280,7 @@ mod gpu_data { range, gamma, color_mapper, + encoding, } = colormapped_texture; let super::RectangleOptions { @@ -247,15 +292,20 @@ mod gpu_data { } = options; let sample_type = match texture_format.sample_type(None) { - Some(wgpu::TextureSampleType::Float { .. }) => { - if texture_info::is_float_filterable(texture_format, device_features) { - SAMPLE_TYPE_FLOAT_FILTER - } else { - SAMPLE_TYPE_FLOAT_NOFILTER - } + Some(wgpu::TextureSampleType::Float { .. }) => if + texture_info::is_float_filterable(texture_format, device_features) + { + SAMPLE_TYPE_FLOAT_FILTER + } else { + SAMPLE_TYPE_FLOAT_NOFILTER } Some(wgpu::TextureSampleType::Sint) => SAMPLE_TYPE_SINT_NOFILTER, - Some(wgpu::TextureSampleType::Uint) => SAMPLE_TYPE_UINT_NOFILTER, + Some(wgpu::TextureSampleType::Uint) => { + match encoding { + Some(TextureEncoding::Nv12) => SAMPLE_TYPE_NV12, + _ => SAMPLE_TYPE_UINT_NOFILTER, + } + } _ => { return Err(RectangleError::DepthTexturesNotSupported); } @@ -265,18 +315,22 @@ mod gpu_data { let color_mapper_int; match texture_info::num_texture_components(texture_format) { - 1 => match color_mapper { - Some(ColorMapper::Function(colormap)) => { - color_mapper_int = COLOR_MAPPER_FUNCTION; - colormap_function = *colormap as u32; - } - Some(ColorMapper::Texture(_)) => { - color_mapper_int = COLOR_MAPPER_TEXTURE; - } - None => { - return Err(RectangleError::MissingColorMapper); + 1 => + match color_mapper { + Some(ColorMapper::Function(colormap)) => { + color_mapper_int = COLOR_MAPPER_FUNCTION; + colormap_function = *colormap as u32; + } + Some(ColorMapper::Texture(_)) => { + color_mapper_int = COLOR_MAPPER_TEXTURE; + } + None => { + if encoding != &Some(TextureEncoding::Nv12) { + return Err(RectangleError::MissingColorMapper); + } + color_mapper_int = COLOR_MAPPER_OFF; + } } - }, 4 => { if color_mapper.is_some() { return Err(RectangleError::ColormappingRgbaTexture); @@ -285,7 +339,7 @@ mod gpu_data { } } num_components => { - return Err(RectangleError::UnsupportedComponentCount(num_components)) + return Err(RectangleError::UnsupportedComponentCount(num_components)); } } @@ -297,7 +351,6 @@ mod gpu_data { super::TextureFilterMag::Linear => FILTER_BILINEAR, super::TextureFilterMag::Nearest => FILTER_NEAREST, }; - Ok(Self { top_left_corner_position: (*top_left_corner_position).into(), colormap_function, @@ -336,7 +389,7 @@ impl DrawData for RectangleDrawData { impl RectangleDrawData { pub fn new( ctx: &mut RenderContext, - rectangles: &[TexturedRect], + rectangles: &[TexturedRect] ) -> Result { crate::profile_function!(); @@ -345,7 +398,7 @@ impl RectangleDrawData { &ctx.shared_renderer_data, &mut ctx.gpu_resources, &ctx.device, - &mut ctx.resolver, + &mut ctx.resolver ); if rectangles.is_empty() { @@ -363,7 +416,7 @@ impl RectangleDrawData { let uniform_buffer_bindings = create_and_fill_uniform_buffer_batch( ctx, "rectangle uniform buffers".into(), - uniform_buffers.into_iter(), + uniform_buffers.into_iter() ); let mut instances = Vec::with_capacity(rectangles.len()); @@ -371,12 +424,12 @@ impl RectangleDrawData { let options = &rectangle.options; let sampler = ctx.gpu_resources.samplers.get_or_create( &ctx.device, - &SamplerDesc { + &(SamplerDesc { label: format!( "rectangle sampler mag {:?} min {:?}", - options.texture_filter_magnification, options.texture_filter_minification - ) - .into(), + options.texture_filter_magnification, + options.texture_filter_minification + ).into(), mag_filter: match options.texture_filter_magnification { TextureFilterMag::Linear => wgpu::FilterMode::Linear, TextureFilterMag::Nearest => wgpu::FilterMode::Nearest, @@ -387,15 +440,13 @@ impl RectangleDrawData { }, mipmap_filter: wgpu::FilterMode::Nearest, ..Default::default() - }, + }) ); let texture = &rectangle.colormapped_texture.texture; let texture_format = texture.creation_desc.format; if texture_format.required_features() != Default::default() { - return Err(RectangleError::SpecialFeatures( - texture_format.required_features(), - )); + return Err(RectangleError::SpecialFeatures(texture_format.required_features())); } // We set up several texture sources, then instruct the shader to read from at most one of them. @@ -424,8 +475,8 @@ impl RectangleDrawData { } // We also set up an optional colormap texture. - let colormap_texture = if let Some(ColorMapper::Texture(handle)) = - &rectangle.colormapped_texture.color_mapper + let colormap_texture = if + let Some(ColorMapper::Texture(handle)) = &rectangle.colormapped_texture.color_mapper { let format = handle.format(); if format != wgpu::TextureFormat::Rgba8UnormSrgb { @@ -440,7 +491,7 @@ impl RectangleDrawData { bind_group: ctx.gpu_resources.bind_groups.alloc( &ctx.device, &ctx.gpu_resources, - &BindGroupDesc { + &(BindGroupDesc { label: "RectangleInstance::bind_group".into(), entries: smallvec![ uniform_buffer, @@ -449,10 +500,10 @@ impl RectangleDrawData { BindGroupEntry::DefaultTextureView(texture_sint), BindGroupEntry::DefaultTextureView(texture_uint), BindGroupEntry::DefaultTextureView(colormap_texture), - BindGroupEntry::DefaultTextureView(texture_float_filterable), + BindGroupEntry::DefaultTextureView(texture_float_filterable) ], layout: rectangle_renderer.bind_group_layout, - }, + }) ), draw_outline_mask: rectangle.options.outline_mask.is_some(), }); @@ -476,13 +527,13 @@ impl Renderer for RectangleRenderer { shared_data: &SharedRendererData, pools: &mut WgpuResourcePools, device: &wgpu::Device, - resolver: &mut FileResolver, + resolver: &mut FileResolver ) -> Self { crate::profile_function!(); let bind_group_layout = pools.bind_group_layouts.get_or_create( device, - &BindGroupLayoutDesc { + &(BindGroupLayoutDesc { label: "RectangleRenderer::bind_group_layout".into(), entries: vec![ wgpu::BindGroupLayoutEntry { @@ -493,8 +544,9 @@ impl Renderer for RectangleRenderer { // We could use dynamic offset here into a single large buffer. // But we have to set a new texture anyways and its doubtful that splitting the bind group is of any use. has_dynamic_offset: false, - min_binding_size: (std::mem::size_of::() - as u64) + min_binding_size: ( + std::mem::size_of::() as u64 + ) .try_into() .ok(), }, @@ -561,29 +613,29 @@ impl Renderer for RectangleRenderer { multisampled: false, }, count: None, - }, + } ], - }, + }) ); let pipeline_layout = pools.pipeline_layouts.get_or_create( device, - &PipelineLayoutDesc { + &(PipelineLayoutDesc { label: "RectangleRenderer::pipeline_layout".into(), entries: vec![shared_data.global_bindings.layout, bind_group_layout], - }, - &pools.bind_group_layouts, + }), + &pools.bind_group_layouts ); let shader_module_vs = pools.shader_modules.get_or_create( device, resolver, - &include_shader_module!("../../shader/rectangle_vs.wgsl"), + &include_shader_module!("../../shader/rectangle_vs.wgsl") ); let shader_module_fs = pools.shader_modules.get_or_create( device, resolver, - &include_shader_module!("../../shader/rectangle_fs.wgsl"), + &include_shader_module!("../../shader/rectangle_fs.wgsl") ); let render_pipeline_desc_color = RenderPipelineDesc { @@ -594,12 +646,14 @@ impl Renderer for RectangleRenderer { fragment_entrypoint: "fs_main".into(), fragment_handle: shader_module_fs, vertex_buffers: smallvec![], - render_targets: smallvec![Some(wgpu::ColorTargetState { - format: ViewBuilder::MAIN_TARGET_COLOR_FORMAT, - // TODO(andreas): have two render pipelines, an opaque one and a transparent one. Transparent shouldn't write depth! - blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], + render_targets: smallvec![ + Some(wgpu::ColorTargetState { + format: ViewBuilder::MAIN_TARGET_COLOR_FORMAT, + // TODO(andreas): have two render pipelines, an opaque one and a transparent one. Transparent shouldn't write depth! + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + }) + ], primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleStrip, cull_mode: None, @@ -612,35 +666,35 @@ impl Renderer for RectangleRenderer { device, &render_pipeline_desc_color, &pools.pipeline_layouts, - &pools.shader_modules, + &pools.shader_modules ); let render_pipeline_picking_layer = pools.render_pipelines.get_or_create( device, - &RenderPipelineDesc { + &(RenderPipelineDesc { label: "RectangleRenderer::render_pipeline_picking_layer".into(), fragment_entrypoint: "fs_main_picking_layer".into(), render_targets: smallvec![Some(PickingLayerProcessor::PICKING_LAYER_FORMAT.into())], depth_stencil: PickingLayerProcessor::PICKING_LAYER_DEPTH_STATE, multisample: PickingLayerProcessor::PICKING_LAYER_MSAA_STATE, ..render_pipeline_desc_color.clone() - }, + }), &pools.pipeline_layouts, - &pools.shader_modules, + &pools.shader_modules ); let render_pipeline_outline_mask = pools.render_pipelines.get_or_create( device, - &RenderPipelineDesc { + &(RenderPipelineDesc { label: "RectangleRenderer::render_pipeline_outline_mask".into(), fragment_entrypoint: "fs_main_outline_mask".into(), render_targets: smallvec![Some(OutlineMaskProcessor::MASK_FORMAT.into())], depth_stencil: OutlineMaskProcessor::MASK_DEPTH_STATE, multisample: OutlineMaskProcessor::mask_default_msaa_state( - shared_data.config.hardware_tier, + shared_data.config.hardware_tier ), ..render_pipeline_desc_color - }, + }), &pools.pipeline_layouts, - &pools.shader_modules, + &pools.shader_modules ); RectangleRenderer { @@ -656,7 +710,7 @@ impl Renderer for RectangleRenderer { pools: &'a WgpuResourcePools, phase: DrawPhase, pass: &mut wgpu::RenderPass<'a>, - draw_data: &'a Self::RendererDrawData, + draw_data: &'a Self::RendererDrawData ) -> anyhow::Result<()> { crate::profile_function!(); if draw_data.instances.is_empty() { @@ -686,10 +740,6 @@ impl Renderer for RectangleRenderer { fn participated_phases() -> &'static [DrawPhase] { // TODO(andreas): This a hack. We have both opaque and transparent. - &[ - DrawPhase::OutlineMask, - DrawPhase::Opaque, - DrawPhase::PickingLayer, - ] + &[DrawPhase::OutlineMask, DrawPhase::Opaque, DrawPhase::PickingLayer] } } diff --git a/crates/re_renderer/src/workspace_shaders.rs b/crates/re_renderer/src/workspace_shaders.rs index 7d999da73645..845b90623493 100644 --- a/crates/re_renderer/src/workspace_shaders.rs +++ b/crates/re_renderer/src/workspace_shaders.rs @@ -37,6 +37,12 @@ pub fn init() { fs.create_file(virtpath, content).unwrap(); } + { + let virtpath = Path::new("shader/decodings.wgsl"); + let content = include_str!("../shader/decodings.wgsl").into(); + fs.create_file(virtpath, content).unwrap(); + } + { let virtpath = Path::new("shader/depth_cloud.wgsl"); let content = include_str!("../shader/depth_cloud.wgsl").into(); diff --git a/crates/re_sdk/src/lib.rs b/crates/re_sdk/src/lib.rs index a7e7fe27607f..bddf42773f19 100644 --- a/crates/re_sdk/src/lib.rs +++ b/crates/re_sdk/src/lib.rs @@ -77,7 +77,8 @@ pub mod components { EncodedMesh3D, InstanceKey, KeypointId, Label, LineStrip2D, LineStrip3D, Mat3x3, Mesh3D, MeshFormat, MeshId, Pinhole, Point2D, Point3D, Quaternion, Radius, RawMesh3D, Rect2D, Rigid3, Scalar, ScalarPlotProps, Size3D, Tensor, TensorData, TensorDataMeaning, - TensorDimension, TensorId, TextEntry, Transform, Vec2D, Vec3D, Vec4D, ViewCoordinates, + TensorDimension, TensorId, TextEntry, Transform, Vec2D, Vec3D, Vec4D, + ViewCoordinates, }; } diff --git a/crates/re_ui/data/icons/app_icon_mac.png b/crates/re_ui/data/icons/app_icon_mac.png index 80df0b007d7588d6eff7a4a61221a0295d16ef97..1b951089cf2a6bb35d753d2bba96ef226cf01134 100644 GIT binary patch literal 4402 zcmV-25zX$2P)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H15Wh)8 zK~#90?VWpcT-9}lzkOz8*|PC7){KP#TN=rL3oEGuEDT8vX#$j&aT$$`F$)q*C?N#e z6xxQh>ZF8{0*l3?1QHTjjIl<;!-a%bmK_I76CgA|u))?yKmchZWBit_$DG|iWD_CF z8d-DCy;p|sFYC^o-`;a)`_8%N?7ffhDI__wZ4A4+XBd%6kSc*n3snXgr|5WKBuE)O z|Ly>G0Na3VBDzD7Z9pTcZ;6%lNPa_I9? z9-G+#yau|`!m1tY_}x?4>JE=>c+No^0P`!Gzo^FiE$BRungCqbh4O+L{gvjmF4I); zrvQ8?$UzkVsmhHtnC{C!&IgVN+Mixnh1IA&pd%xXWY(7N2-=~&4Eq2`qLE0V^-P6t z0beEjpJ^xPBPMTdUH*3L^`M;z`^DNX$(d_Mn2w2;qVkUj(}MPA2q^G4R)3gon(}f` zP7J#MSb~~$iM9&_{XS3`v_IaU�Pzt8a|G;J4*r6#%t~*3$&N1^A1geQ|&aER(!^ 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 literal 69290 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

~bcfs?EY}saK;35k3 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�&tR# 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