├── .github ├── .gitignore └── workflows │ └── pkgdown.yaml ├── vignettes ├── .gitignore └── articles │ ├── benchmark.Rmd │ └── compare.Rmd ├── LICENSE ├── src ├── .gitignore ├── entrypoint.c ├── Makevars.ucrt ├── Makevars.in ├── Makevars.win.in └── rust │ ├── Cargo.toml │ ├── src │ ├── shaders │ │ ├── shader.wgsl │ │ └── sdf_shape.wgsl │ ├── render_pipeline.rs │ ├── text.rs │ ├── file.rs │ ├── lib.rs │ └── graphics_device.rs │ └── Cargo.lock ├── NAMESPACE ├── man └── figures │ └── README-unnamed-chunk-2-1.png ├── .gitignore ├── .Rbuildignore ├── _pkgdown.yml ├── wgpugd.Rproj ├── R └── extendr-wrappers.R ├── DESCRIPTION ├── LICENSE.md ├── configure.win ├── configure ├── README.Rmd ├── README.md └── tools └── configure.R /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.R 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2022 2 | COPYRIGHT HOLDER: wgpugd authors 3 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.so 3 | *.dll 4 | target 5 | 6 | /rust/.vscode/settings.json 7 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(wgpugd) 4 | useDynLib(wgpugd, .registration = TRUE) 5 | -------------------------------------------------------------------------------- /man/figures/README-unnamed-chunk-2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yutannihilation/wgpugd/HEAD/man/figures/README-unnamed-chunk-2-1.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .Rdata 4 | .httr-oauth 5 | .DS_Store 6 | 7 | /src/Makevars.win 8 | /src/Makevars 9 | docs 10 | -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^wgpugd\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^README\.Rmd$ 4 | ^LICENSE\.md$ 5 | ^vignettes/articles$ 6 | ^_pkgdown\.yml$ 7 | ^docs$ 8 | ^pkgdown$ 9 | ^\.github$ 10 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: ~ 2 | template: 3 | bootstrap: 5 4 | 5 | articles: 6 | - title: Comparison with other graphics device 7 | navbar: ~ 8 | contents: 9 | - articles/compare 10 | -------------------------------------------------------------------------------- /src/entrypoint.c: -------------------------------------------------------------------------------- 1 | // We need to forward routine registration from C to Rust 2 | // to avoid the linker removing the static library. 3 | 4 | void R_init_wgpugd_extendr(void *dll); 5 | 6 | void R_init_wgpugd(void *dll) { 7 | R_init_wgpugd_extendr(dll); 8 | } 9 | -------------------------------------------------------------------------------- /src/Makevars.ucrt: -------------------------------------------------------------------------------- 1 | # Use GNU toolchain for R >= 4.2 2 | TOOLCHAIN = stable-gnu 3 | 4 | 5 | # Rtools42 doesn't have the linker in the location that cargo expects, so we 6 | # need to overwrite it via configuration. 7 | CARGO_LINKER = x86_64-w64-mingw32.static.posix-gcc.exe 8 | 9 | 10 | include Makevars.win 11 | -------------------------------------------------------------------------------- /src/Makevars.in: -------------------------------------------------------------------------------- 1 | LIBDIR = ./rust/target/release 2 | PKG_LIBS = -L$(LIBDIR) -lwgpugd 3 | STATLIB = $(LIBDIR)/libwgpugd.a 4 | 5 | all: C_clean 6 | 7 | $(SHLIB): $(STATLIB) 8 | 9 | $(STATLIB): 10 | @BEFORE_CARGO_BUILD@ cargo build --lib --release --manifest-path=./rust/Cargo.toml 11 | @AFTER_CARGO_BUILD@ 12 | 13 | C_clean: 14 | rm -Rf $(SHLIB) $(OBJECTS) @CLEAN_EXTRA@ 15 | 16 | clean: 17 | rm -Rf $(SHLIB) $(OBJECTS) $(STATLIB) rust/target 18 | 19 | .PHONY: all C_clean clean 20 | -------------------------------------------------------------------------------- /wgpugd.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | LineEndingConversion: Posix 18 | 19 | BuildType: Package 20 | PackageUseDevtools: Yes 21 | PackageInstallArgs: --no-multiarch --with-keep.source 22 | PackageRoxygenize: rd,collate,namespace 23 | -------------------------------------------------------------------------------- /R/extendr-wrappers.R: -------------------------------------------------------------------------------- 1 | # Generated by extendr: Do not edit by hand 2 | # 3 | # This file was created with the following call: 4 | # .Call("wrap__make_wgpugd_wrappers", use_symbols = TRUE, package_name = "wgpugd") 5 | 6 | #' @docType package 7 | #' @usage NULL 8 | #' @useDynLib wgpugd, .registration = TRUE 9 | NULL 10 | 11 | #' A WebGPU Graphics Device for R 12 | #' 13 | #' @param filename 14 | #' @param width Device width in inch. 15 | #' @param height Device width in inch. 16 | #' @export 17 | wgpugd <- function(filename = 'Rplot%03d.png', width = 7, height = 7) invisible(.Call(wrap__wgpugd, filename, width, height)) 18 | 19 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: wgpugd 2 | Title: A WebGPU Graphics Device for R 3 | Version: 0.0.0.9000 4 | Authors@R: 5 | person(given = "Hiroaki", 6 | family = "Yutani", 7 | role = c("aut", "cre"), 8 | email = "yutani.ini@gmail.com", 9 | comment = c(ORCID = "0000-0002-3385-7233")) 10 | Description: A high-performance raster graphics device utilizing the capabilities 11 | of GPU hardware via 'WebGPU' API (). In additon 12 | to the R's graphics device API, the device provides an interface to customize 13 | the output with 'WebGPU Shader Language' (WGSL, ) 14 | for more advanced usages. 15 | URL: https://yutannihilation.github.io/wgpugd/, https://github.com/yutannihilation/wgpugd 16 | BugReports: https://github.com/yutannihilation/wgpugd/issues 17 | License: MIT + file LICENSE 18 | Encoding: UTF-8 19 | Roxygen: list(markdown = TRUE) 20 | RoxygenNote: 7.1.2 21 | Suggests: 22 | rmarkdown 23 | Config/Needs/website: 24 | ragg, 25 | ggplot2, 26 | tidyr, 27 | bench, 28 | ggbeeswarm 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2022 wgpugd authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Makevars.win.in: -------------------------------------------------------------------------------- 1 | # WIN is used in the usual rwinlib Makevars, but it seems WIN envvar is not 2 | # available when configure.win is used. So, R_ARCH is chosen here. 3 | TARGET = $(subst /x64,x86_64,$(subst /i386,i686,$(R_ARCH)))-pc-windows-gnu 4 | 5 | # This is provided in Makevars.ucrt for R >= 4.2 6 | TOOLCHAIN ?= stable-msvc 7 | 8 | LIBDIR = ./rust/target/$(TARGET)/release 9 | 10 | # -ld3dcompiler is for Direct3D API, needed only for Windows (and probably we 11 | # can disable this by limiting the backends to Vulkan) 12 | PKG_LIBS = -L$(LIBDIR) -lwgpugd -lws2_32 -ladvapi32 -luserenv -lbcrypt -ld3dcompiler 13 | STATLIB = $(LIBDIR)/libwgpugd.a 14 | 15 | all: C_clean 16 | 17 | $(SHLIB): $(STATLIB) 18 | 19 | $(STATLIB): 20 | mkdir -p $(LIBDIR)/libgcc_mock 21 | cd $(LIBDIR)/libgcc_mock && \ 22 | touch gcc_mock.c && \ 23 | gcc -c gcc_mock.c -o gcc_mock.o && \ 24 | ar -r libgcc_eh.a gcc_mock.o && \ 25 | cp libgcc_eh.a libgcc_s.a 26 | 27 | @BEFORE_CARGO_BUILD@ cargo +$(TOOLCHAIN) build --target=$(TARGET) --lib --release --manifest-path=./rust/Cargo.toml 28 | @AFTER_CARGO_BUILD@ 29 | 30 | C_clean: 31 | rm -Rf $(SHLIB) $(OBJECTS) @CLEAN_EXTRA@ 32 | 33 | clean: 34 | rm -Rf $(SHLIB) $(OBJECTS) $(STATLIB) rust/target 35 | 36 | .PHONY: all C_clean clean 37 | -------------------------------------------------------------------------------- /src/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wgpugd" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["staticlib"] 8 | 9 | [dependencies] 10 | extendr-api = { git = "https://github.com/extendr/extendr", features = ["graphics"] } 11 | 12 | wgpu = { git = "https://github.com/gfx-rs/wgpu/" } 13 | # bytemuck converts Rust data into Plain Old Data, which can be passed to WebGPU 14 | bytemuck = { version = "1.7", features = [ "derive" ] } 15 | # pollster is needed to use async functions in non-async functions 16 | pollster = "0.2" 17 | 18 | # lyon does great job on tessellation 19 | lyon = "0.17" 20 | 21 | # png of course generates PNG 22 | png = "0.17" 23 | 24 | # regex is for parsing the user-supplied filename template (e.g. 25 | # "Rplot%03d.png") because, unfortunately, there's no such thing as sprintf() in 26 | # Rust. 27 | regex = "1" 28 | once_cell = "1.9" 29 | 30 | # fontdb and ttf-parser is needed for rendering texts. 31 | fontdb = "0.8" 32 | ttf-parser = "0.14.0" 33 | 34 | # glam is needed for matrix algebra 35 | glam = { version = "0.20", features = ["mint"] } 36 | 37 | # mint and euclid is needed to convert between lyon and glam 38 | mint = "0.5" 39 | euclid = { version = "0.22", features = ["mint"] } 40 | 41 | 42 | [patch.crates-io] 43 | libR-sys = { git = "https://github.com/extendr/libR-sys" } 44 | -------------------------------------------------------------------------------- /src/rust/src/shaders/shader.wgsl: -------------------------------------------------------------------------------- 1 | struct VertexInput { 2 | @location(0) pos: vec2, 3 | @location(1) color: u32, 4 | }; 5 | 6 | struct VertexOutput { 7 | @builtin(position) coords: vec4, 8 | @location(0) color: u32, 9 | }; 10 | 11 | struct GlobalsUniform { 12 | @location(0) resolution: vec2, 13 | }; 14 | 15 | @group(0) @binding(0) 16 | var globals: GlobalsUniform; 17 | 18 | @vertex 19 | fn vs_main( 20 | model: VertexInput, 21 | ) -> VertexOutput { 22 | var vs_out: VertexOutput; 23 | 24 | vs_out.color = model.color; 25 | 26 | // Scale the X and Y positions from [0, width or height] to [-1, 1] 27 | vs_out.coords = vec4(2.0 * model.pos.xy / globals.resolution - 1.0, 0.0, 1.0); 28 | 29 | return vs_out; 30 | } 31 | 32 | @fragment 33 | fn fs_main( 34 | vs_out: VertexOutput 35 | ) -> @location(0) vec4 { 36 | // R's color representation is in the order of RGBA, which can be simply 37 | // unpacked by unpack4x8unorm(). 38 | // 39 | // https://github.com/wch/r-source/blob/8ebcb33a9f70e729109b1adf60edd5a3b22d3c6f/src/include/R_ext/GraphicsDevice.h#L766-L796 40 | // https://www.w3.org/TR/WGSL/#unpack-builtin-functions 41 | var color: vec4 = unpack4x8unorm(vs_out.color); 42 | // return the alpha-premultiplied version of value 43 | return vec4(color.rgb * color.a, color.a); 44 | } 45 | -------------------------------------------------------------------------------- /configure.win: -------------------------------------------------------------------------------- 1 | # Variables used for tweaking Makevars 2 | BEFORE_CARGO_BUILD='' 3 | AFTER_CARGO_BUILD='' 4 | CLEAN_EXTRA='' 5 | 6 | # Check the Rust installation, and, if not available, try downloading the 7 | # precompiled binary. 8 | "${R_HOME}/bin${R_ARCH_BIN}/Rscript.exe" "./tools/configure.R" 9 | 10 | ret=$? 11 | 12 | case $ret in 13 | # The case when Rust is available 14 | 0) 15 | CLEAN_EXTRA='$(STATLIB)' 16 | ;; 17 | 18 | # The case when the precompiled binary is used 19 | 100) 20 | ;; 21 | 22 | # The case when both Rust and the precompiled binary are unavailable (or the 23 | # R script failed some unexpected error) 24 | *) 25 | exit $ret 26 | esac 27 | 28 | # To address the change of the toolchain on R 4.2 29 | BEFORE_CARGO_BUILD="${BEFORE_CARGO_BUILD}"' export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER="$(CARGO_LINKER)" \&\&' 30 | BEFORE_CARGO_BUILD="${BEFORE_CARGO_BUILD}"' export LIBRARY_PATH="$${LIBRARY_PATH};$(CURDIR)/$(LIBDIR)/libgcc_mock" \&\&' 31 | 32 | # If it's on CRAN, a package is not allowed to write in any other place than the 33 | # temporary directory on installation. So, we need to tweak Makevars to make the 34 | # compilation happen only within the package directory (i.e. `$(PWD)`). 35 | if [ "${NOT_CRAN}" != "true" ]; then 36 | BEFORE_CARGO_BUILD="${BEFORE_CARGO_BUILD}"' export CARGO_HOME="$(PWD)/.cargo" \&\&' 37 | AFTER_CARGO_BUILD="${AFTER_CARGO_BUILD}"'rm -Rf $(PWD)/.cargo $(LIBDIR)/build' 38 | else 39 | echo "*** Detected NOT_CRAN=true, do not override CARGO_HOME" 40 | fi 41 | 42 | sed \ 43 | -e "s|@CLEAN_EXTRA@|${CLEAN_EXTRA}|" \ 44 | -e "s|@BEFORE_CARGO_BUILD@|${BEFORE_CARGO_BUILD}|" \ 45 | -e "s|@AFTER_CARGO_BUILD@|${AFTER_CARGO_BUILD}|" \ 46 | src/Makevars.win.in > src/Makevars.win 47 | 48 | # Uncomment this to debug 49 | # 50 | # cat src/Makevars.win 51 | -------------------------------------------------------------------------------- /configure: -------------------------------------------------------------------------------- 1 | # Variables used for tweaking Makevars 2 | BEFORE_CARGO_BUILD='' 3 | AFTER_CARGO_BUILD='' 4 | CLEAN_EXTRA='' 5 | 6 | # Even when `cargo` is on `PATH`, `rustc` might not. We need to source 7 | # ~/.cargo/env to ensure PATH is configured correctly in some cases 8 | # (c.f. yutannihilation/string2path#4). However, this file is not always 9 | # available (e.g. when Rust is installed via apt on Ubuntu), so it might be 10 | # more straightforward to add `PATH` directly. 11 | if [ -e "${HOME}/.cargo/env" ]; then 12 | . "${HOME}/.cargo/env" 13 | BEFORE_CARGO_BUILD="${BEFORE_CARGO_BUILD} . \"${HOME}/.cargo/env\" \\&\\&" 14 | fi 15 | 16 | # Check the Rust installation, and, if not available, try downloading the 17 | # precompiled binary. 18 | "${R_HOME}/bin/Rscript" "./tools/configure.R" 19 | 20 | ret=$? 21 | 22 | case $ret in 23 | # The case when Rust is available 24 | 0) 25 | CLEAN_EXTRA='$(STATLIB)' 26 | ;; 27 | 28 | # The case when the precompiled binary is used 29 | 100) 30 | ;; 31 | 32 | # The case when both Rust and the precompiled binary are unavailable (or the 33 | # R script failed some unexpected error) 34 | *) 35 | exit $ret 36 | esac 37 | 38 | # If it's on CRAN, a package is not allowed to write in any other place than the 39 | # temporary directory on installation. So, we need to tweak Makevars to make the 40 | # compilation happen only within the package directory (i.e. `$(PWD)`). 41 | if [ "${NOT_CRAN}" != "true" ]; then 42 | BEFORE_CARGO_BUILD="${BEFORE_CARGO_BUILD}"' export CARGO_HOME="$(PWD)/.cargo" \&\&' 43 | AFTER_CARGO_BUILD="${AFTER_CARGO_BUILD}"'rm -Rf $(PWD)/.cargo $(LIBDIR)/build' 44 | else 45 | echo "*** Detected NOT_CRAN=true, do not override CARGO_HOME" 46 | fi 47 | 48 | sed \ 49 | -e "s|@CLEAN_EXTRA@|${CLEAN_EXTRA}|" \ 50 | -e "s|@BEFORE_CARGO_BUILD@|${BEFORE_CARGO_BUILD}|" \ 51 | -e "s|@AFTER_CARGO_BUILD@|${AFTER_CARGO_BUILD}|" \ 52 | src/Makevars.in > src/Makevars 53 | 54 | # Uncomment this to debug 55 | # 56 | # cat src/Makevars 57 | -------------------------------------------------------------------------------- /vignettes/articles/benchmark.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "benchmark" 3 | --- 4 | 5 | ```{r, include = FALSE} 6 | knitr::opts_chunk$set( 7 | collapse = TRUE, 8 | eval = FALSE, 9 | comment = "#>" 10 | ) 11 | ``` 12 | 13 | ## Benchmark 14 | 15 | ### Just open and close the device 16 | 17 | ```{r} 18 | #| label: bench1 19 | 20 | library(ggplot2) 21 | 22 | file <- tempfile(fileext = '.png') 23 | 24 | res <- bench::mark( 25 | wgpugd = { 26 | wgpugd::wgpugd(file, 10, 10) 27 | dev.off() 28 | }, 29 | ragg = { 30 | ragg::agg_png(file, 10, 10, unit = "in") 31 | dev.off() 32 | }, 33 | min_iterations = 30 34 | ) 35 | 36 | res 37 | 38 | autoplot(res) 39 | ``` 40 | 41 | 42 | ### Actual plotting 43 | 44 | ```{r} 45 | #| label: bench2 46 | 47 | set.seed(10) 48 | dsamp <- diamonds[sample(nrow(diamonds), 1000), ] 49 | 50 | p <- ggplot(dsamp, aes(carat, price)) + 51 | geom_point(aes(colour = clarity)) 52 | 53 | res <- bench::mark( 54 | wgpugd = { 55 | wgpugd::wgpugd(file, 10, 10) 56 | print(p) 57 | dev.off() 58 | }, 59 | ragg = { 60 | ragg::agg_png(file, 10, 10, unit = "in") 61 | print(p) 62 | dev.off() 63 | }, 64 | min_iterations = 30 65 | ) 66 | 67 | res 68 | 69 | autoplot(res) 70 | ``` 71 | 72 | ### Multiple plotting 73 | 74 | ```{r} 75 | #| label: bench3 76 | 77 | set.seed(10) 78 | dsamp <- diamonds[sample(nrow(diamonds), 1000), ] 79 | 80 | p <- ggplot(dsamp, aes(carat, price)) + 81 | geom_point(aes(colour = clarity)) 82 | 83 | temp_dir <- tempfile() 84 | dir.create(temp_dir) 85 | file <- file.path(temp_dir, "p%03d.png") 86 | 87 | res <- bench::mark( 88 | wgpugd = { 89 | wgpugd::wgpugd(file, 10, 10) 90 | for (i in 1:50) { 91 | print(p) 92 | } 93 | dev.off() 94 | }, 95 | ragg = { 96 | ragg::agg_png(file, 10, 10, unit = "in") 97 | for (i in 1:50) { 98 | print(p) 99 | } 100 | dev.off() 101 | }, 102 | min_iterations = 10 103 | ) 104 | 105 | res 106 | 107 | autoplot(res) 108 | ``` 109 | -------------------------------------------------------------------------------- /src/rust/src/render_pipeline.rs: -------------------------------------------------------------------------------- 1 | #[allow(clippy::too_many_arguments)] 2 | pub(crate) fn create_render_pipeline( 3 | device: &wgpu::Device, 4 | pipeline_layout_label: &str, 5 | pipeline_label: &str, 6 | bind_group_layouts: &[&wgpu::BindGroupLayout], 7 | shader_desc: &wgpu::ShaderModuleDescriptor, 8 | vertex_buffer_layouts: &[wgpu::VertexBufferLayout], 9 | sample_count: u32, 10 | ) -> wgpu::RenderPipeline { 11 | let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 12 | label: Some(pipeline_layout_label), 13 | bind_group_layouts, 14 | push_constant_ranges: &[], 15 | }); 16 | 17 | let shader = device.create_shader_module(shader_desc); 18 | device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 19 | label: Some(pipeline_label), 20 | layout: Some(&render_pipeline_layout), 21 | vertex: wgpu::VertexState { 22 | module: &shader, 23 | entry_point: "vs_main", 24 | buffers: vertex_buffer_layouts, 25 | }, 26 | primitive: wgpu::PrimitiveState { 27 | topology: wgpu::PrimitiveTopology::TriangleList, 28 | strip_index_format: None, 29 | front_face: wgpu::FrontFace::Ccw, 30 | cull_mode: None, // TODO: what's the correct value here? 31 | unclipped_depth: false, 32 | polygon_mode: wgpu::PolygonMode::Fill, 33 | conservative: false, 34 | }, 35 | depth_stencil: None, 36 | multisample: wgpu::MultisampleState { 37 | count: sample_count, 38 | ..Default::default() 39 | }, 40 | fragment: Some(wgpu::FragmentState { 41 | module: &shader, 42 | entry_point: "fs_main", 43 | targets: &[wgpu::ColorTargetState { 44 | format: wgpu::TextureFormat::Rgba8Unorm, 45 | blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), 46 | write_mask: wgpu::ColorWrites::all(), 47 | }], 48 | }), 49 | multiview: None, 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | release: 9 | types: [published] 10 | workflow_dispatch: 11 | 12 | name: pkgdown 13 | 14 | jobs: 15 | pkgdown: 16 | runs-on: ubuntu-latest 17 | # Only restrict concurrency for non-PR jobs 18 | concurrency: 19 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 20 | env: 21 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 22 | steps: 23 | - uses: actions/checkout@v2 24 | 25 | - uses: r-lib/actions/setup-pandoc@v2 26 | 27 | - uses: actions-rs/toolchain@v1 28 | with: 29 | toolchain: stable 30 | default: true 31 | 32 | # This setting comes from https://github.com/gfx-rs/wgpu/blob/ebca3298f01ea72f80aeac4ab1be889469f9c699/.github/workflows/ci.yml#L155-L168 33 | - name: install Vulkan sdk 34 | run: | 35 | sudo apt-get update -y -qq 36 | 37 | # llvmpipe/lavapipe 38 | sudo add-apt-repository ppa:oibaf/graphics-drivers -y 39 | 40 | # vulkan sdk 41 | wget -qO - https://packages.lunarg.com/lunarg-signing-key-pub.asc | sudo apt-key add - 42 | sudo wget -qO /etc/apt/sources.list.d/lunarg-vulkan-focal.list https://packages.lunarg.com/vulkan/lunarg-vulkan-focal.list 43 | 44 | sudo apt-get update 45 | sudo apt install -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers vulkan-sdk 46 | 47 | - uses: r-lib/actions/setup-r@v2 48 | with: 49 | use-public-rspm: true 50 | 51 | - uses: r-lib/actions/setup-r-dependencies@v2 52 | with: 53 | extra-packages: any::pkgdown, local::. 54 | needs: website 55 | 56 | - name: Build site 57 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 58 | shell: Rscript {0} 59 | 60 | - name: Deploy to GitHub pages 🚀 61 | if: github.event_name != 'pull_request' 62 | uses: JamesIves/github-pages-deploy-action@4.1.4 63 | with: 64 | clean: false 65 | branch: gh-pages 66 | folder: docs 67 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | 7 | ```{r, include = FALSE} 8 | knitr::opts_chunk$set( 9 | collapse = TRUE, 10 | comment = "#>", 11 | fig.path = "man/figures/README-", 12 | out.width = "100%" 13 | ) 14 | ``` 15 | 16 | # wgpugd: A WebGPU Graphics Device for R 17 | 18 | 19 | 20 | 21 | ## Overview 22 | 23 | ### What is WebGPU? 24 | 25 | [WebGPU](https://www.w3.org/TR/webgpu/) is an API that exposes the capabilities of GPU hardware. 26 | As the name indicates, it's primarily designed for the Web. However, it's not only for the Web[^1]. 27 | 28 | [^1]: 29 | 30 | ### What is wgpu? 31 | 32 | As the name indicates, the wgpugd package uses [wgpu](https://wgpu.rs/), a pure-Rust 33 | implementation of the WebGPU standard. wgpu is what's behind the WebGPU support 34 | of Firefox and Deno, and is widely used over the Rust's graphics ecosystem. 35 | 36 | ### Wait, Rust...? Can we use Rust in R?? 37 | 38 | Yes! [extendr](https://extendr.github.io/) is the Rust framework for interacting with R. 39 | 40 | ### Why WebGPU for R? 41 | 42 | The main motivation is to add post-effect to graphics with [WebGPU Shader Language (WGSL)](https://www.w3.org/TR/WGSL/). 43 | But, of course, the power of GPU should simply contribute to high performance! 44 | 45 | ## Installation 46 | 47 | You can install the development version of wgpugd like so: 48 | 49 | ``` r 50 | devtools::install_github("yutannihilation/wgpugd") 51 | ``` 52 | 53 | ## Usages 54 | 55 | :warning: wgpugd is currently at its verrry early stage of the development! :warning: 56 | 57 | ```{r} 58 | library(wgpugd) 59 | library(ggplot2) 60 | 61 | file <- knitr::fig_path('.png') 62 | wgpugd(file, 10, 10) 63 | # ragg::agg_png(file, 10, 10, unit = "in") 64 | 65 | set.seed(10) 66 | dsamp <- diamonds[sample(nrow(diamonds), 1000), ] 67 | 68 | ggplot(dsamp, aes(carat, price)) + 69 | geom_point(aes(colour = clarity)) + 70 | ggtitle("\( 'ω')/ウオオオオオオアアアアーーーーッッッ!!!!") + 71 | theme(text = element_text(size = 30, family = "Noto Sans JP")) 72 | 73 | dev.off() 74 | 75 | knitr::include_graphics(file) 76 | ``` 77 | 78 | ## References 79 | 80 | * wgpugd uses [extendr](https://extendr.github.io/), a Rust extension mechanism for R, both to communicate with the actual graphics device implementation in Rust from R, and to access R's graphics API from Rust. 81 | * If you are curious about developing a Rust program with wgpu, I'd recommend [Learn Wgpu](https://sotrh.github.io/learn-wgpu/) to get started. 82 | * [lyon](https://github.com/nical/lyon) is a library for "path tessellation," which is necessary to draw lines on GPU. 83 | 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This repository is archived! My new attempt is here: https://github.com/yutannihilation/vellogd-r 2 | 3 | ---------------------- 4 | 5 | 6 | 7 | # wgpugd: A WebGPU Graphics Device for R 8 | 9 | 10 | 11 | 12 | ## Overview 13 | 14 | ### What is WebGPU? 15 | 16 | [WebGPU](https://www.w3.org/TR/webgpu/) is an API that exposes the 17 | capabilities of GPU hardware. As the name indicates, it’s primarily 18 | designed for the Web. However, it’s not only for the Web[^1]. 19 | 20 | ### What is wgpu? 21 | 22 | As the name indicates, the wgpugd package uses [wgpu](https://wgpu.rs/), 23 | a pure-Rust implementation of the WebGPU standard. wgpu is what’s behind 24 | the WebGPU support of Firefox and Deno, and is widely used over the 25 | Rust’s graphics ecosystem. 26 | 27 | ### Wait, Rust…? Can we use Rust in R?? 28 | 29 | Yes! [extendr](https://extendr.github.io/) is the Rust framework for 30 | interacting with R. 31 | 32 | ### Why WebGPU for R? 33 | 34 | The main motivation is to add post-effect to graphics with [WebGPU 35 | Shader Language (WGSL)](https://www.w3.org/TR/WGSL/). But, of course, 36 | the power of GPU should simply contribute to high performance! 37 | 38 | ## Installation 39 | 40 | You can install the development version of wgpugd like so: 41 | 42 | ``` r 43 | devtools::install_github("yutannihilation/wgpugd") 44 | ``` 45 | 46 | ## Usages 47 | 48 | :warning: wgpugd is currently at its verrry early stage of the 49 | development! :warning: 50 | 51 | ``` r 52 | library(wgpugd) 53 | library(ggplot2) 54 | 55 | file <- knitr::fig_path('.png') 56 | wgpugd(file, 10, 10) 57 | # ragg::agg_png(file, 10, 10, unit = "in") 58 | 59 | set.seed(10) 60 | dsamp <- diamonds[sample(nrow(diamonds), 1000), ] 61 | 62 | ggplot(dsamp, aes(carat, price)) + 63 | geom_point(aes(colour = clarity)) + 64 | ggtitle("\( 'ω')/ウオオオオオオアアアアーーーーッッッ!!!!") + 65 | theme(text = element_text(size = 30, family = "Noto Sans JP")) 66 | 67 | dev.off() 68 | #> png 69 | #> 2 70 | 71 | knitr::include_graphics(file) 72 | ``` 73 | 74 | 75 | 76 | ## References 77 | 78 | - wgpugd uses [extendr](https://extendr.github.io/), a Rust extension 79 | mechanism for R, both to communicate with the actual graphics device 80 | implementation in Rust from R, and to access R’s graphics API from 81 | Rust. 82 | - If you are curious about developing a Rust program with wgpu, I’d 83 | recommend [Learn Wgpu](https://sotrh.github.io/learn-wgpu/) to get 84 | started. 85 | - [lyon](https://github.com/nical/lyon) is a library for “path 86 | tessellation,” which is necessary to draw lines on GPU. 87 | 88 | [^1]: 89 | -------------------------------------------------------------------------------- /src/rust/src/shaders/sdf_shape.wgsl: -------------------------------------------------------------------------------- 1 | struct VertexInput { 2 | @location(0) pos: vec2, 3 | }; 4 | 5 | struct VertexOutput { 6 | @builtin(position) coords: vec4, 7 | @location(0) center: vec2, 8 | @location(1) radius: f32, 9 | @location(2) stroke_width: f32, 10 | @location(3) fill_color: u32, 11 | @location(4) stroke_color: u32, 12 | 13 | }; 14 | 15 | struct GlobalsUniform { 16 | @location(0) resolution: vec2, 17 | }; 18 | 19 | @group(0) @binding(0) 20 | var globals: GlobalsUniform; 21 | 22 | struct InstanceInput { 23 | @location(1) center: vec2, 24 | @location(2) radius: f32, 25 | @location(3) stroke_width: f32, 26 | @location(4) fill_color: u32, 27 | @location(5) stroke_color: u32, 28 | }; 29 | 30 | @vertex 31 | fn vs_main( 32 | model: VertexInput, 33 | instance: InstanceInput, 34 | ) -> VertexOutput { 35 | var vs_out: VertexOutput; 36 | 37 | vs_out.coords = vec4(model.pos, 0.0, 1.0); 38 | // Y-axis is opposite 39 | vs_out.center = vec2(instance.center.x, globals.resolution.y - instance.center.y); 40 | vs_out.radius = instance.radius; 41 | vs_out.stroke_width = instance.stroke_width; 42 | vs_out.fill_color = instance.fill_color; 43 | vs_out.stroke_color = instance.stroke_color; 44 | 45 | return vs_out; 46 | } 47 | 48 | @fragment 49 | fn fs_main(vs_out: VertexOutput) -> @location(0) vec4 { 50 | // width to apply anti-aliase 51 | let HALF_PIXEL = 0.5; 52 | 53 | var fill_color: vec4 = unpack4x8unorm(vs_out.fill_color); 54 | var stroke_color: vec4 = unpack4x8unorm(vs_out.stroke_color); 55 | 56 | var dist_fill = distance(vs_out.coords.xy, vs_out.center) - vs_out.radius; 57 | var dist_stroke_inner = distance(vs_out.coords.xy, vs_out.center) - (vs_out.radius - vs_out.stroke_width * 0.5); 58 | var dist_stroke_outer = distance(vs_out.coords.xy, vs_out.center) - (vs_out.radius + vs_out.stroke_width * 0.5); 59 | 60 | // TODO: A poor-man's anti-aliasing. I don't know how to do it correctly atm... 61 | fill_color.a *= clamp(HALF_PIXEL - dist_fill, 0.0, 1.0); 62 | stroke_color.a *= min( 63 | clamp(HALF_PIXEL - dist_stroke_outer, 0.0, 1.0), // if it's inside of the outer boundary 64 | clamp(dist_stroke_inner + HALF_PIXEL, 0.0, 1.0), // if it's outside of the inner boundary 65 | ); 66 | 67 | // alpha blending 68 | var out_a = stroke_color.a + fill_color.a * (1.0 - stroke_color.a); 69 | if (out_a == 0.0) { 70 | return vec4(0.0); 71 | } else { 72 | return vec4( 73 | // return the alpha-premultiplied values, so don't devide by out_a here. 74 | stroke_color.rgb * stroke_color.a + fill_color.rgb * fill_color.a * (1.0 - stroke_color.a), 75 | out_a 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/rust/src/text.rs: -------------------------------------------------------------------------------- 1 | use extendr_api::prelude::*; 2 | use once_cell::sync::Lazy; 3 | 4 | pub(crate) struct FontDBWrapper { 5 | db: fontdb::Database, 6 | fallback_glyph_id: Option, 7 | } 8 | 9 | impl FontDBWrapper { 10 | pub(crate) fn query(&self, fontfamily: &str, fontface: i32) -> Option { 11 | // TODO: Can I do this more nicely? 12 | let (weight, style) = match fontface { 13 | 1 => (fontdb::Weight::NORMAL, fontdb::Style::Normal), // Plain 14 | 2 => (fontdb::Weight::BOLD, fontdb::Style::Normal), // Bold 15 | 3 => (fontdb::Weight::NORMAL, fontdb::Style::Italic), // Italic 16 | 4 => (fontdb::Weight::BOLD, fontdb::Style::Italic), // BoldItalic 17 | // Symbolic or unknown 18 | _ => { 19 | reprintln!("[WARN] Unsupported fontface"); 20 | (fontdb::Weight::NORMAL, fontdb::Style::Normal) 21 | } 22 | }; 23 | 24 | if let Some(id) = self.db.query(&fontdb::Query { 25 | families: &[fontdb::Family::Name(fontfamily)], 26 | weight, 27 | stretch: fontdb::Stretch::Normal, 28 | style, 29 | }) { 30 | Some(id) 31 | } else { 32 | // TODO: This warning is shown too many times, so disabled temporarily... 33 | // 34 | // reprintln!( 35 | // "[WARN] Cannot find the specified font family, falling back to the default font..." 36 | // ); 37 | self.fallback_glyph_id 38 | } 39 | } 40 | 41 | pub(crate) fn with_face_data(&self, id: fontdb::ID, p: P) -> Option 42 | where 43 | P: FnOnce(&[u8], u32) -> T, 44 | { 45 | self.db.with_face_data(id, p) 46 | } 47 | } 48 | 49 | pub(crate) static FONTDB: Lazy = Lazy::new(|| { 50 | let mut db = fontdb::Database::new(); 51 | db.load_system_fonts(); 52 | 53 | let fallback_glyph_id = db.query(&fontdb::Query { 54 | families: &[fontdb::Family::SansSerif], 55 | ..Default::default() 56 | }); 57 | 58 | FontDBWrapper { 59 | db, 60 | fallback_glyph_id, 61 | } 62 | }); 63 | 64 | pub(crate) struct LyonOutlineBuilder { 65 | pub(crate) builder: lyon::path::path::Builder, 66 | // multiply by this to scale the position into the range of [0, 1]. 67 | scale_factor: f32, 68 | 69 | offset_x: f32, 70 | } 71 | 72 | impl LyonOutlineBuilder { 73 | pub(crate) fn new(scale: f32) -> Self { 74 | Self { 75 | builder: lyon::path::Path::builder(), 76 | scale_factor: scale, 77 | offset_x: 0.0, 78 | } 79 | } 80 | 81 | pub(crate) fn build(self) -> lyon::path::Path { 82 | self.builder.build() 83 | } 84 | 85 | fn point(&self, x: f32, y: f32) -> lyon::math::Point { 86 | lyon::math::point(x * self.scale_factor + self.offset_x, y * self.scale_factor) 87 | } 88 | 89 | pub(crate) fn add_offset_x(&mut self, offset: f32) { 90 | self.offset_x += offset * self.scale_factor; 91 | } 92 | 93 | pub(crate) fn offset_x(&self) -> f32 { 94 | self.offset_x 95 | } 96 | } 97 | 98 | impl ttf_parser::OutlineBuilder for LyonOutlineBuilder { 99 | fn move_to(&mut self, x: f32, y: f32) { 100 | self.builder.begin(self.point(x, y)); 101 | } 102 | 103 | fn line_to(&mut self, x: f32, y: f32) { 104 | self.builder.line_to(self.point(x, y)); 105 | } 106 | 107 | fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { 108 | let ctrl = self.point(x1, y1); 109 | let to = self.point(x, y); 110 | self.builder.quadratic_bezier_to(ctrl, to); 111 | } 112 | 113 | fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { 114 | let ctrl1 = self.point(x1, y1); 115 | let ctrl2 = self.point(x2, y2); 116 | let to = self.point(x, y); 117 | self.builder.cubic_bezier_to(ctrl1, ctrl2, to); 118 | } 119 | 120 | fn close(&mut self) { 121 | self.builder.close(); 122 | } 123 | } 124 | 125 | pub(crate) fn find_kerning( 126 | facetables: &ttf_parser::FaceTables, 127 | left: ttf_parser::GlyphId, 128 | right: ttf_parser::GlyphId, 129 | ) -> i16 { 130 | let kern_table = if let Some(kern_table) = facetables.kern { 131 | kern_table 132 | } else { 133 | return 0; 134 | }; 135 | 136 | for st in kern_table.subtables { 137 | if !st.horizontal { 138 | continue; 139 | } 140 | 141 | if let Some(kern) = st.glyphs_kerning(left, right) { 142 | return kern; 143 | } 144 | } 145 | 146 | 0 147 | } 148 | -------------------------------------------------------------------------------- /src/rust/src/file.rs: -------------------------------------------------------------------------------- 1 | // Since there's no direct equivalent to sprintf() in Rust (i.e., we cannot 2 | // simply pass the user-supplied string to format!()), we need to parse the 3 | // filename and construct the filename template. 4 | 5 | use once_cell::sync::Lazy; 6 | use std::path::{Path, PathBuf}; 7 | 8 | use extendr_api::prelude::*; 9 | 10 | #[derive(Debug)] 11 | pub(crate) struct FilenameTemplate { 12 | parent: Option, 13 | prefix: String, 14 | zero_padded: bool, 15 | digit_width: Option, 16 | suffix: String, 17 | } 18 | 19 | static FILENAME_PATTERN: Lazy = Lazy::new(|| { 20 | regex::Regex::new( 21 | r"^(?P[^%]+)(?:%(?P0)?(?P[1-9]\d*)d)?(?P.*)$", 22 | ) 23 | .unwrap() 24 | }); 25 | 26 | impl FilenameTemplate { 27 | pub(crate) fn new(filename: &str) -> Result { 28 | let p = Path::new(filename); 29 | 30 | let parent = match p.parent() { 31 | // It doesn't have any parent directory. 32 | Some(m) if m.to_string_lossy() == "" => None, 33 | None => None, 34 | // If it has a parent directory but it deosn't exist yet, create it 35 | Some(m) if !m.exists() => { 36 | reprintln!("Creating the parent directory: {m:?}"); 37 | if let Err(e) = std::fs::create_dir_all(m) { 38 | return Err(Error::Other(e.to_string())); 39 | } 40 | Some(m.to_path_buf()) 41 | } 42 | // It has a parent directory but it's not a directory in actual. 43 | Some(m) if !m.is_dir() => { 44 | return Err(Error::Other( 45 | "{m:?} is not a directory. Something is wrong...!".to_string(), 46 | )); 47 | } 48 | // If it has a parent directory and it exists, do nothing 49 | Some(m) => Some(m.to_path_buf()), 50 | }; 51 | 52 | let basename = match p.file_name() { 53 | Some(basename) => basename.to_string_lossy(), 54 | None => { 55 | return Err(Error::Other( 56 | "The specified filename doesn't contain a filename in actual.".to_string(), 57 | )) 58 | } 59 | }; 60 | 61 | match FILENAME_PATTERN.captures(&basename) { 62 | Some(cap) => { 63 | let prefix = cap.name("prefix").unwrap().as_str().to_string(); 64 | let zero_padded = cap.name("zero").is_some(); 65 | let digit_width = cap 66 | .name("digit_width") 67 | .map(|m| m.as_str().parse::().unwrap()); 68 | let suffix = cap 69 | .name("suffix") 70 | .map(|m| m.as_str().to_string()) 71 | .unwrap_or_default(); 72 | 73 | Ok(Self { 74 | parent, 75 | prefix, 76 | zero_padded, 77 | digit_width, 78 | suffix, 79 | }) 80 | } 81 | None => return Err(Error::Other(format!("Invalid filename: {basename}"))), 82 | } 83 | } 84 | 85 | pub(crate) fn filename(&self, page_num: u32) -> PathBuf { 86 | let prefix = &self.prefix; 87 | let suffix = &self.suffix; 88 | 89 | let filename = match self.digit_width { 90 | Some(digit_width) => { 91 | if self.zero_padded { 92 | format!("{prefix}{page_num:0digit_width$}{suffix}") 93 | } else { 94 | format!("{prefix}{page_num:digit_width$}{suffix}") 95 | } 96 | } 97 | None => format!("{prefix}{suffix}"), 98 | }; 99 | 100 | if let Some(parent) = &self.parent { 101 | parent.join(Path::new(&filename)) 102 | } else { 103 | Path::new(&filename).to_path_buf() 104 | } 105 | } 106 | } 107 | 108 | #[test] 109 | fn test_file_pattern() { 110 | let cap = FILENAME_PATTERN.captures("Rplot%03d.png").unwrap(); 111 | assert_eq!(cap.name("prefix").unwrap().as_str(), "Rplot"); 112 | assert!(cap.name("zero").is_some()); 113 | assert_eq!(cap.name("digit_width").unwrap().as_str(), "3"); 114 | assert_eq!(cap.name("suffix").unwrap().as_str(), ".png"); 115 | 116 | let cap = FILENAME_PATTERN.captures("Rplot%3d.png").unwrap(); 117 | assert_eq!(cap.name("prefix").unwrap().as_str(), "Rplot"); 118 | assert!(cap.name("zero").is_none()); 119 | assert_eq!(cap.name("digit_width").unwrap().as_str(), "3"); 120 | assert_eq!(cap.name("suffix").unwrap().as_str(), ".png"); 121 | 122 | let cap = FILENAME_PATTERN.captures("Rplot.png").unwrap(); 123 | assert_eq!(cap.name("prefix").unwrap().as_str(), "Rplot.png"); 124 | } 125 | 126 | #[test] 127 | fn test_filename() -> Result<()> { 128 | // No placeholder 129 | assert_eq!( 130 | FilenameTemplate::new("Rplot.png")? 131 | .filename(10) 132 | .to_string_lossy(), 133 | "Rplot.png" 134 | ); 135 | 136 | // Padded with space 137 | assert_eq!( 138 | FilenameTemplate::new("Rplot%3d.png")? 139 | .filename(10) 140 | .to_string_lossy(), 141 | "Rplot 10.png" 142 | ); 143 | 144 | // Padded with zero 145 | assert_eq!( 146 | FilenameTemplate::new("Rplot%03d.png")? 147 | .filename(10) 148 | .to_string_lossy(), 149 | "Rplot010.png" 150 | ); 151 | 152 | Ok(()) 153 | } 154 | -------------------------------------------------------------------------------- /vignettes/articles/compare.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Comparison with other graphics device" 3 | --- 4 | 5 | ```{r, include = FALSE} 6 | knitr::opts_chunk$set( 7 | collapse = TRUE, 8 | comment = "#>" 9 | ) 10 | ``` 11 | 12 | ```{r} 13 | library(wgpugd) 14 | library(ragg) 15 | 16 | agg <- function(...) { 17 | agg_png(width = 7, height = 7, units = "in", ...) 18 | } 19 | ``` 20 | 21 | 22 | ## Line 23 | 24 | ```{r line, fig.show='hold', out.width='50%'} 25 | do_line <- function(dev) { 26 | filename <- knitr::fig_path(paste0(deparse(substitute(dev)), ".png")) 27 | dev(filename = filename) 28 | 29 | a <- pi * 1:3 / 7 30 | x <- sin(a) 31 | y <- cos(a) 32 | col <- c("red", "blue", "green") 33 | le <- c("round", "butt", "square") 34 | 35 | invisible(lapply(1:3, \(i) { 36 | grid::grid.lines( 37 | x = 0.5 + c(-0.35 * x[i], 0.4 * x[i]), 38 | y = 0.5 + c(-0.35 * y[i], 0.4 * y[i]), 39 | gp = grid::gpar( 40 | col = col[i], 41 | alpha = 0.5, 42 | lwd = 50 + 24 * i, 43 | lineend = le[i], 44 | linemitre = 10. 45 | ) 46 | ) 47 | })) 48 | 49 | dev.off() 50 | 51 | knitr::include_graphics(filename) 52 | } 53 | 54 | do_line(agg) 55 | do_line(wgpugd) 56 | ``` 57 | 58 | ## Rect 59 | 60 | ```{r rect, fig.show='hold', out.width='50%'} 61 | do_rect <- function(dev) { 62 | filename <- knitr::fig_path(paste0(deparse(substitute(dev)), ".png")) 63 | dev(filename = filename) 64 | 65 | grid::grid.rect( 66 | x = c(0.8, 0.3), 67 | y = c(0.8, 0.3), 68 | width = c(0.8, 0.7), 69 | height = c(0.8, 0.45), 70 | gp = grid::gpar( 71 | fill = c("green", "blue"), 72 | col = c("yellow"), 73 | lwd = 40, 74 | alpha = 0.5 75 | ) 76 | ) 77 | 78 | dev.off() 79 | 80 | knitr::include_graphics(filename) 81 | } 82 | 83 | do_rect(agg) 84 | do_rect(wgpugd) 85 | ``` 86 | 87 | ## Polygon 88 | 89 | From `?grid.polygon` 90 | 91 | ```{r polygon, fig.show='hold', out.width='50%'} 92 | do_polygon <- function(dev) { 93 | filename <- knitr::fig_path(paste0(deparse(substitute(dev)), ".png")) 94 | dev(filename = filename) 95 | 96 | grid::grid.polygon(x = outer(c(0, .5, 1, .5), 5:1/5), 97 | y = outer(c(.5, 1, .5, 0), 5:1/5), 98 | id.lengths = rep(4, 5), 99 | gp = grid::gpar(fill = 1:5, alpha = 0.5)) 100 | 101 | dev.off() 102 | 103 | knitr::include_graphics(filename) 104 | } 105 | 106 | do_polygon(agg) 107 | do_polygon(wgpugd) 108 | ``` 109 | 110 | 111 | ## Circle 112 | 113 | ```{r circle, fig.show='hold', out.width='50%'} 114 | do_circle <- function(dev) { 115 | filename <- knitr::fig_path(paste0(deparse(substitute(dev)), ".png")) 116 | dev(filename = filename) 117 | 118 | grid::grid.circle( 119 | x = c(0.8, 0.3), 120 | y = c(0.8, 0.3), 121 | r = c(0.5, 0.7), 122 | gp = grid::gpar( 123 | fill = c("green", "blue"), 124 | col = c("yellow"), 125 | lwd = 40, 126 | alpha = 0.5 127 | ) 128 | ) 129 | dev.off() 130 | 131 | knitr::include_graphics(filename) 132 | } 133 | 134 | do_circle(agg) 135 | do_circle(wgpugd) 136 | ``` 137 | 138 | 139 | ## Text 140 | 141 | ```{r text, fig.show='hold', out.width='50%'} 142 | do_text <- function(dev) { 143 | filename <- knitr::fig_path(paste0(deparse(substitute(dev)), ".png")) 144 | dev(filename = filename) 145 | 146 | x <- y <- grid::unit(0.5, "npc") 147 | grid::grid.points(x = x, y = y) 148 | lapply(72 * 7 / 2 + (-2):2 * 20, function(x) { 149 | grid::grid.lines(x = grid::unit(c(0, 1), "npc"), y = grid::unit(c(x, x), "points")) 150 | }) 151 | grid::grid.text(label = "gla\nd", hjust = 1, vjust = 0, gp = grid::gpar(cex = 5, col = "red")) 152 | grid::grid.text(label = "ve\nrrr\nrry", hjust = 0, vjust = 1, gp = grid::gpar(cex = 5, col = "blue")) 153 | grid::grid.text(label = "rot", x = 0.4, y = 0.4, hjust = 0.5, vjust = 0.5, rot = 30, gp = grid::gpar(cex = 5, col = "green", alpha = 0.4)) 154 | dev.off() 155 | 156 | knitr::include_graphics(filename) 157 | } 158 | 159 | do_text(agg) 160 | do_text(wgpugd) 161 | ``` 162 | 163 | ```{r text2, fig.show='hold', out.width='50%'} 164 | do_text2 <- function(dev) { 165 | filename <- knitr::fig_path(paste0(deparse(substitute(dev)), ".png")) 166 | dev(filename = filename) 167 | 168 | grid::grid.text( 169 | x = grid::unit(0.5, "npc"), 170 | y = grid::unit(0.5, "npc"), 171 | label = "d", 172 | hjust = 0.5, 173 | vjust = 0, 174 | gp = grid::gpar(col = "brown", cex = 24, fontfamily = "Iosevka SS08") 175 | ) 176 | 177 | f <- function(x, ...) { 178 | grid::grid.lines( 179 | x = grid::unit(c(0, 1), "npc"), 180 | y = grid::unit(rep(12, 2) * x, "points") + grid::unit(c(0.5, 0.5), "npc"), 181 | gp = grid::gpar(...) 182 | ) 183 | invisible(NULL) 184 | } 185 | 186 | lapply(1:20, f, lty = 5, col = "purple", alpha = 0.7) 187 | lapply(0:2 * 10, f, col = "black") 188 | 189 | dev.off() 190 | 191 | knitr::include_graphics(filename) 192 | } 193 | 194 | do_text2(agg) 195 | do_text2(wgpugd) 196 | ``` 197 | 198 | ## Order of elements 199 | 200 | ```{r order, fig.show='hold', out.width='50%'} 201 | do_order <- function(dev) { 202 | filename <- knitr::fig_path(paste0(deparse(substitute(dev)), ".png")) 203 | dev(filename = filename) 204 | 205 | grid::grid.circle(x = 0.3, r = 0.3, gp = grid::gpar(col = "transparent", fill = "#FF000080")) 206 | grid::grid.circle(x = 0.7, r = 0.3, gp = grid::gpar(col = "transparent", fill = "#00FF0080")) 207 | grid::grid.lines(y = 0.5, gp = grid::gpar(col = "#FFFF0080", lwd = 60)) 208 | grid::grid.circle(y = 0.7, r = 0.3, gp = grid::gpar(col = "transparent", fill = "#0000FF80")) 209 | grid::grid.lines(x = 0.8, gp = grid::gpar(col = "#FF00FF80", lwd = 60)) 210 | 211 | dev.off() 212 | 213 | knitr::include_graphics(filename) 214 | } 215 | 216 | do_order(agg) 217 | do_order(wgpugd) 218 | ``` 219 | 220 | ```{r order2, fig.show='hold', out.width='50%'} 221 | do_order2 <- function(dev) { 222 | filename <- knitr::fig_path(paste0(deparse(substitute(dev)), ".png")) 223 | dev(filename = filename) 224 | 225 | col <- scales::alpha("red", 0.7) 226 | grid::grid.circle(x = 0.2, r = 0.2, gp = grid::gpar(col = col, fill = "blue", lwd = 40)) 227 | grid::grid.lines(y = 0.55, gp = grid::gpar(col = "#00000088", lwd = 50)) 228 | grid::grid.rect(width = 0.4, height = 0.4, gp = grid::gpar(fill = "pink")) 229 | grid::grid.lines(y = 0.45, gp = grid::gpar(col = "#00000088", lwd = 60)) 230 | grid::grid.circle(x = 0.8, r = 0.2, gp = grid::gpar(fill = "lightgreen", lwd = 40)) 231 | grid::grid.lines(y = 0.35, gp = grid::gpar(col = "#00000088", lwd = 60)) 232 | 233 | dev.off() 234 | 235 | knitr::include_graphics(filename) 236 | } 237 | 238 | do_order2(agg) 239 | do_order2(wgpugd) 240 | ``` 241 | -------------------------------------------------------------------------------- /tools/configure.R: -------------------------------------------------------------------------------- 1 | # The following fields in DESCRIPTION can be used for customizing the behavior. 2 | # 3 | # Config//MSRV (optional): 4 | # Minimum Supported Rust version (e.g. 1.41.0). If this is specified, errors 5 | # when the installed cargo is newer than the requirement. 6 | # 7 | # Config//windows_toolchain (optional): 8 | # Expected toolchain of the Rust installation (e.g. stable-msvc). If this is 9 | # specified, errors when the specified toolchain is not available. 10 | # 11 | # Config//github_repo: 12 | # Name of the GitHub repo (e.g. yutannihilation/wgpugd) 13 | # 14 | # Config//crate_name (optional): 15 | # Name of the crate (e.g. wgpugd). If the crate name is the same as the 16 | # repository name, this can be omitted. 17 | # 18 | # Config//github_tag: 19 | # Tag of the GitHub release of the precompiled binaries (e.g. build_20210921-1) 20 | # 21 | # Config//binary_sha256sum: 22 | # The expected checksums of the precompiled binaries as an expression of a list. 23 | # Note: This needs to be an R expression rather than CSV or TSV, because the 24 | # DESCRIPTION gets auto-formatted when compiling, which introduces 25 | # unexpected line breaks. 26 | # 27 | # Example: 28 | # list( 29 | # `aarch64-apple-darwin-libwgpugd.a` = "4a34f99cec66610746b20d456b1e11b346596c22ea1935c1bcb5ef1ab725f0e8", 30 | # `i686-pc-windows-gnu-libwgpugd.a` = "ceda54184fb3bf9e4cbba86848cb2091ff5b77870357f94319f9215fadfa5b25", 31 | # `ucrt-x86_64-pc-windows-gnu-libwgpugd.a` = "26a05f6ee8c2f625027ffc77c97fc8ac9746a182f5bc53d64235999a02c0b0dc", 32 | # `x86_64-apple-darwin-libwgpugd.a` = "be65f074cb7ae50e5784e7650f48579fff35f30ff663d1c01eabdc9f35c1f87c", 33 | # `x86_64-pc-windows-gnu-libwgpugd.a` = "26a05f6ee8c2f625027ffc77c97fc8ac9746a182f5bc53d64235999a02c0b0dc" 34 | # ) 35 | 36 | SYSINFO_OS <- tolower(Sys.info()[["sysname"]]) 37 | SYSINFO_MACHINE <- Sys.info()[["machine"]] 38 | HAS_32BIT_R <- dir.exists(file.path(R.home(), "bin", "i386")) 39 | USE_UCRT <- identical(R.version$crt, "ucrt") 40 | 41 | 42 | # Utilities --------------------------------------------------------------- 43 | 44 | #' Read a field of the package's DESCRIPTION file 45 | #' 46 | #' The field should have the prefix 47 | #' 48 | #' @param field 49 | #' Name of a field without prefix (e.g. `"check_cargo"`). 50 | #' @param prefix 51 | #' Prefix of the field (e.g. `"Config/rextendr/`). 52 | #' @param optional 53 | #' If `TRUE`, return `NA` when there's no field. Otherwise raise an error. 54 | #' 55 | get_desc_field <- function(field, prefix = DESC_FIELD_PREFIX, optional = TRUE) { 56 | field <- paste0(prefix, field) 57 | if (length(field) != 1) { 58 | stop("Field must be length one of character vector") 59 | } 60 | 61 | # `read.dcf()` always succeeds even when the field is missing. 62 | # Detect the failure by checking NA 63 | x <- read.dcf("DESCRIPTION", fields = field)[[1]] 64 | 65 | if (isTRUE(is.na(x)) && !isTRUE(optional)) { 66 | stop("Failed to get the field ", field, " from DESCRIPTION") 67 | } 68 | 69 | x 70 | } 71 | 72 | # This is tricky; while DESC_FIELD_PREFIX is used in get_desc_field()'s default, 73 | # this variable is defined by get_desc_field(). It's no problem as long as the 74 | # default is not used before it exists! 75 | DESC_FIELD_PREFIX <- paste0("Config/", get_desc_field("Package", prefix = ""), "/") 76 | 77 | 78 | safe_system2 <- function(cmd, args) { 79 | result <- list(success = FALSE, output = "") 80 | 81 | output_tmp <- tempfile() 82 | on.exit(unlink(output_tmp, force = TRUE)) 83 | 84 | suppressWarnings(ret <- system2(cmd, args, stdout = output_tmp)) 85 | 86 | if (!identical(ret, 0L)) { 87 | return(result) 88 | } 89 | 90 | result$output <- readLines(output_tmp) 91 | result$success <- TRUE 92 | result 93 | } 94 | 95 | # check_cargo ------------------------------------------------------------- 96 | 97 | #' Check if the cargo command exists and the version is above the requirements 98 | #' 99 | #' @return 100 | #' `TRUE` invisibly if no error was found. 101 | check_cargo <- function() { 102 | ### Check if cargo command works without error ### 103 | 104 | cat("*** Checking if cargo is installed\n") 105 | 106 | cargo_cmd <- "cargo" 107 | cargo_args <- "version" 108 | 109 | res_version <- safe_system2(cargo_cmd, cargo_args) 110 | 111 | if (!isTRUE(res_version$success)) { 112 | stop(errorCondition("cargo command is not available", class = c("wgpugd_error_cargo_check", "error"))) 113 | } 114 | 115 | # On Windows, check the toolchain as well. 116 | if (identical(SYSINFO_OS, "windows")) { 117 | cat("*** Checking if the required Rust toolchain is installed\n") 118 | 119 | toolchain <- windows_toolchain() 120 | cargo_args <- c(paste0("+", toolchain), cargo_args) 121 | 122 | res_version <- safe_system2(cargo_cmd, cargo_args) 123 | 124 | if (!isTRUE(res_version$success)) { 125 | stop(errorCondition( 126 | paste("cargo toolchain ", toolchain, " is not installed"), 127 | class = c("wgpugd_error_cargo_check", "error") 128 | )) 129 | } 130 | } 131 | 132 | ### Check the version ### 133 | 134 | msrv <- get_desc_field("MSRV", optional = TRUE) 135 | 136 | if (isTRUE(!is.na(msrv))) { 137 | cat("*** Checking if cargo is newer than the required version\n") 138 | 139 | version <- res_version$output 140 | 141 | ptn <- "cargo\\s+(\\d+\\.\\d+\\.\\d+)" 142 | m <- regmatches(version, regexec(ptn, version))[[1]] 143 | 144 | if (length(m) != 2) { 145 | stop(errorCondition("cargo version returned unexpected result", class = c("wgpugd_error_cargo_check", "error"))) 146 | } 147 | 148 | if (package_version(m[2]) < package_version(msrv)) { 149 | msg <- sprintf("The installed version of cargo (%s) is older than the requirement (%s)", m[2], msrv) 150 | stop(errorCondition(msg, class = c("wgpugd_error_cargo_check", "error"))) 151 | } 152 | } 153 | 154 | ### Check the toolchains ### 155 | if (identical(SYSINFO_OS, "windows")) { 156 | cat("*** Checking if the required Rust target is installed\n") 157 | 158 | expected_targets <- "x86_64-pc-windows-gnu" 159 | 160 | # If there is 32-bit version of R, check 32bit toolchain as well 161 | if (isTRUE(HAS_32BIT_R)) { 162 | expected_targets <- c(expected_targets, "i686-pc-windows-gnu") 163 | } 164 | 165 | targets <- safe_system2("rustup", c("target", "list", "--installed")) 166 | unavailable_targets <- setdiff(expected_targets, targets$output) 167 | if (length(unavailable_targets) != 0) { 168 | msg <- sprintf( 169 | "The required toolchain %s %s not installed", 170 | paste(unavailable_targets, collapse = " and "), 171 | if (length(unavailable_targets) == 1) "is" else "are" 172 | ) 173 | stop(errorCondition(msg, class = c("wgpugd_error_cargo_check", "error"))) 174 | } 175 | } 176 | 177 | invisible(TRUE) 178 | } 179 | 180 | #' Get the expected Windows toolchain. 181 | #' 182 | #' @return 183 | #' The expected windows toolchain as a length one of a character vector. 184 | windows_toolchain <- function() { 185 | x <- get_desc_field("windows_toolchain", optional = TRUE) 186 | if (isTRUE(!is.na(x))) { 187 | x 188 | } else { 189 | "stable-msvc" 190 | } 191 | } 192 | 193 | 194 | # download_precompiled ---------------------------------------------------- 195 | 196 | #' Download the precompiled binary if available. 197 | download_precompiled <- function() { 198 | 199 | ### Get URLs of precompiled binaries from DESCRIPTION ### 200 | 201 | github_repo <- get_desc_field("github_repo") 202 | github_tag <- get_desc_field("github_tag") 203 | crate_name <- get_desc_field("crate_name") 204 | if (isTRUE(is.na(crate_name))) { 205 | crate_name <- get_desc_field("Package", prefix = "") 206 | } 207 | 208 | if (isTRUE(is.na(github_repo) || is.na(github_tag) || is.na(crate_name))) { 209 | msg <- "No precompiled binary is available as GitHub repository is not specified on the DESCRIPTION file" 210 | stop(errorCondition(msg, class = c("wgpugd_error_download_precompiled", "error"))) 211 | } 212 | 213 | ### Get checksums from DESCRIPTION ### 214 | 215 | checksums <- get_expected_checksums() 216 | 217 | # For UCRT Windows, add ucrt- prefix 218 | crt_prefix <- if (isTRUE(USE_UCRT)) "ucrt-" else "" 219 | 220 | download_targets <- character(0) 221 | 222 | if (identical(SYSINFO_OS, "windows")) { 223 | download_targets <- "x86_64-pc-windows-gnu" 224 | 225 | # If there is 32-bit version installation, download the binary 226 | if (isTRUE(HAS_32BIT_R)) { 227 | download_targets <- c(download_targets, "i686-pc-windows-gnu") 228 | } 229 | 230 | sha256sum_cmd <- "sha256sum" 231 | sha256sum_cmd_args <- character(0) 232 | 233 | } else if (identical(SYSINFO_OS, "darwin")) { 234 | download_targets <- switch (SYSINFO_MACHINE, 235 | x86_64 = "x86_64-apple-darwin", 236 | arm64 = "aarch64-apple-darwin" 237 | ) 238 | 239 | sha256sum_cmd <- "shasum" 240 | sha256sum_cmd_args <- c("-a", "256") 241 | } 242 | 243 | if (length(download_targets) > 0) { 244 | # restrict only the available ones 245 | download_targets <- download_targets[sprintf("%s%s-lib%s.a", crt_prefix, download_targets, crate_name) %in% checksums$filename] 246 | } 247 | 248 | # If there's no checksum available for the platform, it means there's no binary 249 | if (length(download_targets) == 0) { 250 | msg <- sprintf("No precompiled binary is available for { os: %s, arch: %s }", 251 | SYSINFO_OS, SYSINFO_MACHINE) 252 | stop(errorCondition(msg, class = c("wgpugd_error_download_precompiled", "error"))) 253 | } 254 | 255 | # Test sha256sum command 256 | sha256sum_res <- safe_system2(sha256sum_cmd, "--version") 257 | 258 | if (!isTRUE(sha256sum_res$success)) { 259 | msg <- paste0(sha256sum_cmd, " command is not available") 260 | stop(errorCondition(msg, class = c("wgpugd_error_download_precompiled", "error"))) 261 | } 262 | 263 | ### Construct string templates for download URLs and destfiles ### 264 | 265 | if (identical(SYSINFO_OS, "windows")) { 266 | # On Windows, --target is specified, so the dir is nested one level deeper 267 | destfile_tmpl <- paste0("./src/rust/target/%s/release/lib", crate_name, ".a") 268 | } else { 269 | destfile_tmpl <- paste0("./src/rust/target/release/lib", crate_name, ".a") 270 | } 271 | 272 | ### Download the files ### 273 | 274 | for (target in download_targets) { 275 | src_file <- sprintf("%s%s-lib%s.a", crt_prefix, target, crate_name) 276 | checksum_expected <- checksums$sha256sum[checksums$filename == src_file] 277 | 278 | src_url <- paste0("https://github.com/", github_repo, "/releases/download/", github_tag, "/", src_file) 279 | 280 | if (identical(SYSINFO_OS, "windows")) { 281 | # On Windows, --target is specified, so the dir is nested one level deeper 282 | destfile <- paste0("./src/rust/target/", target, "/release/lib", crate_name, ".a") 283 | } else { 284 | destfile <- paste0("./src/rust/target/release/lib", crate_name, ".a") 285 | } 286 | 287 | cat(sprintf(" 288 | *** 289 | *** Download URL: %s 290 | *** Dest file: %s 291 | *** 292 | ", src_url, destfile)) 293 | 294 | dir.create(dirname(destfile), showWarnings = FALSE, recursive = TRUE) 295 | 296 | # Download the file 297 | tryCatch( 298 | { 299 | # To satisfy the CRAN repository policy about caring a slow internet connection 300 | options(timeout = max(300, getOption("timeout"))) 301 | download.file(src_url, destfile = destfile, mode = "wb", quiet = TRUE) 302 | }, 303 | error = function(e) { 304 | msg <- "Failed to download a precompiled binary" 305 | stop(errorCondition(msg, class = c("wgpugd_error_download_precompiled", "error"))) 306 | } 307 | ) 308 | 309 | check_checksum(sha256sum_cmd, sha256sum_cmd_args, destfile, checksum_expected) 310 | 311 | } ### End of for loop 312 | 313 | invisible(TRUE) 314 | } 315 | 316 | 317 | #' Get expected checksums written in DESCRIPTION 318 | #' 319 | #' @return 320 | #' A data.frame of checksums. 321 | get_expected_checksums <- function() { 322 | checksums <- get_desc_field("binary_sha256sum") 323 | 324 | if (isTRUE(is.na(checksums))) { 325 | msg <- sprintf("No precompiled binary is available; the DESCRIPTION file doesn't have %sbinary_sha256sum", DESC_FIELD_PREFIX) 326 | stop(errorCondition(msg, class = c("wgpugd_error_download_precompiled", "error"))) 327 | } 328 | 329 | tryCatch( 330 | { 331 | checksums <- eval(parse(text = checksums)) 332 | stopifnot(is.list(checksums)) 333 | }, 334 | error = function(e) { 335 | msg <- sprintf("The %sbinary_sha256sum field on the DESCRIPTION file is malformed.", DESC_FIELD_PREFIX) 336 | stop(errorCondition(msg, class = c("wgpugd_error_download_precompiled", "error"))) 337 | } 338 | ) 339 | 340 | checksums <- data.frame( 341 | filename = names(checksums), 342 | sha256sum = as.character(checksums) 343 | ) 344 | } 345 | 346 | #' Check if the SHA256sum of the file matches with the expected one. 347 | #' 348 | #' 349 | #' @param cmd 350 | #' Command to calculate SHA256sum (e.g. `"sha256sum"`) 351 | #' @param args 352 | #' Arguments to get passed to `cmd`. (e.g. `c("-a", "256")`) 353 | #' @param file 354 | #' File to check the checksum. 355 | #' @param expected 356 | #' Expected checksum. 357 | #' 358 | #' @return 359 | #' `TRUE` invisibly if no error was found. 360 | check_checksum <- function(cmd, args, file, expected) { 361 | # Get the checksum 362 | checksum_actual <- safe_system2(cmd, args = c(args, file)) 363 | if (!isTRUE(checksum_actual$success)) { 364 | msg <- paste("Failed to get the checksum of", file) 365 | stop(errorCondition(msg, class = c("wgpugd_error_download_precompiled", "error"))) 366 | } 367 | 368 | checksum_actual <- strsplit(checksum_actual$output, "\\s+")[[1]] 369 | 370 | if (length(checksum_actual) != 2) { 371 | msg <- paste0("The output of `", sprintf(sha256sum_cmd_tmpl, file), "` was unexpected") 372 | stop(errorCondition(msg, class = c("wgpugd_error_download_precompiled", "error"))) 373 | } 374 | 375 | checksum_actual <- checksum_actual[1] 376 | 377 | if (!identical(checksum_actual, expected)) { 378 | msg <- paste("Checksum mismatch for the pre-compiled binary: ", file) 379 | stop(errorCondition(msg, class = c("wgpugd_error_download_precompiled", "error"))) 380 | } 381 | 382 | invisible(TRUE) 383 | } 384 | 385 | # MAIN -------------------------------------------------------------------- 386 | 387 | ### Check cargo toolchain ### 388 | 389 | cargo_check_result <- tryCatch( 390 | check_cargo(), 391 | # Defer errors if it's raised by functions here 392 | wgpugd_error_cargo_check = function(e) e$message 393 | ) 394 | 395 | # If cargo is confirmed fine, exit here. But, even if the cargo is not available 396 | # or too old, it's not the end of the world. There might be a pre-compiled 397 | # binary available for the platform. 398 | if (isTRUE(cargo_check_result)) { 399 | cat("*** cargo is ok\n") 400 | quit("no", status = 0) 401 | } 402 | 403 | # If ABORT_WHEN_NO_CARGO is set, abort immediately without trying the binaries 404 | if (identical(Sys.getenv("ABORT_WHEN_NO_CARGO"), "true")) { 405 | cat(sprintf(" 406 | -------------- ERROR: CONFIGURATION FAILED -------------------- 407 | 408 | [cargo check result] 409 | %s 410 | 411 | [precompiled binary] 412 | ABORT_WHEN_NO_CARGO is set to true 413 | 414 | Please refer to to install Rust. 415 | 416 | --------------------------------------------------------------- 417 | 418 | ", cargo_check_result)) 419 | quit("no", status = 2) 420 | } 421 | 422 | ### Try downloading precompiled binaries ### 423 | 424 | download_precompiled_result <- tryCatch( 425 | download_precompiled(), 426 | # Defer errors if it's raised by functions here 427 | wgpugd_error_download_precompiled = function(e) e$message 428 | ) 429 | 430 | if (isTRUE(download_precompiled_result)) { 431 | cat("\n*** Successfully downloaded the precompied binary\n\n\n") 432 | # This needs to exit another status code to notify the status to the configure script 433 | quit("no", status = 100) 434 | } 435 | 436 | 437 | cat(sprintf(" 438 | -------------- ERROR: CONFIGURATION FAILED -------------------- 439 | 440 | [cargo check result] 441 | %s 442 | 443 | [precompiled binary] 444 | %s 445 | 446 | Please refer to to install Rust. 447 | 448 | --------------------------------------------------------------- 449 | 450 | ", cargo_check_result, download_precompiled_result)) 451 | quit("no", status = 3) 452 | -------------------------------------------------------------------------------- /src/rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod file; 2 | mod graphics_device; 3 | mod render_pipeline; 4 | mod text; 5 | 6 | use crate::file::FilenameTemplate; 7 | use crate::graphics_device::WgpugdCommand; 8 | 9 | use std::io::Write; 10 | use std::{fs::File, path::PathBuf}; 11 | 12 | use extendr_api::{ 13 | graphics::{DeviceDescriptor, DeviceDriver}, 14 | prelude::*, 15 | }; 16 | 17 | use lyon::lyon_tessellation::VertexBuffers; 18 | use render_pipeline::create_render_pipeline; 19 | use wgpu::util::DeviceExt; 20 | 21 | // For general shapes -------------------------------------------- 22 | 23 | #[repr(C)] 24 | #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] 25 | struct Vertex { 26 | position: [f32; 2], 27 | color: u32, 28 | } 29 | 30 | impl Vertex { 31 | const ATTRIBS: [wgpu::VertexAttribute; 2] = 32 | wgpu::vertex_attr_array![0 => Float32x2, 1 => Uint32]; 33 | 34 | fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { 35 | wgpu::VertexBufferLayout { 36 | array_stride: std::mem::size_of::() as wgpu::BufferAddress, 37 | step_mode: wgpu::VertexStepMode::Vertex, 38 | attributes: &Self::ATTRIBS, 39 | } 40 | } 41 | } 42 | 43 | // For circles ---------------------------------------------------- 44 | 45 | // For the sake of performance, we treat circle differently as they can be 46 | // simply represented by a SDF. 47 | 48 | #[repr(C)] 49 | #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] 50 | struct SDFVertex { 51 | position: [f32; 2], 52 | } 53 | 54 | impl SDFVertex { 55 | const ATTRIBS: [wgpu::VertexAttribute; 1] = wgpu::vertex_attr_array![0 => Float32x2]; 56 | 57 | fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { 58 | wgpu::VertexBufferLayout { 59 | array_stride: std::mem::size_of::() as wgpu::BufferAddress, 60 | step_mode: wgpu::VertexStepMode::Vertex, 61 | attributes: &Self::ATTRIBS, 62 | } 63 | } 64 | } 65 | 66 | // TODO: measure and set nicer default values 67 | const VERTEX_SIZE: usize = std::mem::size_of::(); 68 | const VERTEX_BUFFER_INITIAL_SIZE: u64 = VERTEX_SIZE as u64 * 10000; 69 | const INDEX_SIZE: usize = std::mem::size_of::(); 70 | const INDEX_BUFFER_INITIAL_SIZE: u64 = INDEX_SIZE as u64 * 10000; 71 | 72 | #[rustfmt::skip] 73 | const RECT_VERTICES: &[SDFVertex] = &[ 74 | SDFVertex { position: [ 1.0, -1.0] }, 75 | SDFVertex { position: [-1.0, -1.0] }, 76 | SDFVertex { position: [-1.0, 1.0] }, 77 | SDFVertex { position: [ 1.0, 1.0] }, 78 | ]; 79 | const RECT_INDICES: &[u16] = &[0, 1, 2, 0, 2, 3]; 80 | 81 | #[repr(C)] 82 | #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] 83 | pub(crate) struct SDFInstance { 84 | center: [f32; 2], 85 | radius: f32, 86 | stroke_width: f32, 87 | fill_color: u32, 88 | stroke_color: u32, 89 | } 90 | 91 | impl SDFInstance { 92 | const ATTRIBS: [wgpu::VertexAttribute; 5] = wgpu::vertex_attr_array![ 93 | 1 => Float32x2, 94 | 2 => Float32, 95 | 3 => Float32, 96 | 4 => Uint32, 97 | 5 => Uint32, 98 | ]; 99 | 100 | pub(crate) fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { 101 | wgpu::VertexBufferLayout { 102 | array_stride: std::mem::size_of::() as wgpu::BufferAddress, 103 | step_mode: wgpu::VertexStepMode::Instance, 104 | attributes: &Self::ATTRIBS, 105 | } 106 | } 107 | } 108 | 109 | #[repr(C)] 110 | #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] 111 | struct Globals { 112 | resolution: [f32; 2], 113 | } 114 | 115 | #[allow(dead_code)] 116 | struct WgpuGraphicsDevice { 117 | device: wgpu::Device, 118 | queue: wgpu::Queue, 119 | 120 | // For writing out a PNG 121 | texture: wgpu::Texture, 122 | texture_extent: wgpu::Extent3d, 123 | output_buffer: wgpu::Buffer, 124 | 125 | globals_bind_group: wgpu::BindGroup, 126 | globals_uniform_buffer: wgpu::Buffer, 127 | 128 | render_pipeline: wgpu::RenderPipeline, 129 | 130 | vertex_buffer: wgpu::Buffer, 131 | index_buffer: wgpu::Buffer, 132 | 133 | sdf_vertex_buffer: wgpu::Buffer, 134 | sdf_index_buffer: wgpu::Buffer, 135 | sdf_render_pipeline: wgpu::RenderPipeline, 136 | 137 | sdf_instances: Vec, 138 | 139 | geometry: VertexBuffers, 140 | 141 | // For MSAA 142 | multisampled_framebuffer: wgpu::TextureView, 143 | 144 | // On clipping or instanced rendering layer, increment this layer id 145 | current_command: Option, 146 | command_queue: Vec, 147 | 148 | // width and height in point 149 | width: u32, 150 | height: u32, 151 | 152 | // The unpadded and padded lengths are both needed because we prepare a 153 | // buffer in the padded size but do not read the padded part. 154 | unpadded_bytes_per_row: u32, 155 | padded_bytes_per_row: u32, 156 | 157 | filename: FilenameTemplate, 158 | cur_page: u32, 159 | } 160 | 161 | impl WgpuGraphicsDevice { 162 | fn filename(&self) -> PathBuf { 163 | self.filename.filename(self.cur_page) 164 | } 165 | 166 | async fn new(filename: &str, width: u32, height: u32) -> Self { 167 | // Set envvar WGPU_BACKEND to specific backend (e.g., vulkan, dx12, metal, opengl) 168 | let backend = wgpu::util::backend_bits_from_env().unwrap_or_else(wgpu::Backends::all); 169 | 170 | // An `Instance` is a "context for all other wgpu objects" 171 | let instance = wgpu::Instance::new(backend); 172 | 173 | // An `Adapter` is a "handle to a physical graphics" 174 | let adapter = instance 175 | .request_adapter(&wgpu::RequestAdapterOptions { 176 | power_preference: wgpu::PowerPreference::default(), 177 | compatible_surface: None, // Currently no window so no surface 178 | force_fallback_adapter: false, 179 | }) 180 | .await 181 | .unwrap(); 182 | 183 | // A `Device` is a "connection to a graphics device" and a `Queue` is a command queue. 184 | let (device, queue) = adapter 185 | .request_device(&Default::default(), None) 186 | .await 187 | .unwrap(); 188 | 189 | let texture_extent = wgpu::Extent3d { 190 | width, 191 | height, 192 | depth_or_array_layers: 1, 193 | }; 194 | 195 | // This texture is where the RenderPass renders. 196 | let texture = device.create_texture(&wgpu::TextureDescriptor { 197 | label: Some("wgpugd texture descriptor"), 198 | size: texture_extent, 199 | mip_level_count: 1, 200 | sample_count: 1, 201 | dimension: wgpu::TextureDimension::D2, 202 | // R don't use sRGB, so don't choose Rgba8UnormSrgb here! 203 | format: wgpu::TextureFormat::Rgba8Unorm, 204 | // The texture is a rendering target and passed in 205 | // `color_attachments`, so `RENDER_ATTACHMENT` is needed. Also, it's 206 | // where the image is copied from so `COPY_SRC` is needed. 207 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, 208 | }); 209 | 210 | // This code is from the example on the wgpu's repo. Why this is needed? 211 | // The comment on there says it's WebGPU's requirement that the buffer 212 | // size is a multiple of `wgpu::COPY_BYTES_PER_ROW_ALIGNMENT` 213 | // 214 | // ref: 215 | // https://github.com/gfx-rs/wgpu/blob/312828f12f1a1497bc0387a72a5346ef911acad7/wgpu/examples/capture/main.rs#L170-L191 216 | let bytes_per_pixel = std::mem::size_of::() as u32; 217 | let unpadded_bytes_per_row = width * bytes_per_pixel; 218 | let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as u32; 219 | let padded_bytes_per_row_padding = (align - unpadded_bytes_per_row % align) % align; 220 | let padded_bytes_per_row = unpadded_bytes_per_row + padded_bytes_per_row_padding; 221 | 222 | // Output buffer is where the texture is copied to, and then written out 223 | // as a PNG image. 224 | let output_buffer = device.create_buffer(&wgpu::BufferDescriptor { 225 | label: Some("wgpugd output buffer"), 226 | size: (padded_bytes_per_row * height) as u64, 227 | usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, 228 | mapped_at_creation: false, 229 | }); 230 | 231 | let globals_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { 232 | label: Some("wgpugd uniform buffer for globals"), 233 | size: std::mem::size_of::() as _, 234 | usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 235 | mapped_at_creation: false, 236 | }); 237 | 238 | let globals_bind_group_layout = 239 | device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 240 | label: Some("wgpugd globals bind group layout"), 241 | entries: &[wgpu::BindGroupLayoutEntry { 242 | binding: 0, 243 | visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, 244 | ty: wgpu::BindingType::Buffer { 245 | ty: wgpu::BufferBindingType::Uniform, 246 | has_dynamic_offset: false, 247 | min_binding_size: None, 248 | }, 249 | count: None, 250 | }], 251 | }); 252 | 253 | let globals_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { 254 | label: Some("wgpugd globals bind group"), 255 | layout: &globals_bind_group_layout, 256 | entries: &[wgpu::BindGroupEntry { 257 | binding: 0, 258 | resource: globals_uniform_buffer.as_entire_binding(), 259 | }], 260 | }); 261 | 262 | let multisampled_framebuffer = device 263 | .create_texture(&wgpu::TextureDescriptor { 264 | label: Some("wgpugd multisampled framebuffer"), 265 | size: texture_extent, 266 | mip_level_count: 1, 267 | sample_count: 4, 268 | dimension: wgpu::TextureDimension::D2, 269 | format: wgpu::TextureFormat::Rgba8Unorm, 270 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT, 271 | }) 272 | .create_view(&wgpu::TextureViewDescriptor::default()); 273 | 274 | let render_pipeline = create_render_pipeline( 275 | &device, 276 | "wgpugd render pipeline layout", 277 | "wgpugd render pipeline", 278 | &[&globals_bind_group_layout], 279 | &wgpu::include_wgsl!("shaders/shader.wgsl"), 280 | &[Vertex::desc()], 281 | 4, 282 | ); 283 | 284 | let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor { 285 | label: Some("wgpugd vertex buffer"), 286 | size: VERTEX_BUFFER_INITIAL_SIZE, 287 | usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, 288 | mapped_at_creation: false, 289 | }); 290 | 291 | let index_buffer = device.create_buffer(&wgpu::BufferDescriptor { 292 | label: Some("wgpugd index buffer"), 293 | size: INDEX_BUFFER_INITIAL_SIZE, 294 | usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST, 295 | mapped_at_creation: false, 296 | }); 297 | 298 | let sdf_vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 299 | label: Some("wgpugd vertex buffer"), 300 | contents: bytemuck::cast_slice(RECT_VERTICES), 301 | usage: wgpu::BufferUsages::VERTEX, 302 | }); 303 | 304 | let sdf_index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 305 | label: Some("wgpugd index buffer"), 306 | contents: bytemuck::cast_slice(RECT_INDICES), 307 | usage: wgpu::BufferUsages::INDEX, 308 | }); 309 | 310 | let sdf_render_pipeline = create_render_pipeline( 311 | &device, 312 | "wgpugd render pipeline layout for SDF shapes", 313 | "wgpugd render pipeline for SDF shapes", 314 | &[&globals_bind_group_layout], 315 | &wgpu::include_wgsl!("shaders/sdf_shape.wgsl"), 316 | &[SDFVertex::desc(), SDFInstance::desc()], 317 | // Technically, this doesn't need to be multisampled, as the SDF 318 | // shapes are out of scope of MSAA anyway, but as we share the 319 | // one renderpipline, the sample count must match the others. 320 | 4, 321 | ); 322 | 323 | let geometry: VertexBuffers = VertexBuffers::new(); 324 | 325 | Self { 326 | device, 327 | queue, 328 | texture, 329 | texture_extent, 330 | output_buffer, 331 | 332 | globals_bind_group, 333 | globals_uniform_buffer, 334 | 335 | render_pipeline, 336 | 337 | vertex_buffer, 338 | index_buffer, 339 | 340 | sdf_vertex_buffer, 341 | sdf_index_buffer, 342 | sdf_render_pipeline, 343 | 344 | sdf_instances: Vec::new(), 345 | 346 | geometry, 347 | 348 | multisampled_framebuffer, 349 | 350 | current_command: None, 351 | command_queue: Vec::new(), 352 | 353 | width, 354 | height, 355 | 356 | unpadded_bytes_per_row: unpadded_bytes_per_row as _, 357 | padded_bytes_per_row: padded_bytes_per_row as _, 358 | 359 | filename: FilenameTemplate::new(filename).unwrap(), 360 | // The page number starts with 0, but newPage() will be immediately 361 | // called and this gets incremented to 1. 362 | cur_page: 0, 363 | } 364 | } 365 | 366 | fn render(&mut self) -> extendr_api::Result<()> { 367 | // TODO: do this more nicely... 368 | if let Some(ref cmd) = self.current_command { 369 | self.command_queue.push(cmd.clone()); 370 | } 371 | 372 | // TODO: recreate the buffer when the data size is over the current buffer size. 373 | self.queue.write_buffer( 374 | &self.vertex_buffer, 375 | 0, 376 | bytemuck::cast_slice(self.geometry.vertices.as_slice()), 377 | ); 378 | self.queue.write_buffer( 379 | &self.index_buffer, 380 | 0, 381 | bytemuck::cast_slice(self.geometry.indices.as_slice()), 382 | ); 383 | 384 | let sdf_instance_buffer = 385 | &self 386 | .device 387 | .create_buffer_init(&wgpu::util::BufferInitDescriptor { 388 | label: Some("wgpugd instance buffer"), 389 | contents: bytemuck::cast_slice(self.sdf_instances.as_slice()), 390 | usage: wgpu::BufferUsages::VERTEX, 391 | }); 392 | 393 | self.queue.write_buffer( 394 | &self.globals_uniform_buffer, 395 | 0, 396 | bytemuck::cast_slice(&[Globals { 397 | resolution: [self.width as _, self.height as _], 398 | }]), 399 | ); 400 | 401 | let texture_view = self 402 | .texture 403 | .create_view(&wgpu::TextureViewDescriptor::default()); 404 | 405 | let mut encoder = self 406 | .device 407 | .create_command_encoder(&wgpu::CommandEncoderDescriptor { 408 | label: Some("wgpugd render encoder"), 409 | }); 410 | 411 | { 412 | let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 413 | label: Some("wgpugd render pass"), 414 | color_attachments: &[wgpu::RenderPassColorAttachment { 415 | view: &self.multisampled_framebuffer, 416 | resolve_target: Some(&texture_view), 417 | ops: wgpu::Operations { 418 | // TODO: set the proper error from the value of gp->bg 419 | load: wgpu::LoadOp::Clear(wgpu::Color::WHITE), 420 | // As described in the wgpu's example of MSAA, if the 421 | // pre-resolved MSAA data is not used anywhere else, we 422 | // should set this to false to save memory. 423 | store: false, 424 | }, 425 | }], 426 | depth_stencil_attachment: None, 427 | }); 428 | 429 | let mut begin_id_polygon = 0_u32; 430 | let mut last_id_polygon; 431 | let mut begin_id_sdf = 0_u32; 432 | let mut last_id_sdf; 433 | 434 | for cmd in self.command_queue.iter() { 435 | match cmd { 436 | WgpugdCommand::DrawPolygon(cmd) => { 437 | last_id_polygon = begin_id_polygon + cmd.count; 438 | 439 | render_pass.set_pipeline(&self.render_pipeline); 440 | render_pass.set_bind_group(0, &self.globals_bind_group, &[]); 441 | render_pass.set_vertex_buffer( 442 | 0, 443 | self.vertex_buffer 444 | .slice(0..(VERTEX_SIZE * self.geometry.vertices.len()) as _), 445 | ); 446 | render_pass.set_index_buffer( 447 | self.index_buffer 448 | .slice(0..(INDEX_SIZE * self.geometry.indices.len()) as _), 449 | wgpu::IndexFormat::Uint32, 450 | ); 451 | render_pass.draw_indexed(begin_id_polygon..last_id_polygon, 0, 0..1); 452 | 453 | begin_id_polygon = last_id_polygon; 454 | } 455 | WgpugdCommand::DrawSDF(cmd) => { 456 | last_id_sdf = begin_id_sdf + cmd.count; 457 | 458 | render_pass.set_pipeline(&self.sdf_render_pipeline); 459 | render_pass.set_bind_group(0, &self.globals_bind_group, &[]); 460 | render_pass.set_vertex_buffer(0, self.sdf_vertex_buffer.slice(..)); 461 | render_pass.set_vertex_buffer(1, sdf_instance_buffer.slice(..)); 462 | render_pass.set_index_buffer( 463 | self.sdf_index_buffer.slice(..), 464 | wgpu::IndexFormat::Uint16, 465 | ); 466 | render_pass.draw_indexed( 467 | 0..RECT_INDICES.len() as _, 468 | 0, 469 | begin_id_sdf..last_id_sdf, 470 | ); 471 | 472 | begin_id_sdf = last_id_sdf; 473 | } 474 | WgpugdCommand::SetClipping { 475 | x, 476 | y, 477 | height, 478 | width, 479 | } => render_pass.set_scissor_rect(*x, *y, *width, *height), 480 | } 481 | } 482 | 483 | // reprintln!("{:?}", self.geometry.vertices); 484 | // reprintln!("{:?}", self.sdf_instances); 485 | 486 | // Return the ownership. Otherwise the next operation on encoder would fail 487 | drop(render_pass); 488 | 489 | encoder.copy_texture_to_buffer( 490 | self.texture.as_image_copy(), 491 | wgpu::ImageCopyBuffer { 492 | buffer: &self.output_buffer, 493 | layout: wgpu::ImageDataLayout { 494 | offset: 0, 495 | bytes_per_row: Some( 496 | std::num::NonZeroU32::new(self.padded_bytes_per_row).unwrap(), 497 | ), 498 | // This parameter is needed when there are multiple 499 | // images, and it's not the case this time. 500 | rows_per_image: None, 501 | }, 502 | }, 503 | self.texture_extent, 504 | ); 505 | } 506 | 507 | self.queue.submit(Some(encoder.finish())); 508 | 509 | Ok(()) 510 | } 511 | 512 | // c.f. https://github.com/gfx-rs/wgpu/blob/312828f12f1a1497bc0387a72a5346ef911acad7/wgpu/examples/capture/main.rs#L119 513 | async fn write_png(&mut self) { 514 | let file = match File::create(self.filename()) { 515 | Ok(f) => f, 516 | Err(e) => { 517 | reprintln!("Failed to create the output file: {e:?}"); 518 | return; 519 | } 520 | }; 521 | 522 | let buffer_slice = self.output_buffer.slice(..); 523 | let buffer_future = buffer_slice.map_async(wgpu::MapMode::Read); 524 | 525 | // Wait for the future resolves 526 | self.device.poll(wgpu::Maintain::Wait); 527 | 528 | if let Ok(()) = buffer_future.await { 529 | let padded_buffer = buffer_slice.get_mapped_range(); 530 | 531 | let mut png_encoder = png::Encoder::new(file, self.width, self.height); 532 | 533 | png_encoder.set_depth(png::BitDepth::Eight); 534 | png_encoder.set_color(png::ColorType::Rgba); 535 | 536 | // TODO: handle results nicely 537 | let mut png_writer = png_encoder 538 | .write_header() 539 | .unwrap() 540 | .into_stream_writer_with_size(self.unpadded_bytes_per_row as _) 541 | .unwrap(); 542 | 543 | for chunk in padded_buffer.chunks(self.padded_bytes_per_row as _) { 544 | png_writer 545 | // while the buffer is padded, we only need the unpadded part 546 | .write_all(&chunk[..self.unpadded_bytes_per_row as _]) 547 | .unwrap(); 548 | } 549 | png_writer.finish().unwrap(); 550 | 551 | // With the current interface, we have to make sure all mapped views are 552 | // dropped before we unmap the buffer. 553 | drop(padded_buffer); 554 | 555 | self.output_buffer.unmap(); 556 | } 557 | } 558 | } 559 | 560 | /// A WebGPU Graphics Device for R 561 | /// 562 | /// @param filename 563 | /// @param width Device width in inch. 564 | /// @param height Device width in inch. 565 | /// @export 566 | #[extendr] 567 | fn wgpugd( 568 | #[default = "'Rplot%03d.png'"] filename: &str, 569 | #[default = "7"] width: i32, 570 | #[default = "7"] height: i32, 571 | ) { 572 | // Typically, 72 points per inch 573 | let width_pt = width * 72; 574 | let height_pt = height * 72; 575 | 576 | let device_driver = pollster::block_on(WgpuGraphicsDevice::new( 577 | filename, 578 | width_pt as _, 579 | height_pt as _, 580 | )); 581 | 582 | let device_descriptor = 583 | DeviceDescriptor::new().device_size(0.0, width_pt as _, 0.0, height_pt as _); 584 | 585 | device_driver.create_device::(device_descriptor, "wgpugd"); 586 | } 587 | 588 | extendr_module! { 589 | mod wgpugd; 590 | fn wgpugd; 591 | } 592 | -------------------------------------------------------------------------------- /src/rust/src/graphics_device.rs: -------------------------------------------------------------------------------- 1 | use std::{f32::consts::PI, os::raw::c_char}; 2 | 3 | use extendr_api::{ 4 | graphics::{ClippingStrategy, DevDesc, DeviceDriver, R_GE_gcontext, TextMetric}, 5 | prelude::*, 6 | }; 7 | 8 | use lyon::path::Path; 9 | use lyon::tessellation::geometry_builder::*; 10 | use lyon::tessellation::{FillOptions, FillTessellator, FillVertex}; 11 | use lyon::tessellation::{StrokeOptions, StrokeTessellator, StrokeVertex}; 12 | use ttf_parser::GlyphId; 13 | 14 | use glam::f32::Affine2; 15 | 16 | use crate::text::FONTDB; 17 | 18 | // TODO: determine tolerance nicely 19 | pub(crate) const DEFAULT_TOLERANCE: f32 = lyon::tessellation::FillOptions::DEFAULT_TOLERANCE; 20 | 21 | #[derive(Debug, Clone)] 22 | pub struct DrawCommand { 23 | pub count: u32, 24 | } 25 | 26 | impl DrawCommand { 27 | fn extend(&mut self, count: u32) { 28 | self.count += count; 29 | } 30 | } 31 | 32 | #[derive(Debug, Clone)] 33 | pub enum WgpugdCommand { 34 | // Draw tessellated polygons. 35 | DrawPolygon(DrawCommand), 36 | // Draw shapes represented by an SDF. 37 | DrawSDF(DrawCommand), 38 | // Set clipping range. 39 | SetClipping { 40 | x: u32, 41 | y: u32, 42 | width: u32, 43 | height: u32, 44 | }, 45 | } 46 | 47 | struct VertexCtor { 48 | color: u32, 49 | transform: Affine2, 50 | } 51 | 52 | impl VertexCtor { 53 | fn new(color: i32, transform: Affine2) -> Self { 54 | Self { 55 | color: unsafe { std::mem::transmute(color) }, 56 | transform, 57 | } 58 | } 59 | } 60 | 61 | impl StrokeVertexConstructor for VertexCtor { 62 | fn new_vertex(&mut self, vertex: StrokeVertex) -> crate::Vertex { 63 | let position_orig: mint::Point2<_> = vertex.position().into(); 64 | let position = self.transform.transform_point2(position_orig.into()); 65 | 66 | crate::Vertex { 67 | position: position.into(), 68 | color: self.color, 69 | } 70 | } 71 | } 72 | 73 | impl FillVertexConstructor for VertexCtor { 74 | fn new_vertex(&mut self, vertex: FillVertex) -> crate::Vertex { 75 | let position_orig: mint::Point2<_> = vertex.position().into(); 76 | let position = self.transform.transform_point2(position_orig.into()); 77 | 78 | crate::Vertex { 79 | position: position.into(), 80 | color: self.color, 81 | } 82 | } 83 | } 84 | 85 | impl crate::WgpuGraphicsDevice { 86 | fn tesselate_path_stroke(&mut self, path: &Path, stroke_options: &StrokeOptions, color: i32) { 87 | self.tesselate_path_stroke_with_transform( 88 | path, 89 | stroke_options, 90 | color, 91 | glam::Affine2::IDENTITY, 92 | ); 93 | } 94 | 95 | fn tesselate_path_stroke_with_transform( 96 | &mut self, 97 | path: &Path, 98 | stroke_options: &StrokeOptions, 99 | color: i32, 100 | transform: glam::Affine2, 101 | ) { 102 | if color.is_na() { 103 | return; 104 | } 105 | 106 | let mut stroke_tess = StrokeTessellator::new(); 107 | 108 | let ctxt = VertexCtor::new(color, transform); 109 | 110 | let count = stroke_tess 111 | .tessellate_path( 112 | path, 113 | stroke_options, 114 | &mut BuffersBuilder::new(&mut self.geometry, ctxt), 115 | ) 116 | .unwrap(); 117 | 118 | match self.current_command { 119 | // If the previous command was the same, squash them into one draw 120 | // command. 121 | Some(WgpugdCommand::DrawPolygon(ref mut cmd)) => { 122 | cmd.extend(count.indices); 123 | } 124 | // If the previous command was different, push it to the command 125 | // queue (if exists) and create a new command. 126 | _ => { 127 | let prev = self 128 | .current_command 129 | .replace(WgpugdCommand::DrawPolygon(DrawCommand { 130 | count: count.indices, 131 | })); 132 | if let Some(prev_cmd) = prev { 133 | self.command_queue.push(prev_cmd) 134 | } 135 | } 136 | } 137 | } 138 | 139 | fn tesselate_path_fill(&mut self, path: &Path, fill_options: &FillOptions, color: i32) { 140 | self.tesselate_path_fill_with_transform(path, fill_options, color, glam::Affine2::IDENTITY); 141 | } 142 | 143 | fn tesselate_path_fill_with_transform( 144 | &mut self, 145 | path: &Path, 146 | fill_options: &FillOptions, 147 | color: i32, 148 | transform: glam::Affine2, 149 | ) { 150 | if color.is_na() { 151 | return; 152 | } 153 | 154 | let mut fill_tess = FillTessellator::new(); 155 | 156 | let ctxt = VertexCtor::new(color, transform); 157 | 158 | let count = fill_tess 159 | .tessellate_path( 160 | path, 161 | fill_options, 162 | &mut BuffersBuilder::new(&mut self.geometry, ctxt), 163 | ) 164 | .unwrap(); 165 | 166 | match self.current_command { 167 | // If the previous command was the same, squash them into one draw 168 | // command. 169 | Some(WgpugdCommand::DrawPolygon(ref mut cmd)) => { 170 | cmd.extend(count.indices); 171 | } 172 | // If the previous command was different, push it to the command 173 | // queue (if exists) and create a new command. 174 | _ => { 175 | let prev = self 176 | .current_command 177 | .replace(WgpugdCommand::DrawPolygon(DrawCommand { 178 | count: count.indices, 179 | })); 180 | if let Some(prev_cmd) = prev { 181 | self.command_queue.push(prev_cmd) 182 | } 183 | } 184 | } 185 | } 186 | 187 | fn tesselate_rect_stroke( 188 | &mut self, 189 | rect: &lyon::math::Rect, 190 | stroke_options: &StrokeOptions, 191 | color: i32, 192 | ) { 193 | if color.is_na() { 194 | return; 195 | } 196 | 197 | let mut stroke_tess = StrokeTessellator::new(); 198 | 199 | let ctxt = VertexCtor::new(color, glam::Affine2::IDENTITY); 200 | 201 | let count = stroke_tess 202 | .tessellate_rectangle( 203 | rect, 204 | stroke_options, 205 | &mut BuffersBuilder::new(&mut self.geometry, ctxt), 206 | ) 207 | .unwrap(); 208 | 209 | match self.current_command { 210 | // If the previous command was the same, squash them into one draw 211 | // command. 212 | Some(WgpugdCommand::DrawPolygon(ref mut cmd)) => { 213 | cmd.extend(count.indices); 214 | } 215 | // If the previous command was different, push it to the command 216 | // queue (if exists) and create a new command. 217 | _ => { 218 | let prev = self 219 | .current_command 220 | .replace(WgpugdCommand::DrawPolygon(DrawCommand { 221 | count: count.indices, 222 | })); 223 | if let Some(prev_cmd) = prev { 224 | self.command_queue.push(prev_cmd) 225 | } 226 | } 227 | } 228 | } 229 | 230 | fn tesselate_rect_fill( 231 | &mut self, 232 | rect: &lyon::math::Rect, 233 | fill_options: &FillOptions, 234 | color: i32, 235 | ) { 236 | if color.is_na() { 237 | return; 238 | } 239 | 240 | let mut fill_tess = FillTessellator::new(); 241 | 242 | let ctxt = VertexCtor::new(color, glam::Affine2::IDENTITY); 243 | 244 | let count = fill_tess 245 | .tessellate_rectangle( 246 | rect, 247 | fill_options, 248 | &mut BuffersBuilder::new(&mut self.geometry, ctxt), 249 | ) 250 | .unwrap(); 251 | 252 | match self.current_command { 253 | // If the previous command was the same, squash them into one draw 254 | // command. 255 | Some(WgpugdCommand::DrawPolygon(ref mut cmd)) => { 256 | cmd.extend(count.indices); 257 | } 258 | // If the previous command was different, push it to the command 259 | // queue (if exists) and create a new command. 260 | _ => { 261 | let prev = self 262 | .current_command 263 | .replace(WgpugdCommand::DrawPolygon(DrawCommand { 264 | count: count.indices, 265 | })); 266 | if let Some(prev_cmd) = prev { 267 | self.command_queue.push(prev_cmd) 268 | } 269 | } 270 | } 271 | } 272 | 273 | // This handles polygon(), polyline(), and line(). 274 | #[allow(clippy::too_many_arguments)] 275 | fn polygon_inner>( 276 | &mut self, 277 | coords: T, 278 | color: i32, 279 | fill: i32, 280 | line_width: f32, 281 | line_cap: lyon::tessellation::LineCap, 282 | line_join: lyon::tessellation::LineJoin, 283 | mitre_limit: f32, 284 | close: bool, 285 | ) { 286 | let mut builder = Path::builder(); 287 | 288 | // 289 | // **** Build path *************************** 290 | // 291 | 292 | let mut coords = coords.into_iter(); 293 | 294 | // First point 295 | let (x, y) = coords.next().unwrap(); 296 | builder.begin(lyon::math::point(x as _, y as _)); 297 | 298 | coords.for_each(|(x, y)| { 299 | builder.line_to(lyon::math::point(x as _, y as _)); 300 | }); 301 | builder.end(close); 302 | 303 | let path = builder.build(); 304 | 305 | // 306 | // **** Tessellate fill *************************** 307 | // 308 | 309 | let fill_options = &FillOptions::tolerance(DEFAULT_TOLERANCE); 310 | self.tesselate_path_fill(&path, fill_options, fill); 311 | 312 | // 313 | // **** Tessellate stroke *************************** 314 | // 315 | 316 | let stroke_options = &StrokeOptions::tolerance(DEFAULT_TOLERANCE) 317 | .with_line_width(line_width) 318 | .with_line_cap(line_cap) 319 | .with_line_join(line_join) 320 | .with_miter_limit(mitre_limit); 321 | self.tesselate_path_stroke(&path, stroke_options, color); 322 | } 323 | } 324 | 325 | // TODO: avoid magic numbers 326 | fn translate_line_cap(lend: u32) -> lyon::tessellation::LineCap { 327 | match lend { 328 | 1 => lyon::tessellation::LineCap::Round, 329 | 2 => lyon::tessellation::LineCap::Butt, 330 | 3 => lyon::tessellation::LineCap::Square, 331 | _ => lyon::tessellation::LineCap::Round, 332 | } 333 | } 334 | 335 | // TODO: avoid magic numbers 336 | fn translate_line_join(ljoin: u32) -> lyon::tessellation::LineJoin { 337 | match ljoin { 338 | 1 => lyon::tessellation::LineJoin::Round, 339 | 2 => lyon::tessellation::LineJoin::Miter, 340 | 3 => lyon::tessellation::LineJoin::Bevel, 341 | _ => lyon::tessellation::LineJoin::Round, 342 | } 343 | } 344 | 345 | // R Internals says: 346 | // 347 | // > lwd = 1 should correspond to a line width of 1/96 inch 348 | // 349 | // and 1 inch is 72 points. 350 | fn translate_line_width(lwd: f64) -> f32 { 351 | lwd as f32 * 72. / 96. 352 | } 353 | 354 | impl DeviceDriver for crate::WgpuGraphicsDevice { 355 | const CLIPPING_STRATEGY: ClippingStrategy = ClippingStrategy::Device; 356 | 357 | fn line(&mut self, from: (f64, f64), to: (f64, f64), gc: R_GE_gcontext, _: DevDesc) { 358 | let color = gc.col; 359 | let line_width = translate_line_width(gc.lwd); 360 | let line_cap = translate_line_cap(gc.lend); 361 | let line_join = translate_line_join(gc.ljoin); 362 | let mitre_limit = gc.lmitre as f32; 363 | 364 | self.polygon_inner( 365 | [from, to], 366 | color, 367 | i32::na(), 368 | line_width, 369 | line_cap, 370 | line_join, 371 | mitre_limit, 372 | false, 373 | ); 374 | } 375 | 376 | fn polyline>( 377 | &mut self, 378 | coords: T, 379 | gc: R_GE_gcontext, 380 | _: DevDesc, 381 | ) { 382 | let color = gc.col; 383 | let line_width = translate_line_width(gc.lwd); 384 | let line_cap = translate_line_cap(gc.lend); 385 | let line_join = translate_line_join(gc.ljoin); 386 | let mitre_limit = gc.lmitre as f32; 387 | 388 | self.polygon_inner( 389 | coords, 390 | color, 391 | i32::na(), 392 | line_width, 393 | line_cap, 394 | line_join, 395 | mitre_limit, 396 | false, 397 | ); 398 | } 399 | 400 | fn polygon>( 401 | &mut self, 402 | coords: T, 403 | gc: R_GE_gcontext, 404 | _: DevDesc, 405 | ) { 406 | let color = gc.col; 407 | let fill = gc.fill; 408 | let line_width = translate_line_width(gc.lwd); 409 | let line_cap = translate_line_cap(gc.lend); 410 | let line_join = translate_line_join(gc.ljoin); 411 | let mitre_limit = gc.lmitre as f32; 412 | 413 | self.polygon_inner( 414 | coords, 415 | color, 416 | fill, 417 | line_width, 418 | line_cap, 419 | line_join, 420 | mitre_limit, 421 | false, 422 | ); 423 | } 424 | 425 | fn circle(&mut self, center: (f64, f64), r: f64, gc: R_GE_gcontext, _: DevDesc) { 426 | let color = gc.col; 427 | let fill = gc.fill; 428 | let line_width = translate_line_width(gc.lwd); 429 | 430 | self.sdf_instances.push(crate::SDFInstance { 431 | center: [center.0 as _, center.1 as _], 432 | radius: r as _, 433 | stroke_width: line_width, 434 | fill_color: unsafe { std::mem::transmute(fill) }, 435 | stroke_color: unsafe { std::mem::transmute(color) }, 436 | }); 437 | 438 | match self.current_command { 439 | // If the previous command was the same, squash them into one draw 440 | // command. 441 | Some(WgpugdCommand::DrawSDF(ref mut cmd)) => { 442 | cmd.extend(1); 443 | } 444 | // If the previous command was different, push it to the command 445 | // queue (if exists) and create a new command. 446 | _ => { 447 | let prev = self 448 | .current_command 449 | .replace(WgpugdCommand::DrawSDF(DrawCommand { count: 1 })); 450 | if let Some(prev_cmd) = prev { 451 | self.command_queue.push(prev_cmd) 452 | } 453 | } 454 | } 455 | } 456 | 457 | fn rect(&mut self, from: (f64, f64), to: (f64, f64), gc: R_GE_gcontext, _: DevDesc) { 458 | let color = gc.col; 459 | let fill = gc.fill; 460 | let line_width = translate_line_width(gc.lwd); 461 | let line_cap = translate_line_cap(gc.lend); 462 | let line_join = translate_line_join(gc.ljoin); 463 | let mitre_limit = gc.lmitre as f32; 464 | 465 | let x = from.0.min(to.0) as f32; 466 | let y = from.1.min(to.1) as f32; 467 | let w = (to.0 - from.0).abs() as f32; 468 | let h = (to.1 - from.1).abs() as f32; 469 | 470 | // 471 | // **** Tessellate fill *************************** 472 | // 473 | 474 | let fill_options = &FillOptions::tolerance(DEFAULT_TOLERANCE); 475 | self.tesselate_rect_fill(&lyon::math::rect(x, y, w, h), fill_options, fill); 476 | 477 | // 478 | // **** Tessellate stroke *************************** 479 | // 480 | 481 | let stroke_options = &StrokeOptions::tolerance(DEFAULT_TOLERANCE) 482 | .with_line_width(line_width) 483 | .with_line_cap(line_cap) 484 | .with_line_join(line_join) 485 | .with_miter_limit(mitre_limit); 486 | 487 | self.tesselate_rect_stroke(&lyon::math::rect(x, y, w, h), stroke_options, color); 488 | } 489 | 490 | // Wildly assumes 1 font has 1pt of width, and 10% of horizontal margins on 491 | // top and bottom. This should be properly done by querying to a font 492 | // database (e.g. https://github.com/RazrFalcon/fontdb). 493 | fn char_metric(&mut self, c: char, gc: R_GE_gcontext, _: DevDesc) -> TextMetric { 494 | let fontfamily = 495 | unsafe { std::ffi::CStr::from_ptr(&gc.fontfamily as *const c_char) }.to_string_lossy(); 496 | 497 | let id = match crate::text::FONTDB.query(&fontfamily, gc.fontface) { 498 | Some(id) => id, 499 | None => { 500 | reprintln!("[WARN] No fallback font found, aborting"); 501 | return TextMetric { 502 | ascent: 0.0, 503 | descent: 0.0, 504 | width: 0.0, 505 | }; 506 | } 507 | }; 508 | 509 | FONTDB 510 | .with_face_data(id, |font_data, face_index| { 511 | let font = ttf_parser::Face::from_slice(font_data, face_index).unwrap(); 512 | let scale = gc.cex * gc.ps / font.height() as f64; 513 | 514 | let glyph_id = font.glyph_index(c).unwrap_or(GlyphId(0)); 515 | 516 | match font.glyph_bounding_box(glyph_id) { 517 | Some(bbox) => TextMetric { 518 | ascent: bbox.y_max as f64 * scale, 519 | descent: bbox.y_min as f64 * scale, 520 | width: font 521 | .glyph_hor_advance(glyph_id) 522 | .unwrap_or(bbox.width() as _) as f64 523 | * scale, 524 | }, 525 | // If the glyph info is not available, use font info 526 | _ => TextMetric { 527 | ascent: font.ascender() as f64 * scale, 528 | descent: font.descender() as f64 * scale, 529 | width: font.height() as f64 * scale, 530 | }, 531 | } 532 | }) 533 | .unwrap() 534 | } 535 | 536 | fn text( 537 | &mut self, 538 | pos: (f64, f64), 539 | text: &str, 540 | angle: f64, 541 | hadj: f64, 542 | gc: R_GE_gcontext, 543 | _: DevDesc, 544 | ) { 545 | let fill = gc.col; 546 | 547 | let fontfamily = 548 | unsafe { std::ffi::CStr::from_ptr(&gc.fontfamily as *const c_char) }.to_string_lossy(); 549 | 550 | let id = match crate::text::FONTDB.query(&fontfamily, gc.fontface) { 551 | Some(id) => id, 552 | None => { 553 | reprintln!("[WARN] No fallback font found, aborting"); 554 | return; 555 | } 556 | }; 557 | 558 | FONTDB.with_face_data(id, |font_data, face_index| { 559 | let font = ttf_parser::Face::from_slice(font_data, face_index).unwrap(); 560 | 561 | let facetables = font.tables(); 562 | 563 | // Deviding by `height` is to normalize the font coordinates to 1. 564 | // Then, multiply by `cex` (size of the font in device specific 565 | // unit) and `px` (pointsize, should be 12) to convert to the value 566 | // in points. Since the range of the values actually matters on 567 | // tessellation, we need to multiply before tessellation. 568 | let scale = (gc.cex * gc.ps) as f32 / font.height() as f32; 569 | 570 | let mut builder = crate::text::LyonOutlineBuilder::new(scale); 571 | 572 | let mut prev_glyph: Option = None; 573 | for c in text.chars() { 574 | // Skip control characters. Note that it seems linebreaks are 575 | // handled on R's side, so we don't need to care about multiline 576 | // cases. 577 | if c.is_control() { 578 | prev_glyph = None; 579 | continue; 580 | } 581 | 582 | // Even when we cannot find glyph_id, fill it with 0. 583 | let cur_glyph = font.glyph_index(c).unwrap_or(GlyphId(0)); 584 | 585 | if let Some(prev_glyph) = prev_glyph { 586 | builder.add_offset_x(crate::text::find_kerning( 587 | facetables, prev_glyph, cur_glyph, 588 | ) as _); 589 | } 590 | 591 | font.outline_glyph(cur_glyph, &mut builder); 592 | 593 | if let Some(ha) = font.glyph_hor_advance(cur_glyph) { 594 | builder.add_offset_x(ha as _); 595 | } 596 | 597 | prev_glyph = Some(cur_glyph); 598 | } 599 | 600 | // First, move the origin depending on `hadj` 601 | let transform_hadj = 602 | glam::Affine2::from_translation(glam::vec2(builder.offset_x() * -hadj as f32, 0.0)); 603 | 604 | // Second, rotate and translate to the position 605 | let transform = glam::Affine2::from_angle_translation( 606 | angle as f32 / 360.0 * 2. * PI, 607 | glam::vec2(pos.0 as _, pos.1 as _), 608 | ) * transform_hadj; 609 | 610 | let path = builder.build(); 611 | 612 | // 613 | // **** Tessellate fill *************************** 614 | // 615 | 616 | let fill_options = &FillOptions::tolerance(DEFAULT_TOLERANCE); 617 | self.tesselate_path_fill_with_transform(&path, fill_options, fill, transform); 618 | }); 619 | } 620 | 621 | fn clip(&mut self, from: (f64, f64), to: (f64, f64), _: DevDesc) { 622 | let x0 = from.0.clamp(0.0, self.width as _); 623 | let x1 = to.0.clamp(0.0, self.width as _); 624 | let y0 = from.1.clamp(0.0, self.height as _); 625 | let y1 = to.1.clamp(0.0, self.height as _); 626 | 627 | let cmd = WgpugdCommand::SetClipping { 628 | x: x0.min(x1) as u32, 629 | // Y-axis is upside down 630 | y: self.height - y0.max(y1) as u32, 631 | width: (x0 - x1).abs() as u32, 632 | height: (y0 - y1).abs() as u32, 633 | }; 634 | 635 | let prev = self.current_command.replace(cmd); 636 | match prev { 637 | Some(WgpugdCommand::SetClipping { .. }) => { 638 | // If the new command just replacing the current clipping, do 639 | // nothing; just discard the old one. 640 | } 641 | Some(prev_cmd_inner) => { 642 | self.command_queue.push(prev_cmd_inner); 643 | } 644 | None => { 645 | // Do nothing 646 | } 647 | } 648 | } 649 | 650 | fn new_page(&mut self, _: R_GE_gcontext, _: DevDesc) { 651 | // newPage() is called soon after the device is open, but there's 652 | // nothing to render. So, skip rendering at first. 653 | if self.cur_page != 0 { 654 | self.render().unwrap(); 655 | 656 | pollster::block_on(self.write_png()); 657 | 658 | self.current_command = None; 659 | self.command_queue.clear(); 660 | self.geometry.indices.clear(); 661 | self.geometry.vertices.clear(); 662 | self.sdf_instances.clear(); 663 | } 664 | 665 | self.cur_page += 1; 666 | } 667 | 668 | fn close(&mut self, _: DevDesc) { 669 | self.render().unwrap(); 670 | pollster::block_on(self.write_png()); 671 | } 672 | } 673 | -------------------------------------------------------------------------------- /src/rust/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "adler32" 13 | version = "1.2.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" 16 | 17 | [[package]] 18 | name = "ahash" 19 | version = "0.7.6" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" 22 | dependencies = [ 23 | "getrandom", 24 | "once_cell", 25 | "version_check", 26 | ] 27 | 28 | [[package]] 29 | name = "aho-corasick" 30 | version = "0.7.18" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 33 | dependencies = [ 34 | "memchr", 35 | ] 36 | 37 | [[package]] 38 | name = "arrayvec" 39 | version = "0.5.2" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" 42 | 43 | [[package]] 44 | name = "arrayvec" 45 | version = "0.7.2" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" 48 | 49 | [[package]] 50 | name = "ash" 51 | version = "0.37.0+1.3.209" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "006ca68e0f2b03f22d6fa9f2860f85aed430d257fec20f8879b2145e7c7ae1a6" 54 | dependencies = [ 55 | "libloading", 56 | ] 57 | 58 | [[package]] 59 | name = "autocfg" 60 | version = "1.1.0" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 63 | 64 | [[package]] 65 | name = "bit-set" 66 | version = "0.5.2" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" 69 | dependencies = [ 70 | "bit-vec", 71 | ] 72 | 73 | [[package]] 74 | name = "bit-vec" 75 | version = "0.6.3" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" 78 | 79 | [[package]] 80 | name = "bitflags" 81 | version = "1.3.2" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 84 | 85 | [[package]] 86 | name = "block" 87 | version = "0.1.6" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" 90 | 91 | [[package]] 92 | name = "bumpalo" 93 | version = "3.9.1" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" 96 | 97 | [[package]] 98 | name = "bytemuck" 99 | version = "1.7.3" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "439989e6b8c38d1b6570a384ef1e49c8848128f5a97f3914baef02920842712f" 102 | dependencies = [ 103 | "bytemuck_derive", 104 | ] 105 | 106 | [[package]] 107 | name = "bytemuck_derive" 108 | version = "1.0.1" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "8e215f8c2f9f79cb53c8335e687ffd07d5bfcb6fe5fc80723762d0be46e7cc54" 111 | dependencies = [ 112 | "proc-macro2", 113 | "quote", 114 | "syn", 115 | ] 116 | 117 | [[package]] 118 | name = "byteorder" 119 | version = "1.4.3" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 122 | 123 | [[package]] 124 | name = "cc" 125 | version = "1.0.73" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" 128 | 129 | [[package]] 130 | name = "cfg-if" 131 | version = "1.0.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 134 | 135 | [[package]] 136 | name = "cfg_aliases" 137 | version = "0.1.1" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" 140 | 141 | [[package]] 142 | name = "codespan-reporting" 143 | version = "0.11.1" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" 146 | dependencies = [ 147 | "termcolor", 148 | "unicode-width", 149 | ] 150 | 151 | [[package]] 152 | name = "copyless" 153 | version = "0.1.5" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "a2df960f5d869b2dd8532793fde43eb5427cceb126c929747a26823ab0eeb536" 156 | 157 | [[package]] 158 | name = "core-foundation" 159 | version = "0.9.3" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 162 | dependencies = [ 163 | "core-foundation-sys", 164 | "libc", 165 | ] 166 | 167 | [[package]] 168 | name = "core-foundation-sys" 169 | version = "0.8.3" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 172 | 173 | [[package]] 174 | name = "core-graphics-types" 175 | version = "0.1.1" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" 178 | dependencies = [ 179 | "bitflags", 180 | "core-foundation", 181 | "foreign-types", 182 | "libc", 183 | ] 184 | 185 | [[package]] 186 | name = "crc32fast" 187 | version = "1.3.2" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 190 | dependencies = [ 191 | "cfg-if", 192 | ] 193 | 194 | [[package]] 195 | name = "cty" 196 | version = "0.2.2" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" 199 | 200 | [[package]] 201 | name = "d3d12" 202 | version = "0.4.1" 203 | source = "git+https://github.com/gfx-rs/d3d12-rs.git?rev=ffe5e261da0a6cb85332b82ab310abd2a7e849f6#ffe5e261da0a6cb85332b82ab310abd2a7e849f6" 204 | dependencies = [ 205 | "bitflags", 206 | "libloading", 207 | "winapi", 208 | ] 209 | 210 | [[package]] 211 | name = "deflate" 212 | version = "1.0.0" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f" 215 | dependencies = [ 216 | "adler32", 217 | ] 218 | 219 | [[package]] 220 | name = "euclid" 221 | version = "0.22.6" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "da96828553a086d7b18dcebfc579bd9628b016f86590d7453c115e490fa74b80" 224 | dependencies = [ 225 | "mint", 226 | "num-traits", 227 | ] 228 | 229 | [[package]] 230 | name = "extendr-api" 231 | version = "0.2.0" 232 | source = "git+https://github.com/extendr/extendr#66149191b8e7e5ab54b4c42470ee81aae7704345" 233 | dependencies = [ 234 | "extendr-engine", 235 | "extendr-macros", 236 | "lazy_static", 237 | "libR-sys", 238 | "libc", 239 | "paste", 240 | ] 241 | 242 | [[package]] 243 | name = "extendr-engine" 244 | version = "0.2.0" 245 | source = "git+https://github.com/extendr/extendr#66149191b8e7e5ab54b4c42470ee81aae7704345" 246 | dependencies = [ 247 | "libR-sys", 248 | ] 249 | 250 | [[package]] 251 | name = "extendr-macros" 252 | version = "0.2.0" 253 | source = "git+https://github.com/extendr/extendr#66149191b8e7e5ab54b4c42470ee81aae7704345" 254 | dependencies = [ 255 | "proc-macro2", 256 | "quote", 257 | "syn", 258 | ] 259 | 260 | [[package]] 261 | name = "float_next_after" 262 | version = "0.1.5" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "4fc612c5837986b7104a87a0df74a5460931f1c5274be12f8d0f40aa2f30d632" 265 | dependencies = [ 266 | "num-traits", 267 | ] 268 | 269 | [[package]] 270 | name = "fontdb" 271 | version = "0.8.0" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "029fc9730885d89fa728b849a381d0ffa739ff0eacfefed236284cb37852fe24" 274 | dependencies = [ 275 | "log", 276 | "memmap2", 277 | "ttf-parser", 278 | ] 279 | 280 | [[package]] 281 | name = "foreign-types" 282 | version = "0.3.2" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 285 | dependencies = [ 286 | "foreign-types-shared", 287 | ] 288 | 289 | [[package]] 290 | name = "foreign-types-shared" 291 | version = "0.1.1" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 294 | 295 | [[package]] 296 | name = "fxhash" 297 | version = "0.2.1" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" 300 | dependencies = [ 301 | "byteorder", 302 | ] 303 | 304 | [[package]] 305 | name = "getrandom" 306 | version = "0.2.4" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" 309 | dependencies = [ 310 | "cfg-if", 311 | "libc", 312 | "wasi", 313 | ] 314 | 315 | [[package]] 316 | name = "glam" 317 | version = "0.20.2" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "e4fa84eead97d5412b2a20aed4d66612a97a9e41e08eababdb9ae2bf88667490" 320 | dependencies = [ 321 | "mint", 322 | ] 323 | 324 | [[package]] 325 | name = "glow" 326 | version = "0.11.2" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "d8bd5877156a19b8ac83a29b2306fe20537429d318f3ff0a1a2119f8d9c61919" 329 | dependencies = [ 330 | "js-sys", 331 | "slotmap", 332 | "wasm-bindgen", 333 | "web-sys", 334 | ] 335 | 336 | [[package]] 337 | name = "gpu-alloc" 338 | version = "0.5.3" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "7fc59e5f710e310e76e6707f86c561dd646f69a8876da9131703b2f717de818d" 341 | dependencies = [ 342 | "bitflags", 343 | "gpu-alloc-types", 344 | ] 345 | 346 | [[package]] 347 | name = "gpu-alloc-types" 348 | version = "0.2.0" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "54804d0d6bc9d7f26db4eaec1ad10def69b599315f487d32c334a80d1efe67a5" 351 | dependencies = [ 352 | "bitflags", 353 | ] 354 | 355 | [[package]] 356 | name = "gpu-descriptor" 357 | version = "0.2.2" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "a538f217be4d405ff4719a283ca68323cc2384003eca5baaa87501e821c81dda" 360 | dependencies = [ 361 | "bitflags", 362 | "gpu-descriptor-types", 363 | "hashbrown", 364 | ] 365 | 366 | [[package]] 367 | name = "gpu-descriptor-types" 368 | version = "0.1.1" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "363e3677e55ad168fef68cf9de3a4a310b53124c5e784c53a1d70e92d23f2126" 371 | dependencies = [ 372 | "bitflags", 373 | ] 374 | 375 | [[package]] 376 | name = "hashbrown" 377 | version = "0.11.2" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 380 | dependencies = [ 381 | "ahash", 382 | ] 383 | 384 | [[package]] 385 | name = "hexf-parse" 386 | version = "0.2.1" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" 389 | 390 | [[package]] 391 | name = "indexmap" 392 | version = "1.8.0" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" 395 | dependencies = [ 396 | "autocfg", 397 | "hashbrown", 398 | ] 399 | 400 | [[package]] 401 | name = "inplace_it" 402 | version = "0.3.3" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "90953f308a79fe6d62a4643e51f848fbfddcd05975a38e69fdf4ab86a7baf7ca" 405 | 406 | [[package]] 407 | name = "instant" 408 | version = "0.1.12" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 411 | dependencies = [ 412 | "cfg-if", 413 | "js-sys", 414 | "wasm-bindgen", 415 | "web-sys", 416 | ] 417 | 418 | [[package]] 419 | name = "js-sys" 420 | version = "0.3.56" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" 423 | dependencies = [ 424 | "wasm-bindgen", 425 | ] 426 | 427 | [[package]] 428 | name = "khronos-egl" 429 | version = "4.1.0" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "8c2352bd1d0bceb871cb9d40f24360c8133c11d7486b68b5381c1dd1a32015e3" 432 | dependencies = [ 433 | "libc", 434 | "libloading", 435 | "pkg-config", 436 | ] 437 | 438 | [[package]] 439 | name = "lazy_static" 440 | version = "1.4.0" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 443 | 444 | [[package]] 445 | name = "libR-sys" 446 | version = "0.2.2" 447 | source = "git+https://github.com/extendr/libR-sys#0fe4143ad8d4e4cb42205a627088dbd1a64df3c7" 448 | dependencies = [ 449 | "winapi", 450 | ] 451 | 452 | [[package]] 453 | name = "libc" 454 | version = "0.2.119" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" 457 | 458 | [[package]] 459 | name = "libloading" 460 | version = "0.7.3" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" 463 | dependencies = [ 464 | "cfg-if", 465 | "winapi", 466 | ] 467 | 468 | [[package]] 469 | name = "lock_api" 470 | version = "0.4.6" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" 473 | dependencies = [ 474 | "scopeguard", 475 | ] 476 | 477 | [[package]] 478 | name = "log" 479 | version = "0.4.14" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 482 | dependencies = [ 483 | "cfg-if", 484 | ] 485 | 486 | [[package]] 487 | name = "lyon" 488 | version = "0.17.10" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "cf0510ed5e3e2fb80f3db2061ef5ca92d87bfda1a624bb1eacf3bd50226e4cbb" 491 | dependencies = [ 492 | "lyon_algorithms", 493 | "lyon_tessellation", 494 | ] 495 | 496 | [[package]] 497 | name = "lyon_algorithms" 498 | version = "0.17.7" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "8037f716541ba0d84d3de05c0069f8068baf73990d55980558b84d944c8a244a" 501 | dependencies = [ 502 | "lyon_path", 503 | "sid", 504 | ] 505 | 506 | [[package]] 507 | name = "lyon_geom" 508 | version = "0.17.6" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "ce99ce77c22bfd8f39a95b9c749dffbfc3e2491ea30c874764c801a8b1485489" 511 | dependencies = [ 512 | "arrayvec 0.5.2", 513 | "euclid", 514 | "num-traits", 515 | ] 516 | 517 | [[package]] 518 | name = "lyon_path" 519 | version = "0.17.7" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "5b0a59fdf767ca0d887aa61d1b48d4bbf6a124c1a45503593f7d38ab945bfbc0" 522 | dependencies = [ 523 | "lyon_geom", 524 | ] 525 | 526 | [[package]] 527 | name = "lyon_tessellation" 528 | version = "0.17.10" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "7230e08dd0638048e46f387f255dbe7a7344a3e6705beab53242b5af25635760" 531 | dependencies = [ 532 | "float_next_after", 533 | "lyon_path", 534 | ] 535 | 536 | [[package]] 537 | name = "malloc_buf" 538 | version = "0.0.6" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" 541 | dependencies = [ 542 | "libc", 543 | ] 544 | 545 | [[package]] 546 | name = "memchr" 547 | version = "2.4.1" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 550 | 551 | [[package]] 552 | name = "memmap2" 553 | version = "0.5.3" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "057a3db23999c867821a7a59feb06a578fcb03685e983dff90daf9e7d24ac08f" 556 | dependencies = [ 557 | "libc", 558 | ] 559 | 560 | [[package]] 561 | name = "metal" 562 | version = "0.23.1" 563 | source = "git+https://github.com/gfx-rs/metal-rs?rev=1aaa903#1aaa9033a22b2af7ff8cae2ed412a4733799c3d3" 564 | dependencies = [ 565 | "bitflags", 566 | "block", 567 | "core-graphics-types", 568 | "foreign-types", 569 | "log", 570 | "objc", 571 | ] 572 | 573 | [[package]] 574 | name = "miniz_oxide" 575 | version = "0.5.1" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" 578 | dependencies = [ 579 | "adler", 580 | ] 581 | 582 | [[package]] 583 | name = "mint" 584 | version = "0.5.9" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" 587 | 588 | [[package]] 589 | name = "naga" 590 | version = "0.8.0" 591 | source = "git+https://github.com/gfx-rs/naga?rev=1aa91549#1aa9154964238af8c692cf521ff90e1f2395e147" 592 | dependencies = [ 593 | "bit-set", 594 | "bitflags", 595 | "codespan-reporting", 596 | "hexf-parse", 597 | "indexmap", 598 | "log", 599 | "num-traits", 600 | "rustc-hash", 601 | "spirv", 602 | "termcolor", 603 | "thiserror", 604 | "unicode-xid", 605 | ] 606 | 607 | [[package]] 608 | name = "num-traits" 609 | version = "0.2.14" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 612 | dependencies = [ 613 | "autocfg", 614 | ] 615 | 616 | [[package]] 617 | name = "objc" 618 | version = "0.2.7" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" 621 | dependencies = [ 622 | "malloc_buf", 623 | "objc_exception", 624 | ] 625 | 626 | [[package]] 627 | name = "objc_exception" 628 | version = "0.1.2" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" 631 | dependencies = [ 632 | "cc", 633 | ] 634 | 635 | [[package]] 636 | name = "once_cell" 637 | version = "1.9.0" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" 640 | 641 | [[package]] 642 | name = "parking_lot" 643 | version = "0.11.2" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" 646 | dependencies = [ 647 | "instant", 648 | "lock_api", 649 | "parking_lot_core", 650 | ] 651 | 652 | [[package]] 653 | name = "parking_lot_core" 654 | version = "0.8.5" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" 657 | dependencies = [ 658 | "cfg-if", 659 | "instant", 660 | "libc", 661 | "redox_syscall", 662 | "smallvec", 663 | "winapi", 664 | ] 665 | 666 | [[package]] 667 | name = "paste" 668 | version = "1.0.6" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "0744126afe1a6dd7f394cb50a716dbe086cb06e255e53d8d0185d82828358fb5" 671 | 672 | [[package]] 673 | name = "pkg-config" 674 | version = "0.3.24" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" 677 | 678 | [[package]] 679 | name = "png" 680 | version = "0.17.3" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "8e8f1882177b17c98ec33a51f5910ecbf4db92ca0def706781a1f8d0c661f393" 683 | dependencies = [ 684 | "bitflags", 685 | "crc32fast", 686 | "deflate", 687 | "miniz_oxide", 688 | ] 689 | 690 | [[package]] 691 | name = "pollster" 692 | version = "0.2.5" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" 695 | 696 | [[package]] 697 | name = "proc-macro2" 698 | version = "1.0.36" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" 701 | dependencies = [ 702 | "unicode-xid", 703 | ] 704 | 705 | [[package]] 706 | name = "profiling" 707 | version = "1.0.5" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "9145ac0af1d93c638c98c40cf7d25665f427b2a44ad0a99b1dccf3e2f25bb987" 710 | 711 | [[package]] 712 | name = "quote" 713 | version = "1.0.15" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" 716 | dependencies = [ 717 | "proc-macro2", 718 | ] 719 | 720 | [[package]] 721 | name = "range-alloc" 722 | version = "0.1.2" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "63e935c45e09cc6dcf00d2f0b2d630a58f4095320223d47fc68918722f0538b6" 725 | 726 | [[package]] 727 | name = "raw-window-handle" 728 | version = "0.4.2" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "fba75eee94a9d5273a68c9e1e105d9cffe1ef700532325788389e5a83e2522b7" 731 | dependencies = [ 732 | "cty", 733 | ] 734 | 735 | [[package]] 736 | name = "redox_syscall" 737 | version = "0.2.10" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" 740 | dependencies = [ 741 | "bitflags", 742 | ] 743 | 744 | [[package]] 745 | name = "regex" 746 | version = "1.5.4" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" 749 | dependencies = [ 750 | "aho-corasick", 751 | "memchr", 752 | "regex-syntax", 753 | ] 754 | 755 | [[package]] 756 | name = "regex-syntax" 757 | version = "0.6.25" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" 760 | 761 | [[package]] 762 | name = "renderdoc-sys" 763 | version = "0.7.1" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "f1382d1f0a252c4bf97dc20d979a2fdd05b024acd7c2ed0f7595d7817666a157" 766 | 767 | [[package]] 768 | name = "rustc-hash" 769 | version = "1.1.0" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 772 | 773 | [[package]] 774 | name = "scopeguard" 775 | version = "1.1.0" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 778 | 779 | [[package]] 780 | name = "sid" 781 | version = "0.6.1" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "bd5ac56c121948b4879bba9e519852c211bcdd8f014efff766441deff0b91bdb" 784 | dependencies = [ 785 | "num-traits", 786 | ] 787 | 788 | [[package]] 789 | name = "slotmap" 790 | version = "1.0.6" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" 793 | dependencies = [ 794 | "version_check", 795 | ] 796 | 797 | [[package]] 798 | name = "smallvec" 799 | version = "1.8.0" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" 802 | 803 | [[package]] 804 | name = "spirv" 805 | version = "0.2.0+1.5.4" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "246bfa38fe3db3f1dfc8ca5a2cdeb7348c78be2112740cc0ec8ef18b6d94f830" 808 | dependencies = [ 809 | "bitflags", 810 | "num-traits", 811 | ] 812 | 813 | [[package]] 814 | name = "syn" 815 | version = "1.0.86" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" 818 | dependencies = [ 819 | "proc-macro2", 820 | "quote", 821 | "unicode-xid", 822 | ] 823 | 824 | [[package]] 825 | name = "termcolor" 826 | version = "1.1.2" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 829 | dependencies = [ 830 | "winapi-util", 831 | ] 832 | 833 | [[package]] 834 | name = "thiserror" 835 | version = "1.0.30" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" 838 | dependencies = [ 839 | "thiserror-impl", 840 | ] 841 | 842 | [[package]] 843 | name = "thiserror-impl" 844 | version = "1.0.30" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" 847 | dependencies = [ 848 | "proc-macro2", 849 | "quote", 850 | "syn", 851 | ] 852 | 853 | [[package]] 854 | name = "ttf-parser" 855 | version = "0.14.0" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "4ccbe8381883510b6a2d8f1e32905bddd178c11caef8083086d0c0c9ab0ac281" 858 | 859 | [[package]] 860 | name = "unicode-width" 861 | version = "0.1.9" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" 864 | 865 | [[package]] 866 | name = "unicode-xid" 867 | version = "0.2.2" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 870 | 871 | [[package]] 872 | name = "version_check" 873 | version = "0.9.4" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 876 | 877 | [[package]] 878 | name = "wasi" 879 | version = "0.10.2+wasi-snapshot-preview1" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 882 | 883 | [[package]] 884 | name = "wasm-bindgen" 885 | version = "0.2.79" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" 888 | dependencies = [ 889 | "cfg-if", 890 | "wasm-bindgen-macro", 891 | ] 892 | 893 | [[package]] 894 | name = "wasm-bindgen-backend" 895 | version = "0.2.79" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" 898 | dependencies = [ 899 | "bumpalo", 900 | "lazy_static", 901 | "log", 902 | "proc-macro2", 903 | "quote", 904 | "syn", 905 | "wasm-bindgen-shared", 906 | ] 907 | 908 | [[package]] 909 | name = "wasm-bindgen-futures" 910 | version = "0.4.29" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "2eb6ec270a31b1d3c7e266b999739109abce8b6c87e4b31fcfcd788b65267395" 913 | dependencies = [ 914 | "cfg-if", 915 | "js-sys", 916 | "wasm-bindgen", 917 | "web-sys", 918 | ] 919 | 920 | [[package]] 921 | name = "wasm-bindgen-macro" 922 | version = "0.2.79" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" 925 | dependencies = [ 926 | "quote", 927 | "wasm-bindgen-macro-support", 928 | ] 929 | 930 | [[package]] 931 | name = "wasm-bindgen-macro-support" 932 | version = "0.2.79" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" 935 | dependencies = [ 936 | "proc-macro2", 937 | "quote", 938 | "syn", 939 | "wasm-bindgen-backend", 940 | "wasm-bindgen-shared", 941 | ] 942 | 943 | [[package]] 944 | name = "wasm-bindgen-shared" 945 | version = "0.2.79" 946 | source = "registry+https://github.com/rust-lang/crates.io-index" 947 | checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" 948 | 949 | [[package]] 950 | name = "web-sys" 951 | version = "0.3.56" 952 | source = "registry+https://github.com/rust-lang/crates.io-index" 953 | checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" 954 | dependencies = [ 955 | "js-sys", 956 | "wasm-bindgen", 957 | ] 958 | 959 | [[package]] 960 | name = "wgpu" 961 | version = "0.12.0" 962 | source = "git+https://github.com/gfx-rs/wgpu/#6fadbdecf20ee85f885e9fd821160e808657d780" 963 | dependencies = [ 964 | "arrayvec 0.7.2", 965 | "js-sys", 966 | "log", 967 | "naga", 968 | "parking_lot", 969 | "raw-window-handle", 970 | "smallvec", 971 | "wasm-bindgen", 972 | "wasm-bindgen-futures", 973 | "web-sys", 974 | "wgpu-core", 975 | "wgpu-hal", 976 | "wgpu-types", 977 | ] 978 | 979 | [[package]] 980 | name = "wgpu-core" 981 | version = "0.12.0" 982 | source = "git+https://github.com/gfx-rs/wgpu/#6fadbdecf20ee85f885e9fd821160e808657d780" 983 | dependencies = [ 984 | "arrayvec 0.7.2", 985 | "bitflags", 986 | "cfg_aliases", 987 | "codespan-reporting", 988 | "copyless", 989 | "fxhash", 990 | "log", 991 | "naga", 992 | "parking_lot", 993 | "profiling", 994 | "raw-window-handle", 995 | "smallvec", 996 | "thiserror", 997 | "wgpu-hal", 998 | "wgpu-types", 999 | ] 1000 | 1001 | [[package]] 1002 | name = "wgpu-hal" 1003 | version = "0.12.0" 1004 | source = "git+https://github.com/gfx-rs/wgpu/#6fadbdecf20ee85f885e9fd821160e808657d780" 1005 | dependencies = [ 1006 | "arrayvec 0.7.2", 1007 | "ash", 1008 | "bit-set", 1009 | "bitflags", 1010 | "block", 1011 | "core-graphics-types", 1012 | "d3d12", 1013 | "foreign-types", 1014 | "fxhash", 1015 | "glow", 1016 | "gpu-alloc", 1017 | "gpu-descriptor", 1018 | "inplace_it", 1019 | "js-sys", 1020 | "khronos-egl", 1021 | "libloading", 1022 | "log", 1023 | "metal", 1024 | "naga", 1025 | "objc", 1026 | "parking_lot", 1027 | "profiling", 1028 | "range-alloc", 1029 | "raw-window-handle", 1030 | "renderdoc-sys", 1031 | "thiserror", 1032 | "wasm-bindgen", 1033 | "web-sys", 1034 | "wgpu-types", 1035 | "winapi", 1036 | ] 1037 | 1038 | [[package]] 1039 | name = "wgpu-types" 1040 | version = "0.12.0" 1041 | source = "git+https://github.com/gfx-rs/wgpu/#6fadbdecf20ee85f885e9fd821160e808657d780" 1042 | dependencies = [ 1043 | "bitflags", 1044 | ] 1045 | 1046 | [[package]] 1047 | name = "wgpugd" 1048 | version = "0.1.0" 1049 | dependencies = [ 1050 | "bytemuck", 1051 | "euclid", 1052 | "extendr-api", 1053 | "fontdb", 1054 | "glam", 1055 | "lyon", 1056 | "mint", 1057 | "once_cell", 1058 | "png", 1059 | "pollster", 1060 | "regex", 1061 | "ttf-parser", 1062 | "wgpu", 1063 | ] 1064 | 1065 | [[package]] 1066 | name = "winapi" 1067 | version = "0.3.9" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1070 | dependencies = [ 1071 | "winapi-i686-pc-windows-gnu", 1072 | "winapi-x86_64-pc-windows-gnu", 1073 | ] 1074 | 1075 | [[package]] 1076 | name = "winapi-i686-pc-windows-gnu" 1077 | version = "0.4.0" 1078 | source = "registry+https://github.com/rust-lang/crates.io-index" 1079 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1080 | 1081 | [[package]] 1082 | name = "winapi-util" 1083 | version = "0.1.5" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1086 | dependencies = [ 1087 | "winapi", 1088 | ] 1089 | 1090 | [[package]] 1091 | name = "winapi-x86_64-pc-windows-gnu" 1092 | version = "0.4.0" 1093 | source = "registry+https://github.com/rust-lang/crates.io-index" 1094 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1095 | --------------------------------------------------------------------------------