├── .cargo └── config.toml ├── .envrc ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── Trunk.toml ├── flake.lock ├── flake.nix ├── index.html ├── lc3b ├── Cargo.toml └── src │ └── main.rs ├── misc ├── cpu.lpbp └── screenshots │ ├── logic-paint-cpu.png │ ├── logic-paint-register-file.png │ └── logic-paint-scale.png ├── src ├── coords.rs ├── lib.rs ├── module.rs ├── project.rs ├── shaders │ ├── cell.frag │ └── cell.vert ├── substrate │ ├── buffer.rs │ ├── buffer_brush.rs │ ├── buffer_delta.rs │ ├── buffer_serde.rs │ ├── compiler.rs │ ├── compress.rs │ ├── execution_context.rs │ ├── io.rs │ ├── label_builder.rs │ ├── mask.rs │ └── mod.rs ├── tools │ ├── camera_controller.rs │ ├── draw_metal.rs │ ├── draw_si.rs │ ├── mod.rs │ ├── place_socket.rs │ └── visual.rs ├── upc.rs ├── utils │ ├── convert.rs │ ├── iter.rs │ ├── log.rs │ ├── mod.rs │ ├── names.rs │ └── selection.rs ├── viewport.rs └── wgl2 │ ├── camera.rs │ ├── cell_program.rs │ ├── mod.rs │ ├── quad_vao.rs │ ├── texture.rs │ └── uniform.rs └── templates └── font_file.lpbp /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.wasm32-unknown-unknown] 2 | rustflags = ["-C", "target-feature=+atomics,+bulk-memory,+mutable-globals"] 3 | 4 | [unstable] 5 | build-std = ["std,panic_abort"] 6 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | .cache 4 | .direnv 5 | .docusaurus 6 | .env.secrets 7 | .env.*.secrets 8 | .env.local 9 | .idea 10 | .node_repl_history 11 | .npm 12 | build 13 | coverage 14 | dist 15 | dist_tmp 16 | files-cache 17 | invoice.txt 18 | logs 19 | node_modules 20 | npm-debug.log* 21 | pids 22 | pkg 23 | pocketbase 24 | stats.json 25 | target 26 | yarn-debug.log* 27 | yarn-error.log* 28 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "logic-paint-rs" 3 | version = "0.1.0" 4 | authors = ["Alec Thilenius "] 5 | description = "A logic simulation library written in Rust, inspired by KOHCTPYKTOP." 6 | license = "MIT/Apache-2.0" 7 | repository = "https://gitlab.com/athilenius/logic-paint-rs" 8 | edition = "2018" 9 | 10 | [lib] 11 | crate-type = ["cdylib", "rlib"] 12 | 13 | [features] 14 | default = ["console_error_panic_hook"] 15 | 16 | [dependencies] 17 | arrayvec = { version = "0.7", features = ["serde"] } 18 | base64 = "0.22" 19 | bincode = "2.0.0-rc.3" 20 | console_error_panic_hook = { version = "0.1", optional = true } 21 | futures = "0.3" 22 | glam = { git = "https://github.com/athilenius/glam-rs", default-features = false, features = [ 23 | "serde", 24 | "bytemuck", 25 | "wasm-bindgen", 26 | ] } 27 | im = { version = "15.1", features = ["serde"] } 28 | itertools = "0.14" 29 | js-sys = "0.3" 30 | lazy_static = "1.5.0" 31 | serde = { version = "1.0", features = ["derive", "rc"] } 32 | serde-wasm-bindgen = "0.6" 33 | serde_json = "1.0" 34 | snap = "1.1" 35 | thiserror = "2.0" 36 | tsify = "0.4" 37 | wasm-bindgen-futures = "0.4" 38 | wasm_thread = { version = "0.3" } 39 | 40 | [dependencies.wasm-bindgen] 41 | # Must be pinned to the exact same version as `wasm-bindgen --version` 42 | version = "0.2.100" 43 | 44 | [dev-dependencies] 45 | brotli = "7.0.0" 46 | flate2 = "1.0.34" 47 | tiff = "0.9.1" 48 | 49 | [dependencies.web-sys] 50 | version = "0.3" 51 | features = [ 52 | "CssStyleDeclaration", 53 | "HtmlCanvasElement", 54 | "KeyboardEvent", 55 | "MouseEvent", 56 | "Performance", 57 | "StyleSheet", 58 | "WebGl2RenderingContext", 59 | "WebGlBuffer", 60 | "WebGlProgram", 61 | "WebGlShader", 62 | "WebGlTexture", 63 | "WebGlUniformLocation", 64 | "WebGlVertexArrayObject", 65 | "WheelEvent", 66 | "Window", 67 | "console", 68 | ] 69 | 70 | [profile.profiling] 71 | inherits = "release" 72 | strip = true 73 | opt-level = 3 74 | lto = true 75 | panic = "abort" 76 | 77 | [profile.release] 78 | strip = true 79 | opt-level = 3 80 | lto = true 81 | panic = "abort" 82 | # Debug symbols 83 | debug = true 84 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Logic Paint 2 | 3 | Inspired by Zachtronic's KOHCTPYKTOP, Logic Paint allows you to edit and 4 | simulate transistors, at scale. 5 | 6 | ![Register File](misc/screenshots/logic-paint-cpu.png) 7 | 8 | _A 16-bit micro-coded CPU I built based in Logic Paint, on the LC-3B 9 | micro-architecture._ 10 | 11 | # 2024 - Under Construction 🚧 12 | 13 | **We are moving to the web like it's 1999 😎** 14 | 15 | This project was originally designed to run as a vscode extension. Since then, 16 | vscode has made breaking changes. Also vscode extensions were never what I 17 | really wanted from this project. I'm currently in the middle of moving the whole 18 | thing over to a web interface, where you can explore other people's designs 19 | without needing an account, and make your own designs by logging in. I also have 20 | some exciting plans to support user-created plugins! 21 | 22 | # Standard Controls 23 | 24 | The camera can be panned at any time (including while actively painting) by 25 | holding down the `Space` key. It can be zoomed at any time with the scroll 26 | wheel. 27 | 28 | Standard controls like `Ctrl+Z` and `Ctrl+S` to undo and save should work just 29 | fine. 30 | 31 | ## Modal Editing 32 | 33 | Logic Paint is modal, the active mode is displayed in the upper left corner of 34 | the editor window. It has 4 primary modes: 35 | 36 | - `Visual` (the starting mode) lets you select, copy, paste and delete cells. 37 | - `Silicon` paints (or removes) both N and P type silicon doping onto the 38 | substrate. 39 | - `Metal` paints (or removes) a metal layer and via placements. 40 | - `Execution` compiles logic and prepares to execute it. 41 | - `ModuleEdit` add, remove, and edit modules (I/O to/from the substrate). 42 | - `Label` create a text label using Silicon paint. 43 | 44 | ## Visual Mode (`ESC`) 45 | 46 | Visual mode is accessed by clicking `ESC` from any other mode. While in visual 47 | mode you can perform the following actions: 48 | 49 | - **`LMB`** Create a selection with `LMB` and dragging. Clear the selection with 50 | `ESC` or `RMB`. 51 | - **`Y`** Copy ("yank") the current selection into the mouse-follow buffer. Use 52 | `LMB` to paste the mouse-follow buffer. 53 | - **`D`** Copy the current selection into the mouse-follow buffer _and_ delete 54 | the selection. 55 | - **`Shift+D`** Delete the selection _without_ copying it into the mouse-follow 56 | - **`S+[0-9]`** With a selection _or_ something currently in the mouse-follow 57 | buffer, you can hold `S` and click a number to 'store' that into a numbered 58 | register. 59 | - **`[0-9]`** Once something is stored in a numbered register, you can load it 60 | into the mouse-follow buffer by clicking the number (without holding `S`). 61 | - **`Shift+S+8` and `Shift+8`** Like VIM, there is a special register for 62 | copying/pasting into the system clipboard, the `*` register. This register 63 | will behave like the numbered registers, apart from the fact that it will 64 | always contain the system-clipboard value. 65 | - **`R`** while the mouse-follow buffer is active to rotate it. 66 | - **`M`** while the mouse-follow buffer is active to mirror it about the X axis. 67 | 68 | ### Visual Mouse-follow Buffer 69 | 70 | There is a special buffer that is used for copy-paste and rotation operations, 71 | the "mouse-follow buffer". It is filled when: 72 | 73 | - A selection is yanked with `Y`, or deleted with `D` 74 | - A named register is selected using `[0-9]` or `*` 75 | 76 | While the mouse-follow buffer is active, it will show a preview of the **results 77 | of placing the buffer onto the main blueprint**, not the contents of the buffer 78 | itself. Meaning, the mouse-follow buffer can contain invalid connections (ie 79 | cells that claim to connect with a neighbor who doesn't connect back) and these 80 | broken connections will be 'stitched together' on-paste, when possible. The 81 | mouse-buffer is ephemeral and can be cleared at any time by clicking `ESC` or 82 | `RMB`. Additionally, clicking `R` will rotate the mouse-follow buffer 90 degrees 83 | clockwise, and `M` will mirror it across the X axis (use `R`, `R`, `M` to mirror 84 | about the X axis). 85 | 86 | ## Silicon Mode (`Q`) 87 | 88 | Silicon mode is accessed with the `Q` key. It is used for painting and erasing 89 | silicon doping. While in Silicon mode you can paint N and P type silicon, and 90 | form gates by dragging N-type on top of a P-type trace, or vice versa. 91 | 92 | - **`LMB`** Paint a line of N-type silicon 93 | - **`RMB` (Or `Shift+LMB`)** Paint a line of P-type silicon 94 | - **`Ctrl+LMB`** Erase silicon of either type 95 | 96 | ## Metal Mode (`W`) 97 | 98 | Metal mode is accessed with the `W` key. It is used to connect Silicon together. 99 | It lives "above" the silicon and does not touch the silicon unless you place a 100 | via to connect them. 101 | 102 | - **`LMB`** Paint a line of metal. 103 | - **`RMB` (Or `Shift+LMB`)** Places a Via. Can only be used on a cell that has 104 | both metal and silicon and isn't a transistor. 105 | - **`Ctrl+LMB`** Erase metal and vias. 106 | 107 | ## Execution Mode (`E`) 108 | 109 | Execution mode is accessed with the `E` key. It used to 'simulate' the 110 | substrate. It allows for both single-stepping as well as continuous running 111 | modes. Right now the only run mode is one fundamental clock per frame, but more 112 | will be added later for faster running. 113 | 114 | - **`R`** Enters run-mode. This will execute one fundamental clock per frame. 115 | - **`C`** Pauses run mode (if running) and executes a single fundamental clock. 116 | - **`P`** Pauses run mode. 117 | - **`T`** Executes a single simulation 'tick'. This is mostly for debugging 118 | Logic Paint itself, as ticks have very little parallel with propagation delay. 119 | 120 | ## Module Edit (`A`) 121 | 122 | The module edit mode is used to place, remove and edit modules. These include 123 | clocks, const values, probes and large chunks of RAM. Access the editor by 124 | pressing `A` while in `Visual` mode. Pressing `A` again cycles through module 125 | types that can be placed. While in Module Edit mode, you can visually see module 126 | "roots", ie the cell where the module actually resides. Only one module can 127 | exist per root at a time. 128 | 129 | ## Label Mode (`Enter`) 130 | 131 | Useful tool to quickly create text labels in Silicon. Enter the label mode by 132 | hitting `Enter`, then type your text. The text will be rendered to the mouse 133 | follow buffer. Clicking `LMB` will place a copy of the text (without the cursor) 134 | onto the primary buffer. Click `ESC` to exit label mode. 135 | 136 | # Scale and Performance 137 | 138 | There are a handful of these projects out there, of note 139 | [PharoChipDesigner](https://github.com/pavel-krivanek/PharoChipDesigner) has a 140 | wonderful writeup on both digital circuitry and how this type of logic differs 141 | from real-world MOSFET/BJT/CMOS technology. Also of note is Andre's [Angular 142 | version](https://blog.tst.sh/kohctpyktop-2-electric-bogaloo/) as well as many 143 | more. 144 | 145 | I wanted to build an entire CPU in this type of format (currently in-progress). 146 | No existing project comes remotely close to the level or performance or scale I 147 | needed for that. My ideal was fixed-cost rendering, and the ability to handle 148 | millions of transistors. Additionally, I wanted a much more refined and 149 | functional suite of design tools. 150 | 151 | Logic Paint pulls that off by rendering cells exclusively in a fragment shader 152 | (on the GPU) meaning rendering is based only on the display resolution and is 153 | irrespective of the number of cells being drawn (modulo data transfer during 154 | simulation). It's not an exaggeration to say that the renderer can handle 155 | **billions** of populated cells on modern GPUs, it's limited only by your GPUs 156 | memory, where each cell takes up 4 bytes of data. However, the culling and sync 157 | code on my machine starts to chug at around 2 million cells. But I've done zero 158 | optimizations on it. 159 | 160 | The core of the application is written in Rust, and compiled to WebAssembly. 161 | Right out of the box this gives you some immense performance wins, but more 162 | importantly it allows for directly memory manipulation and blitting which in 163 | tern allows for very fast designs. 164 | 165 | ## Pics or it didn't happen 166 | 167 | This is about a half million cells, rendering at 4K and taking only 2-3ms per 168 | frame. More cells wouldn't slow down rendering though, it would only create 169 | sampling artifacts and a colorful view. I get to about 2 million cells before 170 | the CPU-side of rendering calls start to chug. At 7 million cells I'm at 80ms 171 | frames. 172 | ![Scale](../misc/screenshots/logic-paint-scale.png) 173 | 174 | # License 🧾 175 | 176 | Logic Paint is dual licensed (at your option) 177 | 178 | - MIT License ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT)) 179 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)) 180 | -------------------------------------------------------------------------------- /Trunk.toml: -------------------------------------------------------------------------------- 1 | # Needed for wasm_threads (which uses SharedArrayBuffers) 2 | [serve.headers] 3 | "cross-origin-embedder-policy" = "require-corp" 4 | "cross-origin-opener-policy" = "same-origin" 5 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1742288794, 24 | "narHash": "sha256-Txwa5uO+qpQXrNG4eumPSD+hHzzYi/CdaM80M9XRLCo=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "b6eaf97c6960d97350c584de1b6dcff03c9daf42", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "nixpkgs_2": { 38 | "locked": { 39 | "lastModified": 1736320768, 40 | "narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=", 41 | "owner": "NixOS", 42 | "repo": "nixpkgs", 43 | "rev": "4bc9c909d9ac828a039f288cf872d16d38185db8", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "owner": "NixOS", 48 | "ref": "nixpkgs-unstable", 49 | "repo": "nixpkgs", 50 | "type": "github" 51 | } 52 | }, 53 | "root": { 54 | "inputs": { 55 | "flake-utils": "flake-utils", 56 | "nixpkgs": "nixpkgs", 57 | "rust-overlay": "rust-overlay" 58 | } 59 | }, 60 | "rust-overlay": { 61 | "inputs": { 62 | "nixpkgs": "nixpkgs_2" 63 | }, 64 | "locked": { 65 | "lastModified": 1742351546, 66 | "narHash": "sha256-GPubFcOXyi8TVm1xpltHYPcfGr+iO+if2u/EtzFVnHQ=", 67 | "owner": "oxalica", 68 | "repo": "rust-overlay", 69 | "rev": "b0a7450168c62a46f87d204280e6d9d1c0292671", 70 | "type": "github" 71 | }, 72 | "original": { 73 | "owner": "oxalica", 74 | "repo": "rust-overlay", 75 | "type": "github" 76 | } 77 | }, 78 | "systems": { 79 | "locked": { 80 | "lastModified": 1681028828, 81 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 82 | "owner": "nix-systems", 83 | "repo": "default", 84 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 85 | "type": "github" 86 | }, 87 | "original": { 88 | "owner": "nix-systems", 89 | "repo": "default", 90 | "type": "github" 91 | } 92 | } 93 | }, 94 | "root": "root", 95 | "version": 7 96 | } 97 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Rust WASM build deps"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | rust-overlay.url = "github:oxalica/rust-overlay"; 7 | flake-utils.url = "github:numtide/flake-utils"; 8 | }; 9 | 10 | outputs = { 11 | nixpkgs, 12 | rust-overlay, 13 | flake-utils, 14 | ... 15 | }: 16 | flake-utils.lib.eachDefaultSystem ( 17 | system: let 18 | overlays = [(import rust-overlay)]; 19 | pkgs = import nixpkgs { 20 | inherit system overlays; 21 | }; 22 | in 23 | with pkgs; { 24 | devShells.default = mkShell { 25 | buildInputs = [ 26 | openssl 27 | pkg-config 28 | trunk 29 | wasm-pack 30 | (rust-bin.nightly.latest.default.override { 31 | targets = ["wasm32-unknown-unknown"]; 32 | extensions = ["rust-src"]; 33 | }) 34 | ]; 35 | }; 36 | } 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Test page with a minimal editor in it 4 | 5 | 6 | 13 | 14 | 15 | 16 | 32 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /lc3b/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lc3b" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /lc3b/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::mem::transmute; 2 | 3 | struct LC3B { 4 | /// Memory for the register file 5 | pub reg_file: [u16; 8], 6 | 7 | /// Program Counter 8 | pub pc: u16, 9 | 10 | /// Instruction register. 11 | pub ir: u16, 12 | 13 | /// Memory Address Register 14 | pub mar: u16, 15 | 16 | /// NZP flags 17 | pub n: bool, 18 | pub z: bool, 19 | pub p: bool, 20 | 21 | /// Buses 22 | pub d_bus: u16, 23 | pub a_bus: u16, 24 | pub b_bus: u16, 25 | 26 | /// Addressable memory 27 | pub memory: [u8; 65_536], 28 | } 29 | 30 | /// Notes 31 | /// - All *_word_* values are lshifted 1 bit. 32 | /// - There are no signed values in the sim, because two-complement adds together the same either 33 | /// way. 34 | /// - However! Some values are sign-extended, and some are not. The ones sign-extended are 35 | /// semantically signed. 36 | pub enum OpCode { 37 | /// 38 | BR { 39 | n: bool, 40 | z: bool, 41 | p: bool, 42 | // Sign extended 43 | pc_word_offset: u16, 44 | }, 45 | JMP { 46 | base_r: usize, 47 | }, 48 | Add(AluOperand), 49 | Sub(AluOperand), 50 | And(AluOperand), 51 | Or(AluOperand), 52 | Xor(AluOperand), 53 | LShift(u16), 54 | RShiftLog(u16), 55 | RShiftArith(u16), 56 | LDB { 57 | dr: usize, 58 | base_r: usize, 59 | // Sign extended 60 | offset: u16, 61 | }, 62 | LDW { 63 | dr: usize, 64 | base_r: usize, 65 | // Sign extended 66 | offset: u16, 67 | }, 68 | LDI { 69 | dr: usize, 70 | // Sign extended 71 | value: u16, 72 | }, 73 | LEA { 74 | dr: usize, 75 | // Sign extended 76 | pc_offset: u16, 77 | }, 78 | STB { 79 | sr: usize, 80 | base_r: usize, 81 | // Sign extended 82 | offset: u16, 83 | }, 84 | STW { 85 | sr: usize, 86 | base_r: usize, 87 | // Sign extended 88 | offset: u16, 89 | }, 90 | } 91 | 92 | impl TryFrom for OpCode { 93 | type Error = (); 94 | 95 | fn try_from(value: u16) -> Result { 96 | let opcode = (value & 0xF000) >> 12; 97 | 98 | Ok(match opcode { 99 | 0 => OpCode::BR { 100 | n: value & 0b0000_1000_0000_0000 != 0, 101 | z: value & 0b0000_0100_0000_0000 != 0, 102 | p: value & 0b0000_0010_0000_0000 != 0, 103 | pc_word_offset: sign_extend::<9>(value), 104 | }, 105 | 12 => OpCode::JMP { 106 | base_r: sr1_base_r(value), 107 | }, 108 | 1 => { 109 | // Bit 5 is Register(0), Immediate(1) 110 | let operand = if value & 0b0000_0000_0010_0000 == 0 { 111 | AluOperand::Registers { 112 | dr: dr_sr(value), 113 | sr1: sr1_base_r(value), 114 | sr2: sr2(value), 115 | } 116 | } else { 117 | AluOperand::Immediate { 118 | dr: dr_sr(value), 119 | sr1: sr1_base_r(value), 120 | immediate: sign_extend::<4>(value), 121 | } 122 | }; 123 | 124 | let bit_4 = value & 0b0000_0000_0001_0000 != 0; 125 | 126 | if !bit_4 { 127 | OpCode::Add(operand) 128 | } else { 129 | OpCode::Sub(operand) 130 | } 131 | } 132 | 5 => { 133 | // Bit 5 is Register(0), Immediate(1) 134 | let operand = if value & 0b0000_0000_0010_0000 == 0 { 135 | AluOperand::Registers { 136 | dr: dr_sr(value), 137 | sr1: sr1_base_r(value), 138 | sr2: sr2(value), 139 | } 140 | } else { 141 | AluOperand::Immediate { 142 | dr: dr_sr(value), 143 | sr1: sr1_base_r(value), 144 | immediate: value & 0xF, 145 | } 146 | }; 147 | 148 | let bit_4 = value & 0b0000_0000_0001_0000 != 0; 149 | 150 | if !bit_4 { 151 | OpCode::And(operand) 152 | } else { 153 | OpCode::Or(operand) 154 | } 155 | } 156 | 9 => { 157 | // Bit 5 is Register(0), Immediate(1) 158 | let operand = if value & 0b0000_0000_0010_0000 == 0 { 159 | AluOperand::Registers { 160 | dr: dr_sr(value), 161 | sr1: sr1_base_r(value), 162 | sr2: sr2(value), 163 | } 164 | } else { 165 | AluOperand::Immediate { 166 | dr: dr_sr(value), 167 | sr1: sr1_base_r(value), 168 | immediate: value & 0xF, 169 | } 170 | }; 171 | 172 | let bit_4 = value & 0b0000_0000_0001_0000 != 0; 173 | 174 | if !bit_4 { 175 | OpCode::Xor(operand) 176 | } else { 177 | return Err(()); 178 | } 179 | } 180 | 13 => { 181 | let bit_4 = value & 0b0000_0000_0001_0000 != 0; 182 | let bit_5 = value & 0b0000_0000_0010_0000 != 0; 183 | let amount = value & 0b0000_0000_0000_1111; 184 | 185 | match (bit_5, bit_4) { 186 | (false, false) => OpCode::LShift(amount), 187 | (false, true) => OpCode::RShiftLog(amount), 188 | (true, true) => OpCode::RShiftArith(amount), 189 | _ => return Err(()), 190 | } 191 | } 192 | 2 => OpCode::LDB { 193 | dr: dr_sr(value), 194 | base_r: sr1_base_r(value), 195 | offset: unsafe { transmute(sign_extend::<6>(value)) }, 196 | }, 197 | 6 => OpCode::LDW { 198 | dr: dr_sr(value), 199 | base_r: sr1_base_r(value), 200 | offset: unsafe { transmute(sign_extend::<7>(value << 1)) }, 201 | }, 202 | 10 => OpCode::LDI { 203 | dr: dr_sr(value), 204 | value: unsafe { transmute(sign_extend::<9>(value)) }, 205 | }, 206 | 14 => OpCode::LEA { 207 | dr: dr_sr(value), 208 | pc_offset: unsafe { transmute(sign_extend::<10>(value << 1)) }, 209 | }, 210 | 3 => OpCode::STB { 211 | sr: dr_sr(value), 212 | base_r: sr1_base_r(value), 213 | offset: unsafe { transmute(sign_extend::<6>(value)) }, 214 | }, 215 | 7 => OpCode::STW { 216 | sr: dr_sr(value), 217 | base_r: sr1_base_r(value), 218 | offset: unsafe { transmute(sign_extend::<7>(value << 1)) }, 219 | }, 220 | _ => return Err(()), 221 | }) 222 | } 223 | } 224 | 225 | pub enum AluOperand { 226 | Registers { 227 | dr: usize, 228 | sr1: usize, 229 | sr2: usize, 230 | }, 231 | Immediate { 232 | dr: usize, 233 | sr1: usize, 234 | immediate: u16, 235 | }, 236 | } 237 | 238 | impl LC3B { 239 | // Executes the next instruction 240 | pub fn step(&mut self) -> Result<(), ()> { 241 | // State 16 242 | self.mar = self.pc; 243 | 244 | // State 18 245 | self.pc = self.pc.wrapping_add(2); 246 | 247 | // State 19 248 | let mar = self.mar as usize; 249 | self.ir = ((self.memory[mar] as u16) << 8) | self.memory[mar + 1] as u16; 250 | 251 | // State 20 252 | match OpCode::try_from(self.ir)? { 253 | // State 0 254 | OpCode::BR { 255 | n, 256 | z, 257 | p, 258 | pc_word_offset, 259 | } => { 260 | if (n && self.n) || (z && self.z) || (p && self.p) { 261 | // Branch followed: pc = pc + pc_word_offset 262 | self.d_bus = self.pc.wrapping_add(pc_word_offset); 263 | self.pc = self.d_bus; 264 | } else { 265 | // Branch not followed: pc = pc + 2 266 | self.d_bus = self.pc.wrapping_add(2); 267 | self.pc = self.d_bus; 268 | } 269 | } 270 | // State 12 271 | OpCode::JMP { base_r } => { 272 | self.d_bus = self.reg_file[base_r]; 273 | self.pc = self.d_bus; 274 | } 275 | // State 1 276 | OpCode::Add(alu_operand) => { 277 | match alu_operand { 278 | AluOperand::Registers { dr, sr1, sr2 } => { 279 | self.d_bus = self.reg_file[sr1].wrapping_add(self.reg_file[sr2]); 280 | self.reg_file[dr] = self.d_bus 281 | } 282 | AluOperand::Immediate { dr, sr1, immediate } => { 283 | self.d_bus = self.reg_file[sr1].wrapping_add(immediate); 284 | self.reg_file[dr] = self.d_bus 285 | } 286 | } 287 | 288 | self.set_cc(); 289 | } 290 | // State 1 291 | OpCode::Sub(alu_operand) => { 292 | match alu_operand { 293 | AluOperand::Registers { dr, sr1, sr2 } => { 294 | self.d_bus = self.reg_file[sr1].wrapping_sub(self.reg_file[sr2]); 295 | self.reg_file[dr] = self.d_bus; 296 | } 297 | AluOperand::Immediate { dr, sr1, immediate } => { 298 | self.d_bus = self.reg_file[sr1].wrapping_sub(immediate); 299 | self.reg_file[dr] = self.d_bus; 300 | } 301 | } 302 | 303 | self.set_cc(); 304 | } 305 | // State 5 306 | OpCode::And(alu_operand) => { 307 | match alu_operand { 308 | AluOperand::Registers { dr, sr1, sr2 } => { 309 | self.d_bus = self.reg_file[sr1] & self.reg_file[sr2]; 310 | self.reg_file[dr] = self.d_bus; 311 | } 312 | AluOperand::Immediate { dr, sr1, immediate } => { 313 | self.d_bus = self.reg_file[sr1] & immediate; 314 | self.reg_file[dr] = self.d_bus; 315 | } 316 | } 317 | 318 | self.set_cc(); 319 | } 320 | // State 5 321 | OpCode::Or(alu_operand) => { 322 | match alu_operand { 323 | AluOperand::Registers { dr, sr1, sr2 } => { 324 | self.d_bus = self.reg_file[sr1] | self.reg_file[sr2]; 325 | self.reg_file[dr] = self.d_bus; 326 | } 327 | AluOperand::Immediate { dr, sr1, immediate } => { 328 | self.d_bus = self.reg_file[sr1] | immediate; 329 | self.reg_file[dr] = self.d_bus; 330 | } 331 | } 332 | 333 | self.set_cc(); 334 | } 335 | // State 9 336 | OpCode::Xor(alu_operand) => { 337 | match alu_operand { 338 | AluOperand::Registers { dr, sr1, sr2 } => { 339 | self.d_bus = self.reg_file[sr1] ^ self.reg_file[sr2]; 340 | self.reg_file[dr] = self.d_bus; 341 | } 342 | AluOperand::Immediate { dr, sr1, immediate } => { 343 | self.d_bus = self.reg_file[sr1] ^ immediate; 344 | self.reg_file[dr] = self.d_bus; 345 | } 346 | } 347 | 348 | self.set_cc(); 349 | } 350 | OpCode::LShift(_) => todo!(), 351 | OpCode::RShiftLog(_) => todo!(), 352 | OpCode::RShiftArith(_) => todo!(), 353 | // State 2 354 | OpCode::LDB { dr, base_r, offset } => { 355 | // State 2 356 | self.mar = self.reg_file[base_r].wrapping_add(offset); 357 | let mar = self.mar as usize; 358 | // State 21 359 | self.d_bus = self.memory[mar] as u16; 360 | self.reg_file[dr] = self.d_bus; 361 | self.set_cc(); 362 | } 363 | OpCode::LDW { dr, base_r, offset } => { 364 | // State 6 365 | self.mar = self.reg_file[base_r].wrapping_add(offset); 366 | let mar = self.mar as usize; 367 | // State 22 368 | self.d_bus = ((self.memory[mar] as u16) << 8) | (self.memory[mar + 1] as u16); 369 | self.reg_file[dr] = self.d_bus; 370 | self.set_cc(); 371 | } 372 | // State 10 373 | OpCode::LDI { dr, value } => { 374 | self.d_bus = value; 375 | self.reg_file[dr] = self.d_bus; 376 | self.set_cc(); 377 | } 378 | // State 14 379 | OpCode::LEA { dr, pc_offset } => { 380 | self.d_bus = self.pc.wrapping_add(pc_offset); 381 | self.reg_file[dr] = self.d_bus; 382 | self.set_cc(); 383 | } 384 | // State 3 385 | OpCode::STB { sr, base_r, offset } => { 386 | // State 3 387 | self.mar = self.reg_file[base_r].wrapping_add(offset); 388 | let mar = self.mar as usize; 389 | // State 23 390 | self.memory[mar] = (self.reg_file[sr] & 0xFF) as u8; 391 | } 392 | // State 7 393 | OpCode::STW { sr, base_r, offset } => { 394 | // State 7 395 | self.mar = self.reg_file[base_r].wrapping_add(offset); 396 | let mar = self.mar as usize; 397 | // State 24 398 | self.memory[mar] = (self.reg_file[sr] >> 8) as u8; 399 | self.memory[mar + 1] = (self.reg_file[sr] & 0xFF) as u8; 400 | } 401 | } 402 | 403 | Ok(()) 404 | } 405 | 406 | #[inline(always)] 407 | fn set_cc(&mut self) { 408 | self.n = false; 409 | self.z = false; 410 | self.p = false; 411 | 412 | let signed: i16 = unsafe { transmute(self.d_bus) }; 413 | 414 | if signed > 0 { 415 | self.p = true; 416 | } else if signed < 0 { 417 | self.n = true; 418 | } else { 419 | self.z = true; 420 | } 421 | } 422 | } 423 | 424 | fn main() { 425 | // Program needs to start at 0x2 426 | 427 | println!("Hello, world!"); 428 | } 429 | 430 | #[inline(always)] 431 | fn dr_sr(value: u16) -> usize { 432 | ((value & 0b0000_1110_0000_0000) >> 9) as usize 433 | } 434 | 435 | #[inline(always)] 436 | fn sr1_base_r(value: u16) -> usize { 437 | ((value & 0b0000_0001_1100_0000) >> 6) as usize 438 | } 439 | 440 | #[inline(always)] 441 | fn sr2(value: u16) -> usize { 442 | (value & 0b0000_0000_0000_0111) as usize 443 | } 444 | 445 | #[inline(always)] 446 | fn sign_extend(value: u16) -> u16 { 447 | // Ensure B is in a valid range (1..=16) at compile time 448 | debug_assert!(B >= 1 && B <= 16); 449 | 450 | let lower_bit_mask = (1 << B) - 1; 451 | let value = value & lower_bit_mask; 452 | 453 | // Extract the B-th bit (sign bit of the B-bit number) 454 | let sign_bit = (value >> (B - 1)) & 1; 455 | 456 | // If the sign bit is set, create a mask for the upper bits 457 | if sign_bit != 0 { 458 | let mask = !0u16 << B; 459 | value | mask 460 | } else { 461 | value 462 | } 463 | } 464 | 465 | #[cfg(test)] 466 | mod tests { 467 | use super::*; 468 | 469 | #[test] 470 | fn test_sign_extend_9bit() { 471 | // Test positive 9-bit number 472 | assert_eq!(sign_extend::<9>(0b0_1010_1010), 0b0_1010_1010); 473 | 474 | // Test negative 9-bit number 475 | let input = 0b1_1010_1010; // 9-bit: -86 476 | assert_eq!(sign_extend::<9>(input), unsafe { transmute(-86_i16) }); 477 | 478 | // Verify it keeps higher bits clear 479 | assert_eq!(sign_extend::<9>(0b1111_1010_1010), unsafe { 480 | transmute(-86_i16) 481 | }); 482 | } 483 | 484 | #[test] 485 | fn test_sign_extend_6bit() { 486 | // Test positive 6-bit number 487 | assert_eq!(sign_extend::<6>(0b01_1010), 0b01_1010); 488 | 489 | // Test negative 6-bit number 490 | let input = 0b11_1010; // 6-bit: -6 491 | assert_eq!(sign_extend::<6>(input), unsafe { transmute(-6_i16) }); 492 | 493 | // Test with junk bits 494 | assert_eq!(sign_extend::<6>(0b1010_1010), unsafe { transmute(-22_i16) }); 495 | assert_eq!(sign_extend::<6>(0b1000_1010), 0b0000_0000_0000_1010); 496 | } 497 | } 498 | -------------------------------------------------------------------------------- /misc/screenshots/logic-paint-cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AThilenius/logic-paint-rs/61a91b3c830362cd4359c0eacf9bda0dc26e10e9/misc/screenshots/logic-paint-cpu.png -------------------------------------------------------------------------------- /misc/screenshots/logic-paint-register-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AThilenius/logic-paint-rs/61a91b3c830362cd4359c0eacf9bda0dc26e10e9/misc/screenshots/logic-paint-register-file.png -------------------------------------------------------------------------------- /misc/screenshots/logic-paint-scale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AThilenius/logic-paint-rs/61a91b3c830362cd4359c0eacf9bda0dc26e10e9/misc/screenshots/logic-paint-scale.png -------------------------------------------------------------------------------- /src/coords.rs: -------------------------------------------------------------------------------- 1 | use glam::{IVec2, UVec2}; 2 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 3 | use wasm_bindgen::prelude::*; 4 | 5 | use crate::upc::LOG_UPC_BYTE_LEN; 6 | 7 | pub(crate) const LOG_CHUNK_SIZE: usize = 7; 8 | pub(crate) const CHUNK_SIZE: usize = 1 << LOG_CHUNK_SIZE; 9 | pub(crate) const CHUNK_CELL_COUNT: usize = CHUNK_SIZE * CHUNK_SIZE; 10 | const UPPER_MASK: i32 = !((CHUNK_SIZE as i32) - 1); 11 | const LOWER_MASK: usize = CHUNK_SIZE - 1; 12 | 13 | #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] 14 | #[wasm_bindgen] 15 | pub struct CellCoord(pub IVec2); 16 | 17 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 18 | pub struct ChunkCoord(pub IVec2); 19 | 20 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] 21 | pub struct LocalCoord(pub UVec2); 22 | 23 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] 24 | pub struct CellCoordOffset(pub IVec2); 25 | 26 | #[derive(Serialize, Deserialize)] 27 | pub enum Coord { 28 | Cell(IVec2), 29 | Chunk(IVec2), 30 | Local(UVec2), 31 | } 32 | 33 | impl Serialize for CellCoord { 34 | fn serialize(&self, serializer: S) -> Result 35 | where 36 | S: Serializer, 37 | { 38 | serializer.serialize_str(&format!("{}:{}", self.0.x, self.0.y)) 39 | } 40 | } 41 | 42 | impl<'de> Deserialize<'de> for CellCoord { 43 | fn deserialize(deserializer: D) -> Result 44 | where 45 | D: Deserializer<'de>, 46 | { 47 | let str = String::deserialize(deserializer)?; 48 | let splits: Vec<_> = str.split(":").collect(); 49 | 50 | if splits.len() != 2 { 51 | return Err(serde::de::Error::custom( 52 | "Invalid CellCoord format, expected 123:123", 53 | )); 54 | } 55 | 56 | let x = splits[0].parse::().map_err(serde::de::Error::custom)?; 57 | let y = splits[1].parse::().map_err(serde::de::Error::custom)?; 58 | 59 | Ok(Self(IVec2::new(x, y))) 60 | } 61 | } 62 | 63 | impl Serialize for ChunkCoord { 64 | fn serialize(&self, serializer: S) -> Result 65 | where 66 | S: Serializer, 67 | { 68 | serializer.serialize_str(&format!("{}:{}", self.0.x, self.0.y)) 69 | } 70 | } 71 | 72 | impl<'de> Deserialize<'de> for ChunkCoord { 73 | fn deserialize(deserializer: D) -> Result 74 | where 75 | D: Deserializer<'de>, 76 | { 77 | let str = String::deserialize(deserializer)?; 78 | let splits: Vec<_> = str.split(":").collect(); 79 | 80 | if splits.len() != 2 { 81 | return Err(serde::de::Error::custom( 82 | "Invalid CellCoord format, expected 123:123", 83 | )); 84 | } 85 | 86 | let x = splits[0].parse::().map_err(serde::de::Error::custom)?; 87 | let y = splits[1].parse::().map_err(serde::de::Error::custom)?; 88 | 89 | Ok(Self(IVec2::new(x, y))) 90 | } 91 | } 92 | 93 | impl From for u64 { 94 | fn from(cell_coord: CellCoord) -> Self { 95 | (cell_coord.0.y as u64) << 32 | (cell_coord.0.x as u64) 96 | } 97 | } 98 | 99 | impl From<(i32, i32)> for CellCoord { 100 | fn from(v: (i32, i32)) -> Self { 101 | Self(IVec2::new(v.0, v.1)) 102 | } 103 | } 104 | 105 | impl From<(i32, i32)> for CellCoordOffset { 106 | fn from(v: (i32, i32)) -> Self { 107 | Self(IVec2::new(v.0, v.1)) 108 | } 109 | } 110 | 111 | impl From for ChunkCoord { 112 | #[inline(always)] 113 | fn from(c: CellCoord) -> Self { 114 | Self(IVec2::new(c.0.x >> LOG_CHUNK_SIZE, c.0.y >> LOG_CHUNK_SIZE)) 115 | } 116 | } 117 | 118 | impl From<&CellCoord> for ChunkCoord { 119 | #[inline(always)] 120 | fn from(c: &CellCoord) -> Self { 121 | Self(IVec2::new(c.0.x >> LOG_CHUNK_SIZE, c.0.y >> LOG_CHUNK_SIZE)) 122 | } 123 | } 124 | 125 | impl From for LocalCoord { 126 | #[inline(always)] 127 | fn from(c: CellCoord) -> Self { 128 | Self(UVec2::new( 129 | (c.0.x - (c.0.x & UPPER_MASK)) as u32, 130 | (c.0.y - (c.0.y & UPPER_MASK)) as u32, 131 | )) 132 | } 133 | } 134 | 135 | impl From<&CellCoord> for LocalCoord { 136 | #[inline(always)] 137 | fn from(c: &CellCoord) -> Self { 138 | Self(UVec2::new( 139 | (c.0.x - (c.0.x & UPPER_MASK)) as u32, 140 | (c.0.y - (c.0.y & UPPER_MASK)) as u32, 141 | )) 142 | } 143 | } 144 | 145 | impl CellCoord { 146 | #[inline(always)] 147 | pub fn from_offset_into_chunk(chunk_coord: &ChunkCoord, x: usize, y: usize) -> Self { 148 | CellCoord(IVec2::new( 149 | (chunk_coord.0.x << LOG_CHUNK_SIZE) + x as i32, 150 | (chunk_coord.0.y << LOG_CHUNK_SIZE) + y as i32, 151 | )) 152 | } 153 | } 154 | 155 | #[wasm_bindgen] 156 | impl CellCoord { 157 | #[wasm_bindgen(constructor)] 158 | pub fn _wasm_ctor(x: i32, y: i32) -> Self { 159 | (x, y).into() 160 | } 161 | } 162 | 163 | #[allow(dead_code)] 164 | impl ChunkCoord { 165 | pub fn first_cell_coord(&self) -> CellCoord { 166 | CellCoord(IVec2::new( 167 | self.0.x << LOG_CHUNK_SIZE, 168 | self.0.y << LOG_CHUNK_SIZE, 169 | )) 170 | } 171 | 172 | pub fn last_cell_coord(&self) -> CellCoord { 173 | CellCoord(IVec2::new( 174 | (self.0.x << LOG_CHUNK_SIZE) + CHUNK_SIZE as i32 - 1, 175 | (self.0.y << LOG_CHUNK_SIZE) + CHUNK_SIZE as i32 - 1, 176 | )) 177 | } 178 | } 179 | 180 | #[allow(dead_code)] 181 | impl LocalCoord { 182 | #[inline(always)] 183 | pub fn to_cell_coord(&self, chunk_coord: &ChunkCoord) -> CellCoord { 184 | CellCoord(self.0.as_ivec2() + chunk_coord.first_cell_coord().0) 185 | } 186 | 187 | #[inline(always)] 188 | pub fn from_upc_idx(mut idx: usize) -> LocalCoord { 189 | idx = idx >> LOG_UPC_BYTE_LEN; 190 | let y = idx >> LOG_CHUNK_SIZE; 191 | let x = idx & LOWER_MASK; 192 | 193 | LocalCoord(UVec2::new(x as u32, y as u32)) 194 | } 195 | 196 | #[inline(always)] 197 | pub fn to_upc_idx(&self) -> usize { 198 | (((self.0.y as usize) << LOG_CHUNK_SIZE) | self.0.x as usize) << LOG_UPC_BYTE_LEN 199 | } 200 | } 201 | 202 | impl CellCoordOffset { 203 | pub fn to_cell_coord(&self, anchor: CellCoord) -> CellCoord { 204 | CellCoord(anchor.0 + self.0) 205 | } 206 | } 207 | 208 | impl From for u64 { 209 | fn from(offset: CellCoordOffset) -> Self { 210 | (offset.0.y as u64) << 32 | (offset.0.x as u64) 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use wasm_bindgen::prelude::*; 4 | use wasm_thread as thread; 5 | 6 | mod coords; 7 | mod module; 8 | mod project; 9 | mod substrate; 10 | mod tools; 11 | mod upc; 12 | mod utils; 13 | mod viewport; 14 | mod wgl2; 15 | 16 | #[wasm_bindgen(start)] 17 | pub fn main() { 18 | #[cfg(feature = "console_error_panic_hook")] 19 | console_error_panic_hook::set_once(); 20 | } 21 | 22 | #[wasm_bindgen] 23 | pub fn run_thread_test() { 24 | for _ in 0..2 { 25 | thread::spawn(|| { 26 | for i in 1..3 { 27 | log!( 28 | "hi number {} from the spawned thread {:?}!", 29 | i, 30 | thread::current().id() 31 | ); 32 | thread::sleep(Duration::from_millis(1)); 33 | } 34 | }); 35 | } 36 | 37 | for i in 1..3 { 38 | log!( 39 | "hi number {} from the main thread {:?}!", 40 | i, 41 | thread::current().id() 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/module.rs: -------------------------------------------------------------------------------- 1 | use crate::substrate::execution_context::SimState; 2 | 3 | pub type ModuleGpioHandle = usize; 4 | 5 | /// Connects a module GPIO to a substrate socket. 6 | pub struct ModuleGpio { 7 | /// The handle used to communicate with this GPIO, generally just the index of the GPIO in the 8 | /// Vec storing the module's GPIOs. 9 | pub handle: ModuleGpioHandle, 10 | 11 | /// The human-readable name for this GPIO 12 | pub name: String, 13 | 14 | /// The 'bond wire' that connects this GPIO to a substrate Socket. 15 | pub bonding: Option, 16 | 17 | /// When set to true, the module will be notified (at the completion of the tick) when the 18 | /// `si_output_high` transitions from false to true. There is no way to trigger on a falling 19 | /// edge, just use an inverter infront of the socket. 20 | pub trigger: bool, 21 | 22 | // Set to true when the substrate is driving the pin high. 23 | pub si_output_high: bool, 24 | 25 | // Set to true by the module, to drive the substrate socket to high. 26 | pub si_input_high: bool, 27 | } 28 | 29 | pub trait Module { 30 | /// Provides the human-readable name for this module. 31 | fn get_name(&self) -> &str; 32 | 33 | /// Resets the module back to it's pre-execution state. 34 | fn reset(&mut self); 35 | 36 | /// Provides a list of GPIOs, each with a unique handle that can be referenced by the module. 37 | fn get_gpios_mut(&mut self) -> &mut Vec; 38 | 39 | /// Called when one or more GPIOs have been triggered. By the time this is called, the 40 | /// ModuleGpio instance has already been updated. 41 | fn trigger(&mut self, sim_state: &SimState, handles: Vec) { 42 | let _ = handles; 43 | let _ = sim_state; 44 | } 45 | 46 | /// Called each clock cycle. 47 | fn clock(&mut self, sim_state: &SimState) { 48 | let _ = sim_state; 49 | } 50 | } 51 | 52 | // ==== Clock module ============================================================================== 53 | pub struct ModuleClock { 54 | gpios: Vec, 55 | } 56 | 57 | impl Default for ModuleClock { 58 | fn default() -> Self { 59 | Self { 60 | gpios: vec![ModuleGpio { 61 | handle: 0, 62 | name: "CLK".to_string(), 63 | bonding: None, 64 | trigger: false, 65 | si_output_high: false, 66 | si_input_high: false, 67 | }], 68 | } 69 | } 70 | } 71 | 72 | impl Module for ModuleClock { 73 | fn get_name(&self) -> &str { 74 | "Clock" 75 | } 76 | 77 | fn reset(&mut self) { 78 | self.gpios[0].si_input_high = false; 79 | } 80 | 81 | #[inline(always)] 82 | fn get_gpios_mut(&mut self) -> &mut Vec { 83 | &mut self.gpios 84 | } 85 | 86 | fn clock(&mut self, sim_state: &SimState) { 87 | self.gpios[0].si_input_high = sim_state.ticks % 2 == 0; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/project.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | coords::CellCoord, 3 | module::Module, 4 | substrate::{buffer::Buffer, execution_context::ExecutionContext, io::IoState, mask::Mask}, 5 | tools::{ 6 | camera_controller::ToolCameraController, draw_metal::ToolPaintMetal, draw_si::ToolPaintSi, 7 | place_socket::ToolPlaceSocket, visual::ToolVisual, Tool, ToolInput, ToolOutput, 8 | }, 9 | utils::Selection, 10 | wgl2::Camera, 11 | }; 12 | use wasm_bindgen::prelude::*; 13 | 14 | /// Contains all the underlying state of a project, including the active buffer, mask, modules, 15 | /// history and tools. 16 | #[wasm_bindgen(getter_with_clone)] 17 | pub struct Project { 18 | /// The active buffer that dispatched input will be rendered to (like drawing). 19 | /// This is used as the base for rendering (with mouse-follow stacked on top of it). 20 | pub buffer: Buffer, 21 | 22 | /// The current render mask applied to the buffer. 23 | pub mask: Mask, 24 | 25 | /// The selected (visual mode) cells 26 | pub selection: Selection, 27 | 28 | /// The last used cursor location 29 | pub cursor_coord: Option, 30 | 31 | /// The CSS style that should be applied to the cursor. 32 | pub cursor_style: String, 33 | 34 | /// Modules 35 | modules: Vec>, 36 | 37 | /// All loaded tools 38 | tools: Vec>, 39 | 40 | /// The active tool 41 | active_tool: usize, 42 | 43 | /// When set, buffer must be static. 44 | execution_context: Option, 45 | } 46 | 47 | #[wasm_bindgen(getter_with_clone)] 48 | pub struct EditorDispatchResult { 49 | pub buffer_persist: Option, 50 | pub tools_persist: Vec, 51 | pub camera: Option, 52 | } 53 | 54 | #[wasm_bindgen(getter_with_clone)] 55 | #[derive(Clone)] 56 | pub struct ToolPersist { 57 | pub tool_name: String, 58 | pub serialized_state: Vec, 59 | } 60 | 61 | #[wasm_bindgen] 62 | impl Project { 63 | #[wasm_bindgen(constructor)] 64 | pub fn new(buffer: Buffer) -> Self { 65 | let mut tools = vec![]; 66 | 67 | // Create and activate visual as the default tool 68 | let mut visual = ToolVisual::default(); 69 | let tool_output = visual.activate(buffer.clone()); 70 | 71 | // Store tools 72 | tools.push(Box::new(visual) as Box); 73 | tools.push(Box::new(ToolPaintSi::default()) as Box); 74 | tools.push(Box::new(ToolPaintMetal::default()) as Box); 75 | tools.push(Box::new(ToolPlaceSocket::default()) as Box); 76 | tools.push(Box::new(ToolCameraController::default()) as Box); 77 | 78 | Self { 79 | buffer, 80 | mask: Default::default(), 81 | selection: Default::default(), 82 | cursor_coord: None, 83 | cursor_style: tool_output 84 | .cursor_style 85 | .unwrap_or_else(|| "default".to_string()), 86 | modules: vec![], 87 | tools, 88 | active_tool: 0, 89 | execution_context: None, 90 | } 91 | } 92 | 93 | pub fn dispatch_event(&mut self, io_state: &IoState, camera: &Camera) -> EditorDispatchResult { 94 | self.cursor_coord = Some(io_state.cell); 95 | 96 | let mut new_active = None; 97 | let mut dispatch_result = EditorDispatchResult { 98 | buffer_persist: None, 99 | tools_persist: vec![], 100 | camera: None, 101 | }; 102 | 103 | let mut tool_input = ToolInput { 104 | active: false, 105 | io_state: io_state.clone(), 106 | camera: camera.clone(), 107 | buffer: self.buffer.clone(), 108 | selection: self.selection, 109 | }; 110 | 111 | let a = self.active_tool; 112 | let owned_output: Vec<_> = self 113 | .tools 114 | .iter_mut() 115 | .enumerate() 116 | .map(|(idx, tool)| { 117 | tool_input.active = idx == a; 118 | let output = tool.dispatch_event(&tool_input); 119 | (idx, output) 120 | }) 121 | .collect(); 122 | 123 | for (idx, output) in owned_output { 124 | if output.take_active && new_active.is_none() && idx != self.active_tool { 125 | new_active = Some(idx); 126 | } 127 | 128 | self.handle_dispatch_result(&mut dispatch_result, idx, output); 129 | } 130 | 131 | if let Some(new_active) = new_active { 132 | tool_input.active = false; 133 | let output = self.tools[self.active_tool].deactivate(self.buffer.clone()); 134 | 135 | self.selection = Default::default(); 136 | 137 | self.handle_dispatch_result(&mut dispatch_result, self.active_tool, output); 138 | 139 | tool_input.active = true; 140 | self.active_tool = new_active; 141 | let output = self.tools[self.active_tool].activate(self.buffer.clone()); 142 | 143 | self.handle_dispatch_result(&mut dispatch_result, self.active_tool, output); 144 | } 145 | 146 | dispatch_result 147 | } 148 | 149 | fn handle_dispatch_result( 150 | &mut self, 151 | dispatch_result: &mut EditorDispatchResult, 152 | idx: usize, 153 | output: ToolOutput, 154 | ) { 155 | if let Some(buffer) = output.buffer { 156 | self.buffer = buffer; 157 | } 158 | 159 | if let Some(mask) = output.mask { 160 | self.mask = mask; 161 | } 162 | 163 | if let Some(cursor_style) = output.cursor_style { 164 | self.cursor_style = cursor_style; 165 | } 166 | 167 | if let Some(selection) = output.selection { 168 | self.selection = selection; 169 | } 170 | 171 | if let Some(camera) = output.camera { 172 | dispatch_result.camera = Some(camera); 173 | } 174 | 175 | if output.checkpoint { 176 | dispatch_result.buffer_persist = Some(self.buffer.clone()); 177 | } 178 | 179 | if let Some(bytes) = output.persist_tool_state { 180 | dispatch_result.tools_persist.push(ToolPersist { 181 | tool_name: self.tools[idx].get_name().to_string(), 182 | serialized_state: bytes, 183 | }); 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/shaders/cell.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | precision highp float; 3 | precision highp int; 4 | precision highp usampler2D; 5 | 6 | in vec2 v_uv; 7 | 8 | uniform float time; 9 | uniform usampler2D cells_texture_sampler; 10 | uniform usampler2D mask_texture_sampler; 11 | 12 | // Coord computation 13 | uniform ivec2 chunk_start_cell_offset; 14 | 15 | // Style uniforms 16 | const vec4 n_color = vec4(0.98, 0.0, 0.77, 1.0); 17 | const vec4 p_color = vec4(0.0, 0.87, 1.0, 1.0); 18 | const vec3 metal_color = vec3(0.2, 0.2, 0.2); 19 | const vec3 active_color = vec3(1.0, 1.0, 1.0); 20 | const vec3 active_blend_strength_si_gate_metal = vec3(0.8, 0.8, 0.5); 21 | const vec3 grid_color = vec3(1.0, 1.0, 1.0); 22 | const vec3 background_color = vec3(0.0, 0.0, 0.0); 23 | const vec3 socket_color = vec3(1.0, 0.0, 0.0); 24 | const float grid_blend_strength = 0.065; 25 | const float metal_over_si_blend = 0.75; 26 | 27 | // Selection 28 | const vec3 cell_select_color = vec3(0.32, 0.6, 0.8); 29 | uniform ivec2 cell_select_ll; 30 | uniform ivec2 cell_select_ur; 31 | 32 | // Cursor-follow 33 | const vec3 cursor_follow_color = vec3(0.0, 0.0, 0.0); 34 | uniform ivec2 cursor_coord; 35 | 36 | out vec4 out_color; 37 | 38 | bool connection( 39 | float l, 40 | vec2 texel_uv, 41 | bool up, 42 | bool right, 43 | bool down, 44 | bool left 45 | ) { 46 | // Configure 47 | float h = 1.0 - l; 48 | 49 | float x = texel_uv.x; 50 | float y = texel_uv.y; 51 | 52 | return false 53 | || (y < h && y > l && x > l && x < h) 54 | || (y > h && x > l && x < h && up) 55 | || (y < l && x > l && x < h && down) 56 | || (x > h && y > l && y < h && right) 57 | || (x < l && y > l && y < h && left); 58 | } 59 | 60 | bool connection_gate( 61 | vec2 texel_uv, 62 | bool horizontal, 63 | bool vertical, 64 | bool up_left, 65 | bool down_right 66 | ) { 67 | // Configure 68 | float l = 0.2; 69 | float h = 1.0 - l; 70 | 71 | // Configure 72 | float gl = 0.3; 73 | float gh = 1.0 - gl; 74 | 75 | float x = texel_uv.x; 76 | float y = texel_uv.y; 77 | 78 | return false 79 | || (vertical && (y < h && y > l && x > gl && x < gh)) 80 | || (horizontal && (y < gh && y > gl && x > l && x < h)) 81 | 82 | // Draw the side attachments the same as `connection`. 83 | || (y > h && x > l && x < h && up_left && vertical) 84 | || (y < l && x > l && x < h && down_right && vertical) 85 | || (x > h && y > l && y < h && down_right && horizontal) 86 | || (x < l && y > l && y < h && up_left && horizontal); 87 | } 88 | 89 | void main() { 90 | vec2 float_local_coord = v_uv; 91 | uvec2 local_coord = uvec2(floor(float_local_coord)); 92 | ivec2 cell_coord = chunk_start_cell_offset + ivec2(local_coord); 93 | vec2 tile_uv = fract(float_local_coord); 94 | 95 | uvec4 cells = texelFetch(cells_texture_sampler, ivec2(local_coord), 0); 96 | uvec4 mask = texelFetch(mask_texture_sampler, ivec2(local_coord), 0); 97 | 98 | // Mirrors the format in upc.rs 99 | bool si_n = (cells.r & (1u << 7u)) > 0u; 100 | bool si_p = (cells.r & (1u << 6u)) > 0u; 101 | bool mosfet_horizontal = (cells.r & (1u << 5u)) > 0u; 102 | bool mosfet_vertical = (cells.r & (1u << 4u)) > 0u; 103 | bool si_dir_up = (cells.r & (1u << 3u)) > 0u; 104 | bool si_dir_right = (cells.r & (1u << 2u)) > 0u; 105 | bool si_dir_down = (cells.r & (1u << 1u)) > 0u; 106 | bool si_dir_left = (cells.r & (1u << 0u)) > 0u; 107 | 108 | bool metal = (cells.g & (1u << 7u)) > 3u; 109 | bool metal_dir_up = (cells.g & (1u << 6u)) > 0u; 110 | bool metal_dir_right = (cells.g & (1u << 5u)) > 0u; 111 | bool metal_dir_down = (cells.g & (1u << 4u)) > 0u; 112 | bool metal_dir_left = (cells.g & (1u << 3u)) > 0u; 113 | bool via = (cells.g & (1u << 2u)) > 0u; 114 | bool socket = (cells.g & (1u << 1u)) > 0u; 115 | 116 | // Derrived values 117 | bool is_mosfet = mosfet_horizontal || mosfet_vertical; 118 | 119 | bool metal_active = (mask.r & (1u << 0u)) > 0u; 120 | bool gate_active = (mask.g & (1u << 0u)) > 0u; 121 | bool si_ul_active = (mask.b & (1u << 0u)) > 0u || (!is_mosfet && gate_active); 122 | bool si_dr_active = (mask.a & (1u << 0u)) > 0u || (!is_mosfet && gate_active); 123 | 124 | bool cell_selected = 125 | cell_coord.x >= cell_select_ll.x && 126 | cell_coord.y >= cell_select_ll.y && 127 | cell_coord.x < cell_select_ur.x && 128 | cell_coord.y < cell_select_ur.y; 129 | 130 | bool cursor = 131 | cursor_coord.x == cell_coord.x || 132 | cursor_coord.y == cell_coord.y; 133 | 134 | bool metal_connection = connection( 135 | 0.35, 136 | tile_uv, 137 | metal_dir_up, 138 | metal_dir_right, 139 | metal_dir_down, 140 | metal_dir_left 141 | ); 142 | 143 | bool si_connection = connection( 144 | 0.2, 145 | tile_uv, 146 | si_dir_up, 147 | si_dir_right, 148 | si_dir_down, 149 | si_dir_left 150 | ); 151 | 152 | bool gate_connection = connection_gate( 153 | tile_uv, 154 | mosfet_horizontal, 155 | mosfet_vertical, 156 | // Up-left 157 | (mosfet_vertical && si_dir_up) || (mosfet_horizontal && si_dir_left), 158 | // Down-right 159 | (mosfet_vertical && si_dir_down) || (mosfet_horizontal && si_dir_right) 160 | ); 161 | 162 | vec2 stripe_uv = tile_uv * 2.0; 163 | float stripe_blend = smoothstep( 164 | 0.5, 165 | 0.6, 166 | mod(stripe_uv.x + stripe_uv.y + time, 2.0) * 0.5 167 | ); 168 | 169 | bool grid_1 = (((local_coord.x % 2u) + (local_coord.y % 2u)) % 2u) == 0u; 170 | bool grid_8 = ((((local_coord.x >> 3) % 2u) + ((local_coord.y >> 3) % 2u)) % 2u) == 0u; 171 | float grid_blend = 172 | (grid_8 ? grid_blend_strength * 0.6 : 0.0) 173 | + (grid_1 ? grid_blend_strength : 0.0); 174 | 175 | vec3 si_color = si_n ? n_color.rgb : p_color.rgb; 176 | bool si_active = mosfet_vertical 177 | ? (tile_uv.x < 0.5 ? si_ul_active : si_dr_active) 178 | : (tile_uv.y > 0.5 ? si_ul_active : si_dr_active); 179 | 180 | vec3 stripe_stren = active_blend_strength_si_gate_metal * stripe_blend; 181 | 182 | si_color = mix( 183 | si_color, 184 | active_color, 185 | si_active ? stripe_stren.r : 0.0 186 | ); 187 | float si_blend = (si_n || si_p) && si_connection ? 1.0 : 0.0; 188 | 189 | vec3 gate_color = si_n ? p_color.rgb : n_color.rgb; 190 | gate_color = mix( 191 | gate_color, 192 | active_color, 193 | gate_active ? stripe_stren.g : 0.0 194 | ); 195 | float gate_blend = gate_connection ? 1.0 : 0.0; 196 | 197 | vec3 blended_metal_color = mix( 198 | metal_color, 199 | active_color, 200 | metal_active ? stripe_stren.b : 0.0 201 | ); 202 | float metal_blend = metal && metal_connection ? 1.0 : 0.0; 203 | 204 | vec3 via_color = mix(si_color, vec3(1.0), 1.0); 205 | vec2 via_dist = tile_uv - vec2(0.5); 206 | float via_blend = 1.0 - smoothstep( 207 | 0.1, 208 | 0.3, 209 | dot(via_dist, via_dist) * 8.0 210 | ); 211 | via_blend = via ? via_blend : 0.0; 212 | 213 | // Linear color blending. 214 | // Start with base (checker) color. 215 | vec3 color = mix(background_color, grid_color, grid_blend); 216 | 217 | // Socket overrides background color 218 | color = mix(color, socket_color, socket ? 1.0 : 0.0); 219 | 220 | // Cursor follow highlight. 221 | color = mix( 222 | color, 223 | cursor_follow_color, 224 | cursor ? 0.5 : 0.0 225 | ); 226 | 227 | // Si totally overrides base color. 228 | color = mix(color, si_color, si_blend); 229 | 230 | // Gate totally overrides si 231 | color = mix(color, gate_color, gate_blend); 232 | 233 | // Metal is only blended if there is si. 234 | color = mix( 235 | color, 236 | blended_metal_color, 237 | si_blend > 0.5 ? metal_blend * metal_over_si_blend : metal_blend 238 | ); 239 | 240 | // Vias are on or off. 241 | color = mix(color, via_color, via_blend); 242 | 243 | // Cell selection highlights the whole cell 244 | color = mix( 245 | color, 246 | cell_select_color, 247 | cell_selected ? 0.5 : 0.0 248 | ); 249 | 250 | out_color = vec4(color, 1.0); 251 | } 252 | -------------------------------------------------------------------------------- /src/shaders/cell.vert: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | in vec2 position; 4 | in vec2 uv; 5 | 6 | uniform mat4 view_proj; 7 | 8 | out vec2 v_uv; 9 | 10 | void main() { 11 | gl_Position = view_proj * vec4(position.x, position.y, 0.0, 1.0); 12 | v_uv = uv; 13 | } 14 | -------------------------------------------------------------------------------- /src/substrate/buffer.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cell::{Ref, RefCell}, 3 | rc::Rc, 4 | }; 5 | 6 | use glam::{IVec2, UVec2}; 7 | use wasm_bindgen::prelude::*; 8 | 9 | use crate::{ 10 | coords::{CellCoord, ChunkCoord, LocalCoord, CHUNK_CELL_COUNT, CHUNK_SIZE, LOG_CHUNK_SIZE}, 11 | upc::{Bit, Metal, NormalizedCell, Placement, Silicon, LOG_UPC_BYTE_LEN, UPC, UPC_BYTE_LEN}, 12 | utils::{names::make_name_unique, Selection}, 13 | }; 14 | 15 | const CHUNK_BYTE_LEN: usize = CHUNK_CELL_COUNT * UPC_BYTE_LEN; 16 | 17 | #[derive(Default, Clone)] 18 | #[wasm_bindgen] 19 | pub struct Buffer { 20 | /// The chunks that make up this buffer, stored as a dense list of Copy On Write byte arrays, 21 | /// each ready for blitting to the GPU. Each chunk tracks how many cells are non-default, and 22 | /// empty chunks are repurposed when a new chunk is needed (zero-allocation). Additionally, 23 | /// this vec if very cheap to clone, as the chunk data is stored behind a Rc RefCell. This 24 | /// means that you can clone a Buffer, update one cell, and only a single chunk will actually 25 | /// be copied (the one that was copy-on-write updated). 26 | #[wasm_bindgen(skip)] 27 | pub chunks: Vec, 28 | 29 | /// A socket is a named GPIO in the buffer, connected to the metal layer. 30 | #[wasm_bindgen(skip)] 31 | pub sockets: Vec, 32 | } 33 | 34 | #[derive(Clone)] 35 | pub struct BufferChunk { 36 | /// The chunk coord. Used by a buffer to index chunks, but not used by the BufferChunk itself. 37 | pub chunk_coord: ChunkCoord, 38 | 39 | /// How many cells are non-default. 40 | pub cell_count: usize, 41 | 42 | /// 4-byte cells, in row-major order. Ready for blitting to the GPU. 43 | /// Stored in a Rc> to allow for COW semantics. This sllows Buffers to be very 44 | /// cheaply cloned, and mutations to require a minimum set of chunk copies. 45 | cells: Rc>, 46 | } 47 | 48 | #[derive(Clone)] 49 | pub struct Socket { 50 | /// The unique (within a Buffer) name of the socket. 51 | pub name: String, 52 | 53 | /// Where the socket is in the buffer. Not to be confused with the bond pad around it. 54 | pub cell_coord: CellCoord, 55 | } 56 | 57 | #[wasm_bindgen] 58 | impl Buffer { 59 | #[wasm_bindgen(constructor)] 60 | pub fn new() -> Self { 61 | Default::default() 62 | } 63 | 64 | /// Gets the cell at the given location, returning default if the chunk doesn't exist. 65 | pub fn get_cell(&self, cell_coord: CellCoord) -> UPC { 66 | let chunk_coord: ChunkCoord = cell_coord.into(); 67 | for chunk in &self.chunks { 68 | if chunk.chunk_coord == chunk_coord { 69 | return chunk.get_cell(cell_coord); 70 | } 71 | } 72 | 73 | // Chunk isn't allocated 74 | Default::default() 75 | } 76 | 77 | /// Sets the cell at the given location, allocating a chunk to house it if needed. Note that 78 | /// setting and clearing a socket will have no effect, you must use the dedicated socket 79 | /// methods for that. 80 | pub fn set_cell(&mut self, cell_coord: CellCoord, mut cell: UPC) { 81 | Bit::set(&mut cell, Bit::SOCKET, false); 82 | Bit::set(&mut cell, Bit::BOND_PAD, false); 83 | self.set_cell_unchecked(cell_coord, cell, true); 84 | } 85 | 86 | /// Sets the `cell_coord` to have a socket with the given unique name. If the name is 87 | /// non-unique, a number will be appended to the end of the name. Setting the socket to None is 88 | /// the same as clearing it. 89 | pub fn set_socket(&mut self, cell_coord: CellCoord, name: Option) { 90 | match name { 91 | Some(name) => { 92 | let name = 93 | make_name_unique(name, self.sockets.iter().map(|s| s.name.clone()).collect()); 94 | 95 | // If the socket is already set (aka this is a just a rename) 96 | if let Some(socket) = self.sockets.iter_mut().find(|s| s.cell_coord == cell_coord) { 97 | socket.name = name.to_string(); 98 | return; 99 | } else { 100 | // Set a new socket 101 | let mut cell = self.get_cell(cell_coord); 102 | Bit::set(&mut cell, Bit::SOCKET, true); 103 | self.set_cell_unchecked(cell_coord, cell, false); 104 | self.sockets.push(Socket { 105 | cell_coord, 106 | name: name.to_string(), 107 | }); 108 | } 109 | } 110 | None => { 111 | // Clear the socket 112 | self.sockets.retain(|s| s.cell_coord != cell_coord); 113 | let mut cell = self.get_cell(cell_coord); 114 | Bit::set(&mut cell, Bit::SOCKET, false); 115 | self.set_cell_unchecked(cell_coord, cell, false); 116 | } 117 | } 118 | } 119 | 120 | pub fn clone_selection(&self, selection: &Selection, anchor: CellCoord) -> Buffer { 121 | let mut buffer = Buffer::default(); 122 | let ll = selection.lower_left.0; 123 | let ur = selection.upper_right.0; 124 | 125 | // Clone cells. 126 | for y in ll.y..ur.y { 127 | for x in ll.x..ur.x { 128 | let from_cell_coord = CellCoord(IVec2::new(x, y)); 129 | let to_cell_coord = CellCoord(IVec2::new(x, y) - anchor.0); 130 | let cell = self.get_cell(from_cell_coord); 131 | buffer.set_cell(to_cell_coord, cell); 132 | } 133 | } 134 | 135 | buffer 136 | } 137 | 138 | pub fn paste_at(&mut self, cell_coord: CellCoord, buffer: &Buffer) { 139 | // While pasting values, find the bounding rect the target we are pasting into. 140 | let mut ll = IVec2::new(i32::MAX, i32::MAX); 141 | let mut ur = IVec2::new(i32::MIN, i32::MIN); 142 | 143 | // Paste cells 144 | for chunk in &buffer.chunks { 145 | let target_first_cell_offset = chunk.chunk_coord.first_cell_coord().0 + cell_coord.0; 146 | 147 | for y in 0..CHUNK_SIZE { 148 | for x in 0..CHUNK_SIZE { 149 | let local_coord = LocalCoord(UVec2::new(x as u32, y as u32)); 150 | let cell = chunk.get_cell(local_coord); 151 | 152 | if cell == Default::default() { 153 | continue; 154 | } 155 | 156 | let target_cell_coord = 157 | CellCoord(target_first_cell_offset + IVec2::new(x as i32, y as i32)); 158 | 159 | // Tracking bounding rect. 160 | ll = IVec2::min(ll, target_cell_coord.0); 161 | ur = IVec2::max(ur, target_cell_coord.0); 162 | 163 | self.set_cell(target_cell_coord, cell); 164 | } 165 | } 166 | } 167 | 168 | // Then go through the outline of the bounding rect and fix any broken cells. 169 | for x in ll.x..(ur.x + 1) { 170 | self.fix_cell(CellCoord(IVec2::new(x, ll.y))); 171 | self.fix_cell(CellCoord(IVec2::new(x, ur.y))); 172 | } 173 | 174 | for y in ll.y..(ur.y + 1) { 175 | self.fix_cell(CellCoord(IVec2::new(ll.x, y))); 176 | self.fix_cell(CellCoord(IVec2::new(ur.x, y))); 177 | } 178 | } 179 | 180 | pub fn rotate_to_new(&self) -> Self { 181 | let mut buffer = Self::default(); 182 | 183 | for chunk in &self.chunks { 184 | for y in 0..CHUNK_SIZE { 185 | for x in 0..CHUNK_SIZE { 186 | let local_coord = LocalCoord(UVec2::new(x as u32, y as u32)); 187 | let c = local_coord.to_cell_coord(&chunk.chunk_coord).0; 188 | 189 | // Rotate coordinates around the origin. 190 | buffer.set_cell( 191 | CellCoord(IVec2::new(c.y, -c.x)), 192 | chunk.get_cell(local_coord).rotate(), 193 | ); 194 | } 195 | } 196 | } 197 | 198 | buffer 199 | } 200 | 201 | pub fn mirror_to_new(&self) -> Self { 202 | let mut buffer = Self::default(); 203 | 204 | for chunk in &self.chunks { 205 | for y in 0..CHUNK_SIZE { 206 | for x in 0..CHUNK_SIZE { 207 | let local_coord = LocalCoord(UVec2::new(x as u32, y as u32)); 208 | let c = local_coord.to_cell_coord(&chunk.chunk_coord).0; 209 | 210 | // Mirror around the x axis (flip Y values). 211 | buffer.set_cell( 212 | CellCoord(IVec2::new(c.x, -c.y)), 213 | chunk.get_cell(local_coord).mirror(), 214 | ); 215 | } 216 | } 217 | } 218 | 219 | buffer 220 | } 221 | 222 | pub fn fix_all_cells(&mut self) { 223 | let chunk_coords: Vec = self.chunks.iter().map(|c| c.chunk_coord).collect(); 224 | for chunk_coord in chunk_coords { 225 | let chunk_first_cell = chunk_coord.first_cell_coord().0; 226 | for y in 0..CHUNK_SIZE { 227 | for x in 0..CHUNK_SIZE { 228 | self.fix_cell(CellCoord(IVec2::new(x as i32, y as i32) + chunk_first_cell)); 229 | } 230 | } 231 | } 232 | } 233 | 234 | pub fn cell_count(&self) -> usize { 235 | self.chunks.iter().map(|c| c.cell_count).sum() 236 | } 237 | 238 | /// Sets the cell without first clearing socket or bond pad bits 239 | fn set_cell_unchecked(&mut self, cell_coord: CellCoord, mut cell: UPC, preserve_socket: bool) { 240 | let chunk_coord: ChunkCoord = cell_coord.into(); 241 | 242 | // Existing chunks 243 | for chunk in &mut self.chunks { 244 | if chunk.chunk_coord == chunk_coord { 245 | chunk.set_cell(cell_coord, cell, preserve_socket); 246 | return; 247 | } 248 | } 249 | 250 | // If a default UPC is being set, there is nothing else to do (no point in making an empty 251 | // chunk). 252 | if cell == Default::default() { 253 | return; 254 | } 255 | 256 | // See if we have an empty chunk we can repurpose 257 | for chunk in &mut self.chunks { 258 | if chunk.cell_count == 0 { 259 | chunk.chunk_coord = chunk_coord; 260 | chunk.set_cell(cell_coord, cell, preserve_socket); 261 | return; 262 | } 263 | } 264 | 265 | // Otherwise allocate a new chunk and push it to the back. 266 | let mut chunk = BufferChunk::new(chunk_coord); 267 | chunk.set_cell(cell_coord, cell, preserve_socket); 268 | self.chunks.push(chunk); 269 | } 270 | 271 | fn fix_cell(&mut self, cell_coord: CellCoord) { 272 | // Follow broken connection directions and connect them, if able. The following 273 | // connections will be made (every other connection will be dropped): 274 | // - Metal -> metal 275 | // Both NP and MOSFET, take the type trying to connect (N/P): 276 | // N : NP(n) 277 | // P : NP(p) 278 | // N : MOSFET(npn) if ec_in_line_with_dir 279 | // N : MOSFET(pnp) if gate_in_line_with_dir 280 | // P : MOSFET(pnp) if ec_in_line_with_dir 281 | // P : MOSFET(npn) if gate_in_line_with_dir 282 | // 283 | // Otherwise the connection is culled. 284 | 285 | let orig_upc = self.get_cell(cell_coord); 286 | 287 | if orig_upc == Default::default() { 288 | return; 289 | } 290 | 291 | let mut cell: NormalizedCell = orig_upc.into(); 292 | 293 | // Handle metal (which is simple). 294 | if let Metal::Trace { placement: pl, .. } = &mut cell.metal { 295 | for dir in pl.cardinal_vectors() { 296 | let n: NormalizedCell = self.get_cell(CellCoord(cell_coord.0 + dir)).into(); 297 | 298 | if let Metal::Trace { 299 | placement: n_pl, .. 300 | } = n.metal 301 | { 302 | if !n_pl.has_cardinal(-dir) { 303 | // Cannot make the connection, so remove the placement. 304 | pl.clear_cardinal(dir); 305 | } 306 | } else { 307 | pl.clear_cardinal(dir); 308 | } 309 | } 310 | } 311 | 312 | // Returns a new Placement type that has only valid connections for the is type in the given 313 | // directions. 314 | let check_si_pl = |pl: Placement, is_n: bool| { 315 | let mut fixed_pl = Placement::default(); 316 | 317 | for dir in pl.cardinal_vectors() { 318 | let n: NormalizedCell = self.get_cell(CellCoord(cell_coord.0 + dir)).into(); 319 | 320 | // This matches the success-table in the comment above. 321 | match (is_n, n.si) { 322 | ( 323 | true, 324 | Silicon::NP { 325 | is_n: true, 326 | placement: n_pl, 327 | }, 328 | ) 329 | | ( 330 | false, 331 | Silicon::NP { 332 | is_n: false, 333 | placement: n_pl, 334 | }, 335 | ) 336 | | ( 337 | true, 338 | Silicon::Mosfet { 339 | is_npn: true, 340 | ec_placement: n_pl, 341 | .. 342 | }, 343 | ) 344 | | ( 345 | true, 346 | Silicon::Mosfet { 347 | is_npn: false, 348 | gate_placement: n_pl, 349 | .. 350 | }, 351 | ) 352 | | ( 353 | false, 354 | Silicon::Mosfet { 355 | is_npn: true, 356 | gate_placement: n_pl, 357 | .. 358 | }, 359 | ) 360 | | ( 361 | false, 362 | Silicon::Mosfet { 363 | is_npn: false, 364 | ec_placement: n_pl, 365 | .. 366 | }, 367 | ) => { 368 | if n_pl.has_cardinal(-dir) { 369 | fixed_pl.set_cardinal(dir); 370 | } 371 | } 372 | _ => {} 373 | } 374 | } 375 | 376 | fixed_pl 377 | }; 378 | 379 | // Handle Si type 380 | match &mut cell.si { 381 | Silicon::NP { is_n, placement } => { 382 | *placement = check_si_pl(*placement, *is_n); 383 | } 384 | Silicon::Mosfet { 385 | is_npn, 386 | gate_placement, 387 | ec_placement, 388 | .. 389 | } => { 390 | *ec_placement = check_si_pl(*ec_placement, *is_npn); 391 | *gate_placement = check_si_pl(*gate_placement, !*is_npn); 392 | } 393 | _ => {} 394 | } 395 | 396 | // Write the cell back. 397 | let new_upc: UPC = cell.into(); 398 | self.set_cell(cell_coord, new_upc); 399 | } 400 | } 401 | 402 | impl BufferChunk { 403 | pub fn new(chunk_coord: ChunkCoord) -> Self { 404 | Self { 405 | chunk_coord, 406 | cells: Rc::new(RefCell::new([0_u8; CHUNK_BYTE_LEN])), 407 | cell_count: 0, 408 | } 409 | } 410 | 411 | #[inline(always)] 412 | pub fn get_cell(&self, c: T) -> UPC 413 | where 414 | T: Into, 415 | { 416 | let coord: LocalCoord = c.into(); 417 | let idx = (((coord.0.y << LOG_CHUNK_SIZE) + coord.0.x) as usize) << LOG_UPC_BYTE_LEN; 418 | UPC::from_slice(&self.cells.borrow()[idx..idx + UPC_BYTE_LEN]) 419 | } 420 | 421 | #[inline(always)] 422 | pub fn set_cell(&mut self, c: T, mut cell: UPC, preserve_socket: bool) 423 | where 424 | T: Into, 425 | { 426 | let coord: LocalCoord = c.into(); 427 | let idx = (((coord.0.y << LOG_CHUNK_SIZE) + coord.0.x) as usize) << LOG_UPC_BYTE_LEN; 428 | let existing = UPC::from_slice(&self.cells.borrow()[idx..idx + UPC_BYTE_LEN]); 429 | 430 | if preserve_socket { 431 | Bit::set(&mut cell, Bit::SOCKET, Bit::get(existing, Bit::SOCKET)); 432 | Bit::set(&mut cell, Bit::BOND_PAD, Bit::get(existing, Bit::BOND_PAD)); 433 | } 434 | 435 | // If the existing cell is the same, then there is nothing we need to do. 436 | if existing == cell { 437 | return; 438 | } 439 | 440 | if existing == Default::default() && cell != Default::default() { 441 | self.cell_count += 1; 442 | } else if existing != Default::default() && cell == Default::default() { 443 | self.cell_count -= 1; 444 | } 445 | 446 | // Take ownership of the cell data and set it. 447 | let cells = self.get_cells_mut(); 448 | let slice = &mut cells[idx..idx + UPC_BYTE_LEN]; 449 | slice.copy_from_slice(&cell.0); 450 | } 451 | 452 | pub fn get_cells(&self) -> Ref<[u8; CHUNK_BYTE_LEN]> { 453 | self.cells.borrow() 454 | } 455 | 456 | pub fn get_cells_mut(&mut self) -> &mut [u8; CHUNK_BYTE_LEN] { 457 | Rc::make_mut(&mut self.cells).get_mut() 458 | } 459 | } 460 | -------------------------------------------------------------------------------- /src/substrate/buffer_delta.rs: -------------------------------------------------------------------------------- 1 | use glam::{IVec2, UVec2}; 2 | use im::HashSet; 3 | 4 | use crate::{ 5 | coords::{CellCoord, ChunkCoord, LocalCoord, CHUNK_SIZE}, 6 | upc::UPC, 7 | }; 8 | 9 | use super::buffer::Buffer; 10 | 11 | /// Stores a sparse set of deltas, encoded as (CellCoord, Cell) tuples. It's not particularly 12 | /// compact in-memory, but compresses just fine. Because cell diffs get added one chunk at a time, 13 | /// a BufferDelta can also be efficiently applied to a Buffer. 14 | pub struct BufferDelta { 15 | cell_deltas: Vec, 16 | } 17 | 18 | pub struct CellDelta { 19 | /// The cell coord of this delta. 20 | cell_coord: CellCoord, 21 | /// The call's value in UPC format. This value will be copied without modification to the 22 | /// Buffer while being applied (aka zero values are 'clear' ops, non-zero values set the cell). 23 | cell: UPC, 24 | } 25 | 26 | impl BufferDelta { 27 | /// Create a delta from two buffers. `from` is the previous buffer, and `to` is the target 28 | /// buffer. In other words, the delta can be applied to `from` and will result in `to` as an 29 | /// output. 30 | pub fn new(from: &Buffer, to: &Buffer) -> Self { 31 | let mut cell_deltas = vec![]; 32 | 33 | // Start by collecting a full set of chunk coords we will be visiting, this is a union of 34 | // from and to chunks. 35 | let chunk_coords: HashSet = from 36 | .chunks 37 | .iter() 38 | .map(|c| c.chunk_coord) 39 | .chain(to.chunks.iter().map(|c| c.chunk_coord)) 40 | .collect(); 41 | 42 | for chunk_coord in chunk_coords { 43 | let from = from.chunks.iter().find(|c| c.chunk_coord == chunk_coord); 44 | let to = to.chunks.iter().find(|c| c.chunk_coord == chunk_coord); 45 | let first_cell_coord = chunk_coord.first_cell_coord(); 46 | 47 | for y in 0..CHUNK_SIZE { 48 | for x in 0..CHUNK_SIZE { 49 | let local_coord = LocalCoord(UVec2::new(x as u32, y as u32)); 50 | 51 | match (from, to) { 52 | (None, None) => unreachable!(), 53 | (None, Some(to)) => { 54 | let cell = to.get_cell(local_coord); 55 | // Only encode newly set cells 56 | if cell != Default::default() { 57 | cell_deltas.push(CellDelta::new(first_cell_coord, x, y, cell)); 58 | } 59 | } 60 | (Some(from), None) => { 61 | let cell = from.get_cell(local_coord); 62 | // Encode all from cells as empty (they were all unset) 63 | if cell != Default::default() { 64 | cell_deltas.push(CellDelta::new( 65 | first_cell_coord, 66 | x, 67 | y, 68 | Default::default(), 69 | )); 70 | } 71 | } 72 | (Some(from), Some(to)) => { 73 | let from = from.get_cell(local_coord); 74 | let to = to.get_cell(local_coord); 75 | 76 | // Encode the `to` cell anywhere that from != to (that they changed) 77 | if from != to { 78 | cell_deltas.push(CellDelta::new(first_cell_coord, x, y, to)); 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | Self { cell_deltas } 87 | } 88 | 89 | /// Apply this delta to the `from` buffer, returning what would have been passed into the 90 | /// `to` arg of the `new` method. 91 | pub fn apply(&self, from: &Buffer) -> Buffer { 92 | let mut to = from.clone(); 93 | 94 | for cell_delta in &self.cell_deltas { 95 | to.set_cell(cell_delta.cell_coord, cell_delta.cell); 96 | } 97 | 98 | to 99 | } 100 | } 101 | 102 | impl CellDelta { 103 | pub fn new(first_cell_in_chunk: CellCoord, x: usize, y: usize, cell: UPC) -> Self { 104 | let cell_coord = CellCoord(first_cell_in_chunk.0 + IVec2::new(x as i32, y as i32)); 105 | Self { cell_coord, cell } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/substrate/buffer_serde.rs: -------------------------------------------------------------------------------- 1 | use base64::{engine::general_purpose::STANDARD, Engine}; 2 | use std::io::prelude::*; 3 | use wasm_bindgen::prelude::*; 4 | 5 | use crate::{ 6 | coords::{ChunkCoord, CHUNK_CELL_COUNT}, 7 | substrate::buffer::{Buffer, BufferChunk}, 8 | upc::UPC_BYTE_LEN, 9 | }; 10 | 11 | #[derive(bincode::Encode, bincode::Decode)] 12 | pub enum VersionWrapper { 13 | // Snappy compressed chunk data, using SNAPPY_CHANNELS bytes per cell with 128 cell wide 14 | // chunks. If either of these things change, the format version will need to be bumped. 15 | V1(SnappyBuffer), 16 | } 17 | 18 | #[derive(bincode::Encode, bincode::Decode)] 19 | pub struct SnappyBuffer { 20 | channels: u32, 21 | chunks: Vec, 22 | } 23 | 24 | #[derive(bincode::Encode, bincode::Decode)] 25 | pub struct SnappyChunk { 26 | chunk_x: i32, 27 | chunk_y: i32, 28 | cell_count: u32, 29 | data: Vec, 30 | } 31 | 32 | #[wasm_bindgen] 33 | impl Buffer { 34 | pub fn to_base64_string(&self) -> Result { 35 | let bytes = self.to_bytes()?; 36 | Ok(STANDARD.encode(bytes)) 37 | } 38 | 39 | pub fn to_bytes(&self) -> Result, JsValue> { 40 | const SNAPPY_CHANNELS: usize = 2; 41 | let mut snappy_buffer = SnappyBuffer { 42 | chunks: Vec::new(), 43 | channels: SNAPPY_CHANNELS as u32, 44 | }; 45 | 46 | for chunk in &self.chunks { 47 | let mut snappy_image = [0_u8; CHUNK_CELL_COUNT * SNAPPY_CHANNELS]; 48 | for i in 0..CHUNK_CELL_COUNT { 49 | for j in 0..SNAPPY_CHANNELS { 50 | snappy_image[i * SNAPPY_CHANNELS + j] = chunk.get_cells()[i * UPC_BYTE_LEN + j]; 51 | } 52 | } 53 | 54 | let mut writer = snap::write::FrameEncoder::new(Vec::new()); 55 | writer.write_all(&snappy_image).unwrap(); 56 | writer.flush().unwrap(); 57 | let data = writer.into_inner().unwrap(); 58 | 59 | snappy_buffer.chunks.push(SnappyChunk { 60 | chunk_x: chunk.chunk_coord.0.x, 61 | chunk_y: chunk.chunk_coord.0.y, 62 | cell_count: chunk.cell_count as u32, 63 | data, 64 | }); 65 | } 66 | 67 | // Bincode the frames 68 | let final_bytes = bincode::encode_to_vec( 69 | VersionWrapper::V1(snappy_buffer), 70 | bincode::config::standard(), 71 | ) 72 | .map_err(|e| JsValue::from_str(&e.to_string()))?; 73 | 74 | Ok(final_bytes) 75 | } 76 | 77 | pub fn from_base64_string(base_64_string: &str) -> Result { 78 | let bytes = STANDARD 79 | .decode(base_64_string) 80 | .map_err(|e| JsValue::from_str(&e.to_string()))?; 81 | Buffer::from_bytes(&bytes) 82 | } 83 | 84 | pub fn from_bytes(bytes: &[u8]) -> Result { 85 | let (version, _bytes_read): (VersionWrapper, _) = 86 | bincode::decode_from_slice(&bytes, bincode::config::standard()) 87 | .map_err(|e| JsValue::from_str(&e.to_string()))?; 88 | 89 | match version { 90 | VersionWrapper::V1(snappy_buffer) => { 91 | let channels = snappy_buffer.channels as usize; 92 | let mut buffer = Buffer::new(); 93 | let mut snappy_image = vec![0_u8; CHUNK_CELL_COUNT * channels]; 94 | 95 | for chunk in &snappy_buffer.chunks { 96 | let mut reader = snap::read::FrameDecoder::new(&chunk.data[..]); 97 | match reader.read(&mut snappy_image[..]) { 98 | Ok(read_size) => { 99 | if read_size != CHUNK_CELL_COUNT * channels { 100 | return Err(JsValue::from_str(&format!( 101 | "expected {} bytes but {} bytes were read", 102 | CHUNK_CELL_COUNT * channels, 103 | read_size 104 | ))); 105 | } 106 | } 107 | Err(e) => { 108 | return Err(JsValue::from_str(&format!("{}", e))); 109 | } 110 | } 111 | 112 | // Convert to standard chunk 113 | let mut buffer_chunk = 114 | BufferChunk::new(ChunkCoord((chunk.chunk_x, chunk.chunk_y).into())); 115 | let cells = buffer_chunk.get_cells_mut(); 116 | 117 | for i in 0..CHUNK_CELL_COUNT { 118 | for j in 0..channels { 119 | cells[i * UPC_BYTE_LEN + j] = snappy_image[i * channels + j]; 120 | } 121 | } 122 | 123 | buffer_chunk.cell_count = chunk.cell_count as usize; 124 | buffer.chunks.push(buffer_chunk); 125 | } 126 | 127 | Ok(buffer) 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/substrate/compress.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use flate2::write::ZlibEncoder; 4 | use flate2::Compression; 5 | use tiff::encoder::compression::{Deflate, DeflateLevel, Lzw, Packbits}; 6 | use tiff::encoder::{colortype, TiffEncoder}; 7 | 8 | use std::io::{Cursor, Write}; 9 | use std::time::Instant; 10 | 11 | use crate::{log, utils::convert::import_legacy_blueprint}; 12 | 13 | #[test] 14 | pub fn compare_sizes() { 15 | let now = Instant::now(); 16 | v1_json(); 17 | println!("{} ms\n", now.elapsed().as_millis()); 18 | 19 | // let now = Instant::now(); 20 | // v1_bincode(); 21 | // println!("{} ms\n", now.elapsed().as_millis()); 22 | // 23 | // let now = Instant::now(); 24 | // v2_bincode(); 25 | // println!("{} ms\n", now.elapsed().as_millis()); 26 | 27 | let now = Instant::now(); 28 | tiff_rgba_lzw(); 29 | println!("{} ms\n", now.elapsed().as_millis()); 30 | 31 | let now = Instant::now(); 32 | tiff_rgba_deflate(); 33 | println!("{} ms\n", now.elapsed().as_millis()); 34 | 35 | let now = Instant::now(); 36 | tiff_rgba_packbits(); 37 | println!("{} ms\n", now.elapsed().as_millis()); 38 | 39 | let now = Instant::now(); 40 | tiff_gray_gray_deflate(); 41 | println!("{} ms\n", now.elapsed().as_millis()); 42 | 43 | let now = Instant::now(); 44 | deflate_one_channel(); 45 | println!("{} ms\n", now.elapsed().as_millis()); 46 | 47 | let now = Instant::now(); 48 | deflate_two_channel(); 49 | println!("{} ms\n", now.elapsed().as_millis()); 50 | let now = Instant::now(); 51 | brotli_one_channel(); 52 | println!("{} ms\n", now.elapsed().as_millis()); 53 | 54 | let now = Instant::now(); 55 | brotli_one_channel(); 56 | println!("{} ms\n", now.elapsed().as_millis()); 57 | 58 | let now = Instant::now(); 59 | brotli_two_channel(); 60 | println!("{} ms\n", now.elapsed().as_millis()); 61 | 62 | let now = Instant::now(); 63 | brotli_one_channel_rgba_chunked(); 64 | println!("{} ms\n", now.elapsed().as_millis()); 65 | 66 | let now = Instant::now(); 67 | brotli_one_channel_rg_chunked(); 68 | println!("{} ms\n", now.elapsed().as_millis()); 69 | 70 | // let now = Instant::now(); 71 | // brotli_over_v2(); 72 | // println!("{} ms\n", now.elapsed().as_millis()); 73 | 74 | for l in 0..12 { 75 | let now = Instant::now(); 76 | brotli_one_channel_at_level(l); 77 | println!("{} ms\n", now.elapsed().as_millis()); 78 | } 79 | 80 | let now = Instant::now(); 81 | snappy_one_channel_rg_chunked(); 82 | println!("{} ms\n", now.elapsed().as_millis()); 83 | 84 | let now = Instant::now(); 85 | snappy_two_channel(); 86 | println!("{} ms\n", now.elapsed().as_millis()); 87 | 88 | let now = Instant::now(); 89 | snappy_two_channel_chunked(); 90 | println!("{} ms\n", now.elapsed().as_millis()); 91 | } 92 | 93 | fn v1_json() { 94 | let json = include_str!("../../misc/cpu.lpbp").to_string(); 95 | log!("V1 Json:\t{}", json.len()); 96 | } 97 | 98 | // fn v1_bincode() { 99 | // let json = include_str!("../../misc/cpu.lpbp").to_string(); 100 | // let buffer = import_legacy_blueprint(json).unwrap(); 101 | // let bytes = 102 | // bincode::encode_to_vec(EncodeV1::from(&buffer), bincode::config::standard()).unwrap(); 103 | // log!("V1 Bincode:\t{}", bytes.len()); 104 | // } 105 | // 106 | // fn v2_bincode() { 107 | // let json = include_str!("../../misc/cpu.lpbp").to_string(); 108 | // let buffer = import_legacy_blueprint(json).unwrap(); 109 | // let bytes = 110 | // bincode::encode_to_vec(EncodeV2::from(&buffer), bincode::config::standard()).unwrap(); 111 | // log!("V2 Bincode:\t{}", bytes.len()); 112 | // } 113 | 114 | fn tiff_rgba_lzw() { 115 | let image_data = buffer_as_single_image(); 116 | 117 | let mut file = Cursor::new(Vec::new()); 118 | let mut tiff = TiffEncoder::new(&mut file).unwrap(); 119 | 120 | let image = tiff 121 | .new_image_with_compression::( 122 | image_data.width, 123 | image_data.height, 124 | Lzw::default(), 125 | ) 126 | .unwrap(); 127 | 128 | image.write_data(&image_data.pixels_rgba).unwrap(); 129 | log!("TIFF (RG)BA LZW:\t{}", file.get_ref().len()); 130 | } 131 | 132 | fn tiff_rgba_deflate() { 133 | let image_data = buffer_as_single_image(); 134 | 135 | let mut file = Cursor::new(Vec::new()); 136 | let mut tiff = TiffEncoder::new(&mut file).unwrap(); 137 | 138 | let image = tiff 139 | .new_image_with_compression::( 140 | image_data.width, 141 | image_data.height, 142 | Deflate::with_level(DeflateLevel::Best), 143 | ) 144 | .unwrap(); 145 | 146 | image.write_data(&image_data.pixels_rgba).unwrap(); 147 | log!("TIFF (RG)BA Deflate:\t{}", file.get_ref().len()); 148 | } 149 | 150 | fn tiff_gray_gray_deflate() { 151 | let image_data = buffer_as_single_image(); 152 | let r_data = image_data 153 | .pixels_rgba 154 | .iter() 155 | .step_by(4) 156 | .cloned() 157 | .collect::>(); 158 | let g_data = image_data 159 | .pixels_rgba 160 | .iter() 161 | .skip(1) 162 | .step_by(4) 163 | .cloned() 164 | .collect::>(); 165 | 166 | let r_bytes = { 167 | let mut file = Cursor::new(Vec::new()); 168 | let mut tiff = TiffEncoder::new(&mut file).unwrap(); 169 | 170 | let image = tiff 171 | .new_image_with_compression::( 172 | image_data.width, 173 | image_data.height, 174 | Deflate::with_level(DeflateLevel::Best), 175 | ) 176 | .unwrap(); 177 | 178 | image.write_data(&r_data).unwrap(); 179 | 180 | file.get_ref().len() 181 | }; 182 | 183 | let g_bytes = { 184 | let mut file = Cursor::new(Vec::new()); 185 | let mut tiff = TiffEncoder::new(&mut file).unwrap(); 186 | 187 | let image = tiff 188 | .new_image_with_compression::( 189 | image_data.width, 190 | image_data.height, 191 | Deflate::with_level(DeflateLevel::Best), 192 | ) 193 | .unwrap(); 194 | 195 | image.write_data(&g_data).unwrap(); 196 | 197 | file.get_ref().len() 198 | }; 199 | 200 | log!("TIFF Gray-Gray Deflate:\t{}", r_bytes + g_bytes); 201 | } 202 | 203 | fn tiff_rgba_packbits() { 204 | let image_data = buffer_as_single_image(); 205 | 206 | let mut file = Cursor::new(Vec::new()); 207 | let mut tiff = TiffEncoder::new(&mut file).unwrap(); 208 | 209 | let image = tiff 210 | .new_image_with_compression::( 211 | image_data.width, 212 | image_data.height, 213 | Packbits::default(), 214 | ) 215 | .unwrap(); 216 | 217 | image.write_data(&image_data.pixels_rgba).unwrap(); 218 | log!("TIFF (RG)BA Packbits:\t{}", file.get_ref().len()); 219 | } 220 | 221 | fn deflate_one_channel() { 222 | let image_data = buffer_as_single_image(); 223 | let bytes = image_data 224 | .pixels_rgba 225 | .iter() 226 | .enumerate() 227 | .filter(|(i, _)| i % 4 < 2) 228 | .map(|(_, &val)| val) 229 | .collect::>(); 230 | 231 | let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); 232 | encoder.write_all(&bytes).unwrap(); 233 | let bytes = encoder.finish().unwrap(); 234 | 235 | log!("Deflate one-channel:\t{}", bytes.len()); 236 | } 237 | 238 | fn deflate_two_channel() { 239 | let image_data = buffer_as_single_image(); 240 | let r_data = image_data 241 | .pixels_rgba 242 | .iter() 243 | .step_by(4) 244 | .cloned() 245 | .collect::>(); 246 | let g_data = image_data 247 | .pixels_rgba 248 | .iter() 249 | .skip(1) 250 | .step_by(4) 251 | .cloned() 252 | .collect::>(); 253 | 254 | let r = { 255 | let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); 256 | encoder.write_all(&r_data).unwrap(); 257 | encoder.finish().unwrap() 258 | }; 259 | 260 | let g = { 261 | let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); 262 | encoder.write_all(&g_data).unwrap(); 263 | encoder.finish().unwrap() 264 | }; 265 | 266 | log!("Deflate two-channel:\t{}", r.len() + g.len()); 267 | } 268 | 269 | fn brotli_one_channel() { 270 | let image_data = buffer_as_single_image(); 271 | let bytes = image_data 272 | .pixels_rgba 273 | .iter() 274 | .enumerate() 275 | .filter(|(i, _)| i % 4 < 2) 276 | .map(|(_, &val)| val) 277 | .collect::>(); 278 | 279 | let file = Cursor::new(Vec::new()); 280 | let mut writer = brotli::CompressorWriter::new(file, 100000, 7, 22); 281 | writer.write_all(&bytes).unwrap(); 282 | writer.flush().unwrap(); 283 | 284 | log!("Brotli one-channel:\t{}", writer.get_ref().get_ref().len()); 285 | } 286 | 287 | fn brotli_two_channel() { 288 | let image_data = buffer_as_single_image(); 289 | let r_data = image_data 290 | .pixels_rgba 291 | .iter() 292 | .step_by(4) 293 | .cloned() 294 | .collect::>(); 295 | let g_data = image_data 296 | .pixels_rgba 297 | .iter() 298 | .skip(1) 299 | .step_by(4) 300 | .cloned() 301 | .collect::>(); 302 | 303 | let file = Cursor::new(Vec::new()); 304 | let mut writer = brotli::CompressorWriter::new(file, 100000, 7, 22); 305 | writer.write_all(&r_data).unwrap(); 306 | writer.write_all(&g_data).unwrap(); 307 | writer.flush().unwrap(); 308 | 309 | log!("Brotli two-channel:\t{}", writer.get_ref().get_ref().len()); 310 | } 311 | 312 | fn brotli_one_channel_rgba_chunked() { 313 | let json = include_str!("../../misc/cpu.lpbp").to_string(); 314 | let buffer = import_legacy_blueprint(json).unwrap(); 315 | 316 | let file = Cursor::new(Vec::new()); 317 | let mut writer = brotli::CompressorWriter::new(file, 4096, 7, 22); 318 | 319 | for chunk in buffer.chunks.values() { 320 | writer.write_all(&chunk.cells).unwrap(); 321 | } 322 | writer.flush().unwrap(); 323 | 324 | log!( 325 | "Brotli one channel RGBA chunked:\t{}", 326 | writer.get_ref().get_ref().len() 327 | ); 328 | } 329 | 330 | fn brotli_one_channel_rg_chunked() { 331 | let json = include_str!("../../misc/cpu.lpbp").to_string(); 332 | let buffer = import_legacy_blueprint(json).unwrap(); 333 | 334 | let file = Cursor::new(Vec::new()); 335 | let mut writer = brotli::CompressorWriter::new(file, 4096, 7, 22); 336 | 337 | for chunk in buffer.chunks.values() { 338 | writer 339 | .write_all( 340 | &chunk 341 | .cells 342 | .iter() 343 | .enumerate() 344 | .filter(|(i, _)| i % 4 < 2) 345 | .map(|(_, &val)| val) 346 | .collect::>(), 347 | ) 348 | .unwrap(); 349 | } 350 | writer.flush().unwrap(); 351 | 352 | log!( 353 | "Brotli one channel RG chunked:\t{}", 354 | writer.get_ref().get_ref().len() 355 | ); 356 | } 357 | 358 | fn snappy_one_channel_rg_chunked() { 359 | let json = include_str!("../../misc/cpu.lpbp").to_string(); 360 | let buffer = import_legacy_blueprint(json).unwrap(); 361 | 362 | let mut writer = snap::write::FrameEncoder::new(Vec::new()); 363 | 364 | for chunk in buffer.chunks.values() { 365 | writer 366 | .write_all( 367 | &chunk 368 | .cells 369 | .iter() 370 | .enumerate() 371 | .filter(|(i, _)| i % 4 < 2) 372 | .map(|(_, &val)| val) 373 | .collect::>(), 374 | ) 375 | .unwrap(); 376 | } 377 | writer.flush().unwrap(); 378 | 379 | log!( 380 | "Snappy one channel RG chunked:\t{}", 381 | writer.into_inner().unwrap().len() 382 | ); 383 | } 384 | 385 | fn snappy_two_channel() { 386 | let image_data = buffer_as_single_image(); 387 | let r_data = image_data 388 | .pixels_rgba 389 | .iter() 390 | .step_by(4) 391 | .cloned() 392 | .collect::>(); 393 | let g_data = image_data 394 | .pixels_rgba 395 | .iter() 396 | .skip(1) 397 | .step_by(4) 398 | .cloned() 399 | .collect::>(); 400 | 401 | let mut writer = snap::write::FrameEncoder::new(Vec::new()); 402 | writer.write_all(&r_data).unwrap(); 403 | writer.write_all(&g_data).unwrap(); 404 | writer.flush().unwrap(); 405 | 406 | log!( 407 | "Snappy two channel:\t{}", 408 | writer.into_inner().unwrap().len() 409 | ); 410 | } 411 | 412 | fn snappy_two_channel_chunked() { 413 | let json = include_str!("../../misc/cpu.lpbp").to_string(); 414 | let buffer = import_legacy_blueprint(json).unwrap(); 415 | 416 | let mut writer = snap::write::FrameEncoder::new(Vec::new()); 417 | 418 | for chunk in buffer.chunks.values() { 419 | writer 420 | .write_all( 421 | &chunk 422 | .cells 423 | .iter() 424 | .enumerate() 425 | .filter(|(i, _)| i % 4 < 2) 426 | .map(|(_, &val)| val) 427 | .collect::>(), 428 | ) 429 | .unwrap(); 430 | } 431 | 432 | writer.flush().unwrap(); 433 | 434 | log!( 435 | "Snappy two channel chunked:\t{}", 436 | writer.into_inner().unwrap().len() 437 | ); 438 | } 439 | 440 | // fn brotli_over_v2() { 441 | // let json = include_str!("../../misc/cpu.lpbp").to_string(); 442 | // let buffer = import_legacy_blueprint(json).unwrap(); 443 | // let bytes = 444 | // bincode::encode_to_vec(EncodeV2::from(&buffer), bincode::config::standard()).unwrap(); 445 | // 446 | // let file = Cursor::new(Vec::new()); 447 | // let mut writer = brotli::CompressorWriter::new(file, 4096, 7, 22); 448 | // writer.write_all(&bytes).unwrap(); 449 | // writer.flush().unwrap(); 450 | // 451 | // log!( 452 | // "Brotli over Encode V2:\t{}", 453 | // writer.get_ref().get_ref().len() 454 | // ); 455 | // } 456 | 457 | fn brotli_one_channel_at_level(level: u32) { 458 | let image_data = buffer_as_single_image(); 459 | let bytes = image_data 460 | .pixels_rgba 461 | .iter() 462 | .enumerate() 463 | .filter(|(i, _)| i % 4 < 2) 464 | .map(|(_, &val)| val) 465 | .collect::>(); 466 | 467 | let file = Cursor::new(Vec::new()); 468 | let mut writer = brotli::CompressorWriter::new(file, bytes.len(), level, 22); 469 | writer.write_all(&bytes).unwrap(); 470 | writer.flush().unwrap(); 471 | 472 | log!( 473 | "Brotli one-channel at level {}:\t{}", 474 | level, 475 | writer.get_ref().get_ref().len() 476 | ); 477 | } 478 | 479 | struct SingleImage { 480 | width: u32, 481 | height: u32, 482 | offset_x: i32, 483 | offset_y: i32, 484 | pixels_rgba: Vec, 485 | } 486 | 487 | fn buffer_as_single_image() -> SingleImage { 488 | let json = include_str!("../../misc/cpu.lpbp").to_string(); 489 | let buffer = import_legacy_blueprint(json).unwrap(); 490 | 491 | let x_min = buffer 492 | .chunks 493 | .keys() 494 | .map(|k| k.first_cell_coord().0.x) 495 | .min() 496 | .unwrap(); 497 | let y_min = buffer 498 | .chunks 499 | .keys() 500 | .map(|k| k.first_cell_coord().0.y) 501 | .min() 502 | .unwrap(); 503 | let x_max = buffer 504 | .chunks 505 | .keys() 506 | .map(|k| k.last_cell_coord().0.x) 507 | .max() 508 | .unwrap(); 509 | let y_max = buffer 510 | .chunks 511 | .keys() 512 | .map(|k| k.last_cell_coord().0.y) 513 | .max() 514 | .unwrap(); 515 | 516 | let width = (x_max - x_min).abs() as u32; 517 | let height = (y_max - y_min).abs() as u32; 518 | let offset_x = -x_min; 519 | let offset_y = -y_min; 520 | 521 | let mut pixels_rgba = vec![0_u8; (width * height * 4) as usize]; 522 | 523 | for cell_y in y_min..y_max { 524 | for cell_x in x_min..x_max { 525 | let image_x = cell_x - x_min; 526 | let image_y = cell_y - y_min; 527 | let image_i = (image_y * (width as i32) + image_x) * 4; 528 | let cell = buffer.get_cell((cell_x, cell_y).into()); 529 | 530 | pixels_rgba[image_i as usize + 0] = cell.0[0]; 531 | pixels_rgba[image_i as usize + 1] = cell.0[1]; 532 | pixels_rgba[image_i as usize + 2] = cell.0[2]; 533 | pixels_rgba[image_i as usize + 3] = cell.0[3]; 534 | } 535 | } 536 | 537 | SingleImage { 538 | width, 539 | height, 540 | offset_x, 541 | offset_y, 542 | pixels_rgba, 543 | } 544 | } 545 | } 546 | -------------------------------------------------------------------------------- /src/substrate/execution_context.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | coords::CellCoord, 3 | module::{Module, ModuleGpioHandle}, 4 | substrate::{ 5 | buffer::Buffer, 6 | compiler::{Atom, CellPart, CompilerResults}, 7 | mask::{Mask, MASK_BYTE_LEN}, 8 | }, 9 | }; 10 | 11 | pub type ModuleHandle = usize; 12 | pub type BondWireHandle = isize; 13 | 14 | pub struct ExecutionContext { 15 | pub compiler_results: CompilerResults, 16 | pub modules: Vec>, 17 | // Compacted list of all bond wires. 18 | pub bond_wires: Vec, 19 | // Index by trace to get bond wires. 20 | pub bonded_traces: Vec, 21 | // Pending module triggers, indexed by ModuleHandle. When the inner vec is empty, the module 22 | // has no pending triggers. 23 | pub pending_module_triggers: Vec>, 24 | pub max_ticks_per_clock: usize, 25 | pub buffer_mask: Mask, 26 | pub state: SimState, 27 | } 28 | 29 | #[derive(Default)] 30 | pub struct SimState { 31 | pub micro_ticks: usize, 32 | pub ticks: usize, 33 | pub gate_states: Vec, 34 | pub trace_states: Vec, 35 | } 36 | 37 | #[derive(Clone)] 38 | pub struct BondWire { 39 | // The trace this bond wire ultimately attaches. 40 | pub trace: usize, 41 | 42 | // The cell coordinate of the socket. 43 | pub cell_coord: CellCoord, 44 | 45 | // A handle to the module that owns the GPIO. 46 | pub module_handle: ModuleHandle, 47 | 48 | // A handle to the GPIO within the module. 49 | pub gpio_handle: ModuleGpioHandle, 50 | } 51 | 52 | pub enum CompilerError { 53 | MissingBondWire(String), 54 | } 55 | 56 | impl ExecutionContext { 57 | pub fn compile_from_buffer( 58 | buffer: &Buffer, 59 | mut modules: Vec>, 60 | ) -> Result { 61 | let compiler_results = CompilerResults::from_buffer(&buffer); 62 | let gate_states = vec![false; compiler_results.gates.len()]; 63 | let trace_states = vec![false; compiler_results.traces.len()]; 64 | 65 | // Create bond wires (links between module GPIOs and Buffer Sockets 66 | let mut bond_wires = vec![]; 67 | let mut bonded_traces = vec![-1; compiler_results.traces.len()]; 68 | 69 | for (module_handle, module) in modules.iter_mut().enumerate() { 70 | for (gpio_handle, gpio) in module.get_gpios_mut().iter().enumerate() { 71 | let socket = buffer 72 | .sockets 73 | .iter() 74 | .find(|s| s.name == gpio.name) 75 | .ok_or_else(|| CompilerError::MissingBondWire(gpio.name.clone()))?; 76 | 77 | let Some(&trace) = compiler_results.trace_lookup_by_atom.get(&Atom { 78 | coord: socket.cell_coord, 79 | part: CellPart::Metal, 80 | }) else { 81 | continue; 82 | }; 83 | 84 | bonded_traces[trace] = bond_wires.len() as isize; 85 | bond_wires.push(BondWire { 86 | trace, 87 | cell_coord: socket.cell_coord, 88 | module_handle, 89 | gpio_handle, 90 | }); 91 | } 92 | } 93 | 94 | Ok(Self { 95 | compiler_results, 96 | modules, 97 | bond_wires, 98 | bonded_traces, 99 | pending_module_triggers: vec![], 100 | max_ticks_per_clock: 100_000, 101 | buffer_mask: Default::default(), 102 | state: SimState { 103 | micro_ticks: 0, 104 | ticks: 0, 105 | gate_states, 106 | trace_states, 107 | }, 108 | }) 109 | } 110 | 111 | pub fn tick_once(&mut self) { 112 | // Starts a single tick (one transistor propagation-delay) which is made up of resetting 113 | // traces to low, propagating high traces through the stable gate network via micro-ticks, 114 | // then computing new gate states based on the trace values. 115 | for state in self.state.trace_states.iter_mut() { 116 | *state = false; 117 | } 118 | 119 | // Collect modules for input (from modules into the substrate) state. We skip this on the 120 | // first ever tick to set a consistent gate state. 121 | if self.state.ticks > 0 { 122 | for bond_wire in &self.bond_wires { 123 | self.state.trace_states[bond_wire.trace] |= self.modules[bond_wire.module_handle] 124 | .get_gpios_mut()[bond_wire.gpio_handle] 125 | .si_input_high; 126 | } 127 | } 128 | 129 | // Propagate trace high states through gates (who's state (open/closed) is already known). 130 | // Trace states are reset each tick and high states are fully propagated through the net in 131 | // micro-ticks. This is necessary because our transistors don't have a source/drain, they 132 | // are bi-directional, and a high state on one side drives a high state on the other, 133 | // regardless of what source it's connected to. 134 | // 135 | // Just to be really clear (for myself in the future) this only propagates high states, and 136 | // only one micro-tick (we aren't done computing a single tick until micro-ticks are 137 | // stable. And they will always become stable, because gates aren't changing state (ie the 138 | // graph is stable). 139 | loop { 140 | let mut change = false; 141 | for (i, gate) in self.compiler_results.gates.iter().enumerate() { 142 | // If the gate isn't conducting, ignore it as it can't effect the other trace. 143 | if !self.state.gate_states[i] { 144 | continue; 145 | } 146 | 147 | // Get trace states 148 | let left = self.state.trace_states[gate.left_ec_trace]; 149 | let right = self.state.trace_states[gate.right_ec_trace]; 150 | 151 | // Nothing to update here, both traces are already low or both high. 152 | if left == right { 153 | continue; 154 | } 155 | 156 | // One of the two traces is going high this micro-tick, so mark the change. 157 | change |= true; 158 | 159 | // If left is going high 160 | if !left { 161 | self.state.trace_states[gate.left_ec_trace] = true; 162 | } else { 163 | self.state.trace_states[gate.right_ec_trace] = true; 164 | } 165 | } 166 | 167 | self.state.micro_ticks += 1; 168 | 169 | if !change { 170 | break; 171 | } 172 | } 173 | 174 | // Update gate states 175 | for (i, gate) in self.compiler_results.gates.iter().enumerate() { 176 | let base = self.state.trace_states[gate.base_trace]; 177 | self.state.gate_states[i] = if gate.is_npn { base } else { !base }; 178 | } 179 | 180 | // Reset pending module triggers 181 | for triggers in &mut self.pending_module_triggers { 182 | triggers.clear(); 183 | } 184 | 185 | // Update output (from the substrate to module) states and mark modules that have pending 186 | // triggers. 187 | for bond_wire in &self.bond_wires { 188 | let gpio = 189 | &mut self.modules[bond_wire.module_handle].get_gpios_mut()[bond_wire.module_handle]; 190 | let trace_state = self.state.trace_states[bond_wire.trace]; 191 | 192 | // If the state changes and the GPIO is a trigger 193 | if gpio.trigger && trace_state != gpio.si_output_high { 194 | self.pending_module_triggers[bond_wire.module_handle].push(bond_wire.gpio_handle); 195 | } 196 | 197 | // Set or clear state 198 | gpio.si_output_high = trace_state; 199 | } 200 | 201 | self.state.ticks += 1; 202 | } 203 | 204 | pub fn update_buffer_mask(&mut self) { 205 | for (chunk_coord, cell_part_to_traces) in self 206 | .compiler_results 207 | .trace_to_cell_part_index_by_chunk 208 | .iter() 209 | { 210 | let chunk = self.buffer_mask.get_or_create_chunk_mut(*chunk_coord); 211 | for index in cell_part_to_traces { 212 | let i = index.cell_index_in_chunk * MASK_BYTE_LEN; 213 | let cell_slice = &mut chunk.cells[i..i + MASK_BYTE_LEN]; 214 | cell_slice[0] = if self.state.trace_states[index.metal_trace] { 215 | 1 216 | } else { 217 | 0 218 | }; 219 | cell_slice[1] = if self.state.trace_states[index.si_trace] { 220 | 1 221 | } else { 222 | 0 223 | }; 224 | cell_slice[2] = if self.state.trace_states[index.left_ec_trace] { 225 | 1 226 | } else { 227 | 0 228 | }; 229 | cell_slice[3] = if self.state.trace_states[index.right_ec_trace] { 230 | 1 231 | } else { 232 | 0 233 | }; 234 | } 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/substrate/io.rs: -------------------------------------------------------------------------------- 1 | use glam::{IVec2, Vec2}; 2 | use wasm_bindgen::prelude::*; 3 | use web_sys::{KeyboardEvent, MouseEvent, WheelEvent}; 4 | 5 | use crate::{coords::CellCoord, utils::range_iter, wgl2::Camera}; 6 | 7 | #[wasm_bindgen] 8 | #[derive(PartialEq, Eq, Clone, Copy, Default, Debug)] 9 | pub struct BoolState { 10 | /// The key was just clicked this dispatch. 11 | pub clicked: bool, 12 | 13 | /// The key is being held down. Can be true when `clicked` is true. 14 | pub down: bool, 15 | 16 | /// The key was just released this dispatch. 17 | pub released: bool, 18 | } 19 | 20 | impl BoolState { 21 | /// Steps the boolean state forward if it's in a single-frame state (clicked or released). Has 22 | /// no effect if the state is steady (inactive or held) 23 | pub fn tick(&mut self) { 24 | if self.clicked { 25 | // State continues to be true 26 | *self = Self { 27 | clicked: false, 28 | down: true, 29 | released: false, 30 | } 31 | } else if self.released { 32 | // State finished release 33 | *self = Self { 34 | clicked: false, 35 | down: false, 36 | released: false, 37 | } 38 | } 39 | } 40 | 41 | /// Transisition the boolean state forward, given the new input. Steps to/from both 42 | /// single-frame states and steady states. 43 | pub fn transition(self, new_state: bool) -> Self { 44 | if new_state { 45 | if !self.down { 46 | // State is first true 47 | Self { 48 | clicked: true, 49 | down: true, 50 | released: false, 51 | } 52 | } else { 53 | // State continues to be true 54 | Self { 55 | clicked: false, 56 | down: true, 57 | released: false, 58 | } 59 | } 60 | } else { 61 | if self.down { 62 | // State is first false 63 | Self { 64 | clicked: false, 65 | down: false, 66 | released: true, 67 | } 68 | } else { 69 | // State continues to be false 70 | Self { 71 | clicked: false, 72 | down: false, 73 | released: false, 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | #[wasm_bindgen(getter_with_clone)] 81 | #[derive(Clone)] 82 | pub struct KeyState { 83 | pub key_code: String, 84 | pub key: String, 85 | pub state: BoolState, 86 | } 87 | 88 | #[wasm_bindgen] 89 | #[derive(Clone, Copy, Debug)] 90 | pub struct Drag { 91 | pub start: CellCoord, 92 | pub initial_impulse_vertical: bool, 93 | } 94 | 95 | #[wasm_bindgen(getter_with_clone)] 96 | #[derive(Clone, Default)] 97 | pub struct IoState { 98 | pub hovered: BoolState, 99 | 100 | pub primary: BoolState, 101 | pub secondary: BoolState, 102 | pub scroll_button: BoolState, 103 | pub drag: Option, 104 | pub keys: Vec, 105 | pub screen_point: Vec2, 106 | pub cell: CellCoord, 107 | pub scroll_delta_y: f32, 108 | } 109 | 110 | #[wasm_bindgen] 111 | impl IoState { 112 | #[wasm_bindgen(constructor)] 113 | pub fn new() -> Self { 114 | Default::default() 115 | } 116 | 117 | pub fn event_key_down(&mut self, e: KeyboardEvent) { 118 | let key_state = KeyState { 119 | key: e.key(), 120 | key_code: e.code(), 121 | state: self.get_key_code(&e.code()).transition(true), 122 | }; 123 | 124 | self.keys.retain(|key| key.key_code != e.code()); 125 | self.tick(); 126 | 127 | self.keys.push(key_state); 128 | } 129 | 130 | pub fn event_key_up(&mut self, e: KeyboardEvent) { 131 | let key_state = KeyState { 132 | key: e.key(), 133 | key_code: e.code(), 134 | state: self.get_key_code(&e.code()).transition(false), 135 | }; 136 | 137 | self.keys.retain(|key| key.key_code != e.code()); 138 | self.tick(); 139 | 140 | self.keys.push(key_state); 141 | } 142 | 143 | pub fn event_mouse(&mut self, e: MouseEvent, camera: &Camera) { 144 | // Note: e.buttons is a bitfield 145 | let primary = self.primary.transition(e.buttons() & 1 != 0); 146 | let secondary = self.secondary.transition(e.buttons() & 2 != 0); 147 | let scroll_button = self.scroll_button.transition(e.buttons() & 4 != 0); 148 | 149 | let new_cell = camera.project_screen_point_to_cell(self.screen_point); 150 | 151 | self.tick(); 152 | 153 | // Handle drag start 154 | if (primary.clicked || secondary.clicked) && self.drag.is_none() { 155 | self.drag = Some(Drag { 156 | start: new_cell, 157 | initial_impulse_vertical: false, 158 | }); 159 | } 160 | 161 | // Handle drag ending 162 | if !primary.down && !secondary.down { 163 | self.drag = None; 164 | } 165 | 166 | // Handle first cell that isn't drag_start while dragging (for initial impulse) 167 | if let Some(drag) = &mut self.drag { 168 | if self.cell == drag.start && new_cell != drag.start { 169 | let dist = new_cell.0 - drag.start.0; 170 | drag.initial_impulse_vertical = dist.x.abs() < dist.y.abs(); 171 | } 172 | } 173 | 174 | self.primary = primary; 175 | self.secondary = secondary; 176 | self.scroll_button = scroll_button; 177 | self.screen_point = Vec2::new(e.offset_x() as f32, e.offset_y() as f32); 178 | self.cell = new_cell; 179 | } 180 | 181 | pub fn event_mouse_presence(&mut self, presence: bool) { 182 | let hovered = self.hovered.transition(presence); 183 | self.tick(); 184 | self.hovered = hovered; 185 | } 186 | 187 | pub fn event_wheel(&mut self, e: WheelEvent) { 188 | self.tick(); 189 | self.scroll_delta_y = (e.delta_y() / 1000.0) as f32; 190 | } 191 | 192 | fn tick(&mut self) { 193 | // Tick all input boolean states 194 | self.hovered.tick(); 195 | self.primary.tick(); 196 | self.secondary.tick(); 197 | self.scroll_button.tick(); 198 | 199 | // Tick all keys 200 | for key in &mut self.keys { 201 | key.state.tick(); 202 | } 203 | 204 | // And only retain active 205 | self.keys.retain(|k| k.state != Default::default()); 206 | 207 | // Reset scroll 208 | self.scroll_delta_y = 0.0; 209 | } 210 | 211 | pub fn get_key(&self, key: &str) -> BoolState { 212 | for key_state in &self.keys { 213 | if key_state.key == key { 214 | return key_state.state; 215 | } 216 | } 217 | 218 | Default::default() 219 | } 220 | 221 | pub fn get_key_code(&self, key_code: &str) -> BoolState { 222 | for key_state in &self.keys { 223 | if key_state.key_code == key_code { 224 | return key_state.state; 225 | } 226 | } 227 | 228 | Default::default() 229 | } 230 | 231 | pub fn get_drag_path(&self) -> Vec { 232 | let mut steps = vec![]; 233 | 234 | if let Some(drag) = &self.drag { 235 | let start = drag.start.0; 236 | let end = self.cell.0; 237 | 238 | if drag.initial_impulse_vertical { 239 | // Draw Y first, then X. 240 | for y in range_iter(start.y, end.y) { 241 | steps.push(CellCoord(IVec2::new(start.x, y))); 242 | } 243 | for x in range_iter(start.x, end.x) { 244 | steps.push(CellCoord(IVec2::new(x, end.y))); 245 | } 246 | } else { 247 | // Draw X first, then Y. 248 | for x in range_iter(start.x, end.x) { 249 | steps.push(CellCoord(IVec2::new(x, start.y))); 250 | } 251 | for y in range_iter(start.y, end.y) { 252 | steps.push(CellCoord(IVec2::new(end.x, y))); 253 | } 254 | } 255 | } 256 | 257 | // The last point will be skipped because range_iter is non-inclusive of end point. 258 | steps.push(self.cell); 259 | 260 | steps 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/substrate/label_builder.rs: -------------------------------------------------------------------------------- 1 | use glam::IVec2; 2 | 3 | use crate::{ 4 | coords::CellCoord, 5 | log, 6 | substrate::{buffer::Buffer, io::IoState}, 7 | utils::{convert::import_legacy_blueprint, Selection}, 8 | }; 9 | 10 | #[derive(Clone)] 11 | pub struct LabelBuilder { 12 | text: String, 13 | font_face_buffer: Buffer, 14 | cursor: usize, 15 | } 16 | 17 | impl Default for LabelBuilder { 18 | fn default() -> Self { 19 | let font_face_buffer = { 20 | if let Ok(buffer) = 21 | import_legacy_blueprint(include_str!("../../templates/font_file.lpbp").to_string()) 22 | { 23 | buffer 24 | } else { 25 | log!("Failed to deserialize JSON, or structure is invalid."); 26 | Buffer::default() 27 | } 28 | }; 29 | 30 | Self { 31 | text: Default::default(), 32 | font_face_buffer, 33 | cursor: 0, 34 | } 35 | } 36 | } 37 | 38 | impl LabelBuilder { 39 | pub fn dispatch_input(&mut self, io_state: &IoState) { 40 | // // A bit of a hack: check if the key is 'printable'. 41 | // if io_state.key_clicked.len() == 1 { 42 | // self.text 43 | // .insert(self.cursor, io_state.key_clicked.chars().nth(0).unwrap()); 44 | // self.cursor += 1; 45 | // } else { 46 | // match io_state.key_code_clicked.as_str() { 47 | // "Enter" => { 48 | // self.text.insert(self.cursor, '\n'); 49 | // self.cursor += 1; 50 | // } 51 | // "Backspace" => { 52 | // if self.cursor > 0 { 53 | // self.text.remove(self.cursor - 1); 54 | // self.cursor -= 1; 55 | // 56 | // // Continue removing whole word if Ctrl was held. 57 | // if io_state.ctrl { 58 | // while self.cursor > 0 59 | // && self.text.chars().nth(self.cursor - 1).unwrap() != ' ' 60 | // { 61 | // self.text.remove(self.cursor - 1); 62 | // self.cursor -= 1; 63 | // } 64 | // } 65 | // } 66 | // } 67 | // "ArrowLeft" => self.cursor = if self.cursor > 0 { self.cursor - 1 } else { 0 }, 68 | // "ArrowRight" => { 69 | // self.cursor = if self.cursor < self.text.len() { 70 | // self.cursor + 1 71 | // } else { 72 | // self.text.len() 73 | // } 74 | // } 75 | // _ => {} 76 | // } 77 | // } 78 | } 79 | 80 | pub fn render_to_buffer(&self, render_markers: bool) -> Buffer { 81 | let mut buffer = Buffer::default(); 82 | let mut cursor_x = 0; 83 | let mut cursor_y = 0; 84 | 85 | for c in self.text.chars() { 86 | if c == '\n' { 87 | cursor_y -= 4; 88 | cursor_x = 0; 89 | continue; 90 | } 91 | 92 | if !c.is_ascii() { 93 | continue; 94 | } 95 | 96 | let ascii = c as u8; 97 | 98 | // Space starts at 32. Everything before that are control signals. 99 | let index = (ascii as i32) - 32; 100 | let ll = IVec2::new(index * 3, 0); 101 | 102 | let character_buffer = self.font_face_buffer.clone_selection( 103 | &Selection { 104 | lower_left: CellCoord(ll), 105 | upper_right: CellCoord(ll + IVec2::new(3, 3)), 106 | }, 107 | CellCoord(ll), 108 | ); 109 | 110 | buffer.paste_at(CellCoord(IVec2::new(cursor_x, cursor_y)), &character_buffer); 111 | cursor_x += 3; 112 | } 113 | 114 | // Draw cursor 115 | if render_markers { 116 | let mut c_x = 0; 117 | let mut c_y = 0; 118 | 119 | for (i, c) in self.text.chars().enumerate() { 120 | if i >= self.cursor { 121 | break; 122 | } 123 | 124 | if c == '\n' { 125 | c_y -= 4; 126 | c_x = 0; 127 | } else { 128 | c_x += 3; 129 | } 130 | } 131 | 132 | c_y -= 1; 133 | buffer.draw_metal_link(None, (c_x, c_y).into()); 134 | for _ in 1..5 { 135 | buffer.draw_metal_link(Some((c_x, c_y).into()), (c_x, c_y + 1).into()); 136 | c_y += 1; 137 | } 138 | } 139 | 140 | buffer 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/substrate/mask.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | 3 | use crate::{ 4 | coords::{ChunkCoord, LocalCoord, CHUNK_SIZE}, 5 | substrate::{ 6 | buffer::Buffer, 7 | compiler::{Atom, CellPart, CompilerResults}, 8 | }, 9 | }; 10 | 11 | /// Number of bytes used per cell in a BufferMask 12 | pub const MASK_BYTE_LEN: usize = 4; 13 | 14 | /// Much like a Buffer, except lacking any undo or transaction support. Designed to 'overlay' a 15 | /// buffer, activating various atoms. Any active atom that does not overlay a cell is considered 16 | /// undefined behavior. 17 | #[derive(Default, Clone)] 18 | #[wasm_bindgen] 19 | pub struct Mask { 20 | chunks: im::HashMap, 21 | } 22 | 23 | #[allow(dead_code)] 24 | impl Mask { 25 | pub fn from_highlight_trace(buffer: &Buffer, atom: Atom) -> Mask { 26 | let mut mask = Mask::default(); 27 | let trace = CompilerResults::get_trace_atoms(buffer, atom); 28 | 29 | for atom in trace { 30 | let chunk_coord: ChunkCoord = atom.coord.into(); 31 | let local_coord: LocalCoord = atom.coord.into(); 32 | 33 | let chunk = mask.get_or_create_chunk_mut(chunk_coord); 34 | let i = local_coord.to_upc_idx(); 35 | match atom.part { 36 | CellPart::Metal => chunk.cells[i + 0] = 1, 37 | CellPart::Si => chunk.cells[i + 1] = 1, 38 | CellPart::EcUpLeft => chunk.cells[i + 2] = 1, 39 | CellPart::EcDownRight => chunk.cells[i + 3] = 1, 40 | } 41 | } 42 | 43 | mask 44 | } 45 | 46 | pub fn get_chunk(&self, c: T) -> Option<&BufferMaskChunk> 47 | where 48 | T: Into, 49 | { 50 | let coord: ChunkCoord = c.into(); 51 | self.chunks.get(&coord) 52 | } 53 | 54 | pub fn get_or_create_chunk(&mut self, c: T) -> &BufferMaskChunk 55 | where 56 | T: Into, 57 | { 58 | let coord: ChunkCoord = c.into(); 59 | 60 | match self.chunks.entry(coord) { 61 | im::hashmap::Entry::Occupied(o) => o.into_mut(), 62 | im::hashmap::Entry::Vacant(v) => v.insert(Default::default()), 63 | } 64 | } 65 | 66 | pub fn get_or_create_chunk_mut(&mut self, c: T) -> &mut BufferMaskChunk 67 | where 68 | T: Into, 69 | { 70 | let coord: ChunkCoord = c.into(); 71 | 72 | match self.chunks.entry(coord) { 73 | im::hashmap::Entry::Occupied(o) => o.into_mut(), 74 | im::hashmap::Entry::Vacant(v) => v.insert(Default::default()), 75 | } 76 | } 77 | } 78 | 79 | #[derive(Clone)] 80 | pub struct BufferMaskChunk { 81 | /// 4-byte cells, in row-major order. Ready for blitting to the GPU. 82 | pub cells: Vec, 83 | } 84 | 85 | impl Default for BufferMaskChunk { 86 | fn default() -> Self { 87 | Self { 88 | cells: vec![Default::default(); MASK_BYTE_LEN * CHUNK_SIZE * CHUNK_SIZE], 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/substrate/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod buffer; 2 | pub mod buffer_brush; 3 | pub mod buffer_delta; 4 | pub mod buffer_serde; 5 | pub mod compiler; 6 | pub mod compress; 7 | pub mod execution_context; 8 | pub mod io; 9 | pub mod label_builder; 10 | pub mod mask; 11 | -------------------------------------------------------------------------------- /src/tools/camera_controller.rs: -------------------------------------------------------------------------------- 1 | use glam::Vec2; 2 | 3 | use super::{Tool, ToolInput, ToolOutput}; 4 | 5 | #[derive(Default)] 6 | pub struct ToolCameraController { 7 | drag_world_anchor: Option, 8 | } 9 | 10 | impl Tool for ToolCameraController { 11 | fn get_name(&self) -> &str { 12 | "camera-controller" 13 | } 14 | 15 | fn dispatch_event( 16 | &mut self, 17 | ToolInput { 18 | io_state, camera, .. 19 | }: &ToolInput, 20 | ) -> ToolOutput { 21 | let mut camera = camera.clone(); 22 | 23 | // Track the drag-anchor for panning on initial click of Space. 24 | if io_state.get_key_code("Space").down || io_state.scroll_button.down { 25 | self.drag_world_anchor = 26 | Some(self.drag_world_anchor.unwrap_or_else(|| { 27 | camera.project_screen_point_to_world(io_state.screen_point) 28 | })); 29 | } else { 30 | self.drag_world_anchor = None; 31 | } 32 | 33 | // Handle pan mouse dragging. We want to put the drag_world_anchor directly under the mouse. 34 | let new_world_point = camera.project_screen_point_to_world(io_state.screen_point); 35 | if let Some(anchor) = self.drag_world_anchor { 36 | // How far we need to move the camera to move the anchor under the mouse 37 | camera.translation += anchor - new_world_point; 38 | } else { 39 | // Handle scroll zooming around the world anchor under the mouse. 40 | let origin_world = camera.project_screen_point_to_world(io_state.screen_point); 41 | camera.scale += camera.scale * io_state.scroll_delta_y; 42 | camera.scale = f32::clamp(camera.scale, 0.04, 40.0); 43 | let new_world_point = camera.project_screen_point_to_world(io_state.screen_point); 44 | camera.translation += origin_world - new_world_point; 45 | } 46 | 47 | ToolOutput { 48 | camera: Some(camera), 49 | ..Default::default() 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/tools/draw_metal.rs: -------------------------------------------------------------------------------- 1 | use crate::substrate::{ 2 | buffer::Buffer, 3 | compiler::{Atom, CellPart}, 4 | mask::Mask, 5 | }; 6 | 7 | use super::{Tool, ToolInput, ToolOutput}; 8 | 9 | #[derive(Default)] 10 | pub struct ToolPaintMetal { 11 | // The last complete drawing op. The buffer will be reverted to this state is drawing is 12 | // cancelled. 13 | checkpoint: Buffer, 14 | // Drawing is tracked separately from io_state to allow for primary+secondary cancelling 15 | drawing: bool, 16 | } 17 | 18 | impl Tool for ToolPaintMetal { 19 | fn get_name(&self) -> &str { 20 | "paint-metal" 21 | } 22 | 23 | fn activate(&mut self, buffer: Buffer) -> ToolOutput { 24 | self.drawing = false; 25 | self.checkpoint = buffer; 26 | ToolOutput { 27 | cursor_style: Some("crosshair".to_string()), 28 | ..Default::default() 29 | } 30 | } 31 | 32 | fn deactivate(&mut self, _buffer: Buffer) -> ToolOutput { 33 | ToolOutput { 34 | buffer: if self.drawing { 35 | Some(self.checkpoint.clone()) 36 | } else { 37 | None 38 | }, 39 | mask: Some(Default::default()), 40 | cursor_style: Some("default".to_string()), 41 | ..Default::default() 42 | } 43 | } 44 | 45 | fn dispatch_event( 46 | &mut self, 47 | ToolInput { 48 | active, 49 | io_state, 50 | buffer: previous_buffer, 51 | .. 52 | }: &ToolInput, 53 | ) -> ToolOutput { 54 | if io_state.get_key_code("KeyW").clicked { 55 | return ToolOutput { 56 | take_active: true, 57 | ..Default::default() 58 | }; 59 | } 60 | 61 | // Drawing tools have no actions while inactive. 62 | if !active { 63 | return Default::default(); 64 | } 65 | 66 | // Cancelling draw (with other mouse button) 67 | if self.drawing && (io_state.primary.clicked || io_state.secondary.clicked) { 68 | self.drawing = false; 69 | return ToolOutput { 70 | buffer: Some(self.checkpoint.clone()), 71 | mask: Some(Default::default()), 72 | ..Default::default() 73 | }; 74 | } 75 | 76 | // End drawing with commit 77 | if self.drawing && (io_state.primary.released || io_state.secondary.released) { 78 | self.drawing = false; 79 | self.checkpoint = previous_buffer.clone(); 80 | return ToolOutput { 81 | checkpoint: true, 82 | ..Default::default() 83 | }; 84 | } 85 | 86 | let mut buffer = self.checkpoint.clone(); 87 | 88 | // Start drawing 89 | if !self.drawing && (io_state.primary.clicked || io_state.secondary.clicked) { 90 | self.drawing = true; 91 | } 92 | 93 | if !self.drawing { 94 | // We aren't drawing. Peace out, Homie. 95 | return Default::default(); 96 | } 97 | 98 | let path = io_state.get_drag_path(); 99 | let mut mask = Mask::default(); 100 | 101 | // If Ctrl is held down, then we are clearing. The logic for clearing is totally different 102 | // from painting, so we handle it separately. These of we. The proverbial we. It's just me. 103 | if io_state.get_key("Control").down { 104 | path.into_iter().for_each(|c| buffer.clear_cell_metal(c)) 105 | } else { 106 | let mut from = None; 107 | 108 | for cell_coord in &path { 109 | // Primary paints metal, secondary places a Via (only once). 110 | if io_state.primary.down { 111 | buffer.draw_metal_link(from, *cell_coord); 112 | } else if io_state.secondary.down { 113 | buffer.draw_via(*cell_coord); 114 | } 115 | from = Some(*cell_coord); 116 | } 117 | 118 | // Create a highlight mask for the highlighted atom (if any) 119 | if let Some(&coord) = path.first() { 120 | mask = Mask::from_highlight_trace( 121 | &buffer, 122 | Atom { 123 | coord, 124 | part: CellPart::Metal, 125 | }, 126 | ); 127 | } 128 | } 129 | 130 | ToolOutput { 131 | buffer: Some(buffer), 132 | mask: Some(mask), 133 | ..Default::default() 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/tools/draw_si.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | substrate::{ 3 | buffer::Buffer, 4 | compiler::{Atom, CellPart}, 5 | mask::Mask, 6 | }, 7 | upc::{NormalizedCell, Silicon}, 8 | }; 9 | 10 | use super::{Tool, ToolInput, ToolOutput}; 11 | 12 | #[derive(Default)] 13 | pub struct ToolPaintSi { 14 | // The last complete drawing op. The buffer will be reverted to this state is drawing is 15 | // cancelled. 16 | checkpoint: Buffer, 17 | // Drawing is tracked separately to io_state to allow for primary+secondary cancelling 18 | drawing: bool, 19 | } 20 | 21 | impl Tool for ToolPaintSi { 22 | fn get_name(&self) -> &str { 23 | "paint-si" 24 | } 25 | 26 | fn activate(&mut self, buffer: Buffer) -> ToolOutput { 27 | self.drawing = false; 28 | self.checkpoint = buffer; 29 | ToolOutput { 30 | cursor_style: Some("crosshair".to_string()), 31 | ..Default::default() 32 | } 33 | } 34 | 35 | fn deactivate(&mut self, _buffer: Buffer) -> ToolOutput { 36 | ToolOutput { 37 | buffer: if self.drawing { 38 | Some(self.checkpoint.clone()) 39 | } else { 40 | None 41 | }, 42 | mask: Some(Default::default()), 43 | cursor_style: Some("default".to_string()), 44 | ..Default::default() 45 | } 46 | } 47 | 48 | fn dispatch_event( 49 | &mut self, 50 | ToolInput { 51 | active, 52 | io_state, 53 | buffer: previous_buffer, 54 | .. 55 | }: &ToolInput, 56 | ) -> ToolOutput { 57 | if io_state.get_key_code("KeyQ").clicked { 58 | return ToolOutput { 59 | take_active: true, 60 | ..Default::default() 61 | }; 62 | } 63 | 64 | // Drawing tools have no actions while inactive. 65 | if !active { 66 | return Default::default(); 67 | } 68 | 69 | // Cancelling draw (with other mouse button) 70 | if self.drawing && (io_state.primary.clicked || io_state.secondary.clicked) { 71 | self.drawing = false; 72 | return ToolOutput { 73 | buffer: Some(self.checkpoint.clone()), 74 | mask: Some(Default::default()), 75 | ..Default::default() 76 | }; 77 | } 78 | 79 | // End drawing with commit 80 | if self.drawing && (io_state.primary.released || io_state.secondary.released) { 81 | self.drawing = false; 82 | self.checkpoint = previous_buffer.clone(); 83 | return ToolOutput { 84 | checkpoint: true, 85 | ..Default::default() 86 | }; 87 | } 88 | 89 | let mut buffer = self.checkpoint.clone(); 90 | 91 | // Start drawing 92 | if !self.drawing && (io_state.primary.clicked || io_state.secondary.clicked) { 93 | self.drawing = true; 94 | } 95 | 96 | if !self.drawing { 97 | // We aren't drawing. Peace out, Homie. 98 | return Default::default(); 99 | } 100 | 101 | let path = io_state.get_drag_path(); 102 | let mut mask = Mask::default(); 103 | 104 | // If Ctrl is held down, then we are clearing. The logic for clearing is totally different 105 | // from painting, so we handle it separately. These of we. The preverbal we. It's just me. 106 | if io_state.get_key("Control").down { 107 | path.into_iter().for_each(|c| buffer.clear_cell_si(c)); 108 | } else { 109 | // Input modes are much, much more complicated. That logic is delegated to it's own file 110 | // because they are so stupid-complicated. 111 | let mut from = None; 112 | 113 | for cell_coord in &path { 114 | buffer.draw_si_link(from, *cell_coord, io_state.primary.down); 115 | from = Some(*cell_coord); 116 | } 117 | 118 | // Handle highlighting the trace as you draw. 119 | if path.len() > 0 { 120 | let first = path[0]; 121 | let first_cell = NormalizedCell::from(buffer.get_cell(path[0])); 122 | 123 | if let Silicon::NP { .. } = first_cell.si { 124 | mask = Mask::from_highlight_trace( 125 | &buffer, 126 | Atom { 127 | coord: first, 128 | part: CellPart::Si, 129 | }, 130 | ) 131 | } else if path.len() > 1 { 132 | let second = path[1]; 133 | let ec_up_left = first.0.x > second.0.x || first.0.y < second.0.y; 134 | mask = Mask::from_highlight_trace( 135 | &buffer, 136 | Atom { 137 | coord: first, 138 | part: if ec_up_left { 139 | CellPart::EcUpLeft 140 | } else { 141 | CellPart::EcDownRight 142 | }, 143 | }, 144 | ); 145 | } 146 | } 147 | } 148 | 149 | ToolOutput { 150 | buffer: Some(buffer), 151 | mask: Some(mask), 152 | ..Default::default() 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/tools/mod.rs: -------------------------------------------------------------------------------- 1 | use camera_controller::ToolCameraController; 2 | use draw_metal::ToolPaintMetal; 3 | use draw_si::ToolPaintSi; 4 | use visual::ToolVisual; 5 | 6 | use crate::{ 7 | substrate::{buffer::Buffer, io::IoState, mask::Mask}, 8 | utils::Selection, 9 | wgl2::Camera, 10 | }; 11 | 12 | pub mod camera_controller; 13 | pub mod draw_metal; 14 | pub mod draw_si; 15 | pub mod place_socket; 16 | pub mod visual; 17 | 18 | pub trait Tool { 19 | fn get_name(&self) -> &str; 20 | 21 | fn activate(&mut self, buffer: Buffer) -> ToolOutput { 22 | let _ = buffer; 23 | Default::default() 24 | } 25 | 26 | fn deactivate(&mut self, buffer: Buffer) -> ToolOutput { 27 | let _ = buffer; 28 | Default::default() 29 | } 30 | 31 | fn dispatch_event(&mut self, input: &ToolInput) -> ToolOutput; 32 | } 33 | 34 | pub struct ToolInput { 35 | /// If this is the active tool. Mostly here as a convenience, tools could of course track 36 | /// active themselves. 37 | pub active: bool, 38 | /// The input state of this event. 39 | pub io_state: IoState, 40 | /// The camera currently being used. 41 | pub camera: Camera, 42 | /// The editor's buffer. 43 | pub buffer: Buffer, 44 | /// The editor's selection. 45 | pub selection: Selection, 46 | } 47 | 48 | #[derive(Default)] 49 | pub struct ToolOutput { 50 | /// The buffer to persist to the Editor. If set to None, the previously set Buffer remains 51 | /// active. 52 | pub buffer: Option, 53 | /// The mask to persist to the Editor. If set to None, the previously set mask remains active. 54 | /// Masks are alwasy reset when tools switch. 55 | pub mask: Option, 56 | /// The camera to persist to the current Viewport. If set to None, the previously set camera 57 | /// remains active. 58 | pub camera: Option, 59 | /// What custom CSS cursor the tool would like to switch to. 60 | pub cursor_style: Option, 61 | /// When set to true, this is a good time to checkpoint the buffer. 62 | pub checkpoint: bool, 63 | /// Allows the tool to 'save' itself to persistent storage. 64 | pub persist_tool_state: Option>, 65 | /// When set to true, the given tool will become active. Note that `deactivate will be called 66 | /// on the previously active tool (allowing it to cleanup) before `activate` is called on this 67 | /// tool. If two tools take active, only the first will become active. 68 | pub take_active: bool, 69 | /// The selection to persist to the current Editor and Viewport. 70 | pub selection: Option, 71 | } 72 | -------------------------------------------------------------------------------- /src/tools/place_socket.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | log, 3 | substrate::{ 4 | buffer::Buffer, 5 | compiler::{Atom, CellPart}, 6 | mask::Mask, 7 | }, 8 | upc::Bit, 9 | }; 10 | 11 | use super::{Tool, ToolInput, ToolOutput}; 12 | 13 | #[derive(Default)] 14 | pub struct ToolPlaceSocket { 15 | // The last complete drawing op. The buffer will be reverted to this state is drawing is 16 | // cancelled. 17 | checkpoint: Buffer, 18 | } 19 | 20 | impl Tool for ToolPlaceSocket { 21 | fn get_name(&self) -> &str { 22 | "place-socket" 23 | } 24 | 25 | fn activate(&mut self, buffer: Buffer) -> ToolOutput { 26 | self.checkpoint = buffer; 27 | ToolOutput { 28 | cursor_style: Some("crosshair".to_string()), 29 | ..Default::default() 30 | } 31 | } 32 | 33 | fn deactivate(&mut self, _buffer: Buffer) -> ToolOutput { 34 | ToolOutput { 35 | buffer: Some(self.checkpoint.clone()), 36 | mask: Some(Default::default()), 37 | cursor_style: Some("default".to_string()), 38 | ..Default::default() 39 | } 40 | } 41 | 42 | fn dispatch_event( 43 | &mut self, 44 | ToolInput { 45 | active, io_state, .. 46 | }: &ToolInput, 47 | ) -> ToolOutput { 48 | if io_state.get_key_code("KeyE").clicked { 49 | return ToolOutput { 50 | take_active: true, 51 | ..Default::default() 52 | }; 53 | } 54 | 55 | // Drawing tools have no actions while inactive. 56 | if !active { 57 | return Default::default(); 58 | } 59 | 60 | let mut buffer = self.checkpoint.clone(); 61 | 62 | if io_state.get_key("Control").down { 63 | if Bit::get(buffer.get_cell(io_state.cell), Bit::SOCKET) { 64 | buffer.set_socket(io_state.cell, None); 65 | } 66 | } else { 67 | if !Bit::get(buffer.get_cell(io_state.cell), Bit::SOCKET) { 68 | buffer.set_socket(io_state.cell, Some("p1".to_string())); 69 | } 70 | } 71 | 72 | if io_state.primary.clicked { 73 | self.checkpoint = buffer; 74 | return ToolOutput { 75 | checkpoint: true, 76 | ..Default::default() 77 | }; 78 | } 79 | 80 | // Label all sockets 81 | for socket in buffer.sockets.clone() { 82 | buffer.draw_label(socket.cell_coord, &socket.name, None); 83 | } 84 | 85 | ToolOutput { 86 | buffer: Some(buffer), 87 | ..Default::default() 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/tools/visual.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::{substrate::buffer::Buffer, utils::Selection}; 4 | 5 | use super::{Tool, ToolInput, ToolOutput}; 6 | 7 | #[derive(Default)] 8 | pub struct ToolVisual { 9 | // The last complete drawing op. The buffer will be reverted to this state is drawing is 10 | // cancelled. 11 | checkpoint: Buffer, 12 | 13 | /// The buffer following the mouse, drawn to the `front_buffer`. Not serialized as persisting 14 | /// it makes little sense. 15 | mouse_follow_buffer: Option, 16 | 17 | /// The selected cells. Persisted until tool is deactivated. 18 | selection: Selection, 19 | 20 | /// Map of all saved register buffers. 21 | /// TODO serialize 22 | registers: HashMap, 23 | } 24 | 25 | impl Tool for ToolVisual { 26 | fn get_name(&self) -> &str { 27 | "visual" 28 | } 29 | 30 | fn activate(&mut self, buffer: Buffer) -> ToolOutput { 31 | self.checkpoint = buffer; 32 | ToolOutput { 33 | cursor_style: Some("cell".to_string()), 34 | ..Default::default() 35 | } 36 | } 37 | 38 | fn deactivate(&mut self, _buffer: Buffer) -> ToolOutput { 39 | self.selection = Default::default(); 40 | self.mouse_follow_buffer = None; 41 | ToolOutput { 42 | buffer: Some(self.checkpoint.clone()), 43 | mask: Some(Default::default()), 44 | cursor_style: Some("default".to_string()), 45 | ..Default::default() 46 | } 47 | } 48 | 49 | fn dispatch_event( 50 | &mut self, 51 | ToolInput { 52 | active, io_state, .. 53 | }: &ToolInput, 54 | ) -> ToolOutput { 55 | if io_state.get_key_code("Escape").clicked { 56 | self.selection = Default::default(); 57 | self.mouse_follow_buffer = None; 58 | return ToolOutput { 59 | take_active: true, 60 | ..Default::default() 61 | }; 62 | } 63 | 64 | if !active { 65 | return Default::default(); 66 | } 67 | 68 | let mut buffer = self.checkpoint.clone(); 69 | let mut checkpoint = false; 70 | 71 | // Check if a named register was clicked (we use this in multiple places). 72 | let named_register_clicked = "1234567890*" 73 | .chars() 74 | .map(|c| c.to_string()) 75 | .filter(|c| io_state.get_key(c).clicked) 76 | .next(); 77 | 78 | if let Some(mouse_follow_buffer) = self.mouse_follow_buffer.clone() { 79 | // Handle placing the mouse follow buffer. 80 | if io_state.primary.clicked { 81 | self.checkpoint 82 | .paste_at(io_state.cell, &mouse_follow_buffer); 83 | checkpoint = true; 84 | } 85 | 86 | // Right click (and ESC) clears the mouse follow buffer. 87 | if io_state.secondary.clicked { 88 | self.mouse_follow_buffer = None; 89 | } 90 | 91 | // KeyR will rotate the mouse-follow buffer 92 | if io_state.get_key_code("KeyR").clicked { 93 | self.mouse_follow_buffer = Some(mouse_follow_buffer.rotate_to_new()); 94 | } 95 | 96 | // KeyM will mirror the mouse-follow buffer 97 | if io_state.get_key_code("KeyM").clicked { 98 | self.mouse_follow_buffer = Some(mouse_follow_buffer.mirror_to_new()); 99 | } 100 | 101 | // Hitting KeyS + any of the named register keys will save the mouse-follow 102 | // buffer into the named register. 103 | if io_state.get_key_code("KeyS").down { 104 | if let Some(named_register) = &named_register_clicked { 105 | // If it's the clipboard register, also set the clipboard. 106 | if named_register == "*" { 107 | // TODO: Escalate to clipboard. 108 | } else { 109 | self.registers 110 | .insert(named_register.clone(), mouse_follow_buffer.clone()); 111 | } 112 | self.selection = Default::default(); 113 | } 114 | } else { 115 | // Otherwise override the mouse-follow buffer with the newly selected 116 | // register, if it exists. 117 | if let Some(named_register) = &named_register_clicked { 118 | if let Some(buffer) = self.registers.get(named_register) { 119 | self.mouse_follow_buffer = Some(buffer.clone()); 120 | } 121 | } 122 | } 123 | } else { 124 | if io_state.primary.down { 125 | if let Some(drag) = io_state.drag { 126 | self.selection = Selection::from_rectangle_inclusive(drag.start, io_state.cell); 127 | } 128 | } else if io_state.secondary.down { 129 | self.selection = Default::default(); 130 | } 131 | 132 | // Delete selection 133 | if io_state.get_key_code("KeyD").clicked { 134 | if !io_state.get_key("Shift").down { 135 | self.mouse_follow_buffer = 136 | Some(buffer.clone_selection(&self.selection, io_state.cell)); 137 | } 138 | buffer.clear_selection(&self.selection); 139 | self.selection = Default::default(); 140 | } 141 | 142 | // Yank selection to mouse-follow buffer 143 | if io_state.get_key_code("KeyY").clicked { 144 | self.mouse_follow_buffer = 145 | Some(buffer.clone_selection(&self.selection, io_state.cell)); 146 | self.selection = Default::default(); 147 | } 148 | 149 | // Hitting KeyS + any of the named register keys will save the selected cells 150 | // into the named register. 151 | if io_state.get_key_code("KeyS").down && !self.selection.is_zero() { 152 | if let Some(named_register) = &named_register_clicked { 153 | let buffer = buffer.clone_selection(&self.selection, io_state.cell); 154 | 155 | // If it's the clipboard register, also set the clipboard. 156 | self.registers.insert(named_register.clone(), buffer); 157 | // TODO: escalate 158 | self.selection = Default::default(); 159 | } 160 | } else { 161 | // Hitting any of the named register keys (while not holding KeyS) will load 162 | // the register into the mouse-follow buffer. 163 | if let Some(named_register) = named_register_clicked { 164 | // If it's the clipboard register then we have to request the clipboard 165 | // from JS and wait for it to come back. Sucks. 166 | if named_register == "*" { 167 | // TODO: escalate 168 | } else if let Some(buffer) = self.registers.get(&named_register) { 169 | self.mouse_follow_buffer = Some(buffer.clone()); 170 | } 171 | self.selection = Default::default(); 172 | } 173 | } 174 | } 175 | 176 | // If the mouse follow buffer is set after dispatch, render it to the buffer 177 | if let Some(mouse_follow_buffer) = &self.mouse_follow_buffer { 178 | buffer.paste_at(io_state.cell, mouse_follow_buffer) 179 | } 180 | 181 | ToolOutput { 182 | buffer: Some(buffer), 183 | checkpoint, 184 | selection: Some(self.selection), 185 | ..Default::default() 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/upc.rs: -------------------------------------------------------------------------------- 1 | use std::ops; 2 | 3 | use arrayvec::ArrayVec; 4 | use glam::IVec2; 5 | use serde::{Deserialize, Serialize}; 6 | use tsify::Tsify; 7 | use wasm_bindgen::prelude::*; 8 | 9 | // The number of bytes per cell, as stored in chunk data. This is 32 bit aligned to make blitting 10 | // to the GPU fast. 11 | pub const UPC_BYTE_LEN: usize = 4; 12 | // The number of bytes actually used, right now just the first 2. 13 | pub const UPC_BYTES_USED: usize = 2; 14 | pub const LOG_UPC_BYTE_LEN: usize = 2; 15 | 16 | /// Universal Packed Cell format stores each cell as a bit packed [u8; 4], ready for direct blitting 17 | /// to a GPU RGBu8 texture. Stored as [u8; 4] instead of u32 for endian agnosticism during blitting. 18 | /// Does not encode BufferMask data. The first 16 bits are also encoded as part of Blueprint 19 | /// serialization. 20 | #[derive(Default, Clone, Copy, PartialEq, Eq)] 21 | #[wasm_bindgen] 22 | pub struct UPC(#[wasm_bindgen(skip)] pub [u8; UPC_BYTE_LEN]); 23 | 24 | impl UPC { 25 | #[inline] 26 | pub fn from_slice(slice: &[u8]) -> Self { 27 | let mut bytes = [0_u8; UPC_BYTE_LEN]; 28 | bytes.copy_from_slice(slice); 29 | Self(bytes) 30 | } 31 | 32 | #[inline(always)] 33 | pub fn get_bit(&self, bit: Bit) -> bool { 34 | Bit::get(*self, bit) 35 | } 36 | 37 | #[inline(always)] 38 | pub fn set_bit(&mut self, bit: Bit) { 39 | Bit::set(self, bit, true); 40 | } 41 | 42 | pub fn is_mosfet(&self) -> bool { 43 | self.get_bit(Bit::MOSFET_HORIZONTAL) || self.get_bit(Bit::MOSFET_VERTICAL) 44 | } 45 | 46 | pub fn rotate(&self) -> UPC { 47 | let mut upc = self.clone(); 48 | Bit::set( 49 | &mut upc, 50 | Bit::MOSFET_HORIZONTAL, 51 | self.get_bit(Bit::MOSFET_VERTICAL), 52 | ); 53 | Bit::set( 54 | &mut upc, 55 | Bit::MOSFET_VERTICAL, 56 | self.get_bit(Bit::MOSFET_HORIZONTAL), 57 | ); 58 | 59 | Bit::set(&mut upc, Bit::SI_DIR_UP, self.get_bit(Bit::SI_DIR_LEFT)); 60 | Bit::set(&mut upc, Bit::SI_DIR_RIGHT, self.get_bit(Bit::SI_DIR_UP)); 61 | Bit::set(&mut upc, Bit::SI_DIR_DOWN, self.get_bit(Bit::SI_DIR_RIGHT)); 62 | Bit::set(&mut upc, Bit::SI_DIR_LEFT, self.get_bit(Bit::SI_DIR_DOWN)); 63 | 64 | Bit::set( 65 | &mut upc, 66 | Bit::METAL_DIR_UP, 67 | self.get_bit(Bit::METAL_DIR_LEFT), 68 | ); 69 | Bit::set( 70 | &mut upc, 71 | Bit::METAL_DIR_RIGHT, 72 | self.get_bit(Bit::METAL_DIR_UP), 73 | ); 74 | Bit::set( 75 | &mut upc, 76 | Bit::METAL_DIR_DOWN, 77 | self.get_bit(Bit::METAL_DIR_RIGHT), 78 | ); 79 | Bit::set( 80 | &mut upc, 81 | Bit::METAL_DIR_LEFT, 82 | self.get_bit(Bit::METAL_DIR_DOWN), 83 | ); 84 | 85 | upc 86 | } 87 | 88 | pub fn mirror(&self) -> UPC { 89 | let mut upc = self.clone(); 90 | 91 | Bit::set(&mut upc, Bit::SI_DIR_UP, self.get_bit(Bit::SI_DIR_DOWN)); 92 | Bit::set(&mut upc, Bit::SI_DIR_DOWN, self.get_bit(Bit::SI_DIR_UP)); 93 | 94 | Bit::set( 95 | &mut upc, 96 | Bit::METAL_DIR_UP, 97 | self.get_bit(Bit::METAL_DIR_DOWN), 98 | ); 99 | Bit::set( 100 | &mut upc, 101 | Bit::METAL_DIR_DOWN, 102 | self.get_bit(Bit::METAL_DIR_UP), 103 | ); 104 | 105 | upc 106 | } 107 | } 108 | 109 | #[wasm_bindgen] 110 | impl UPC { 111 | pub fn normalize(self) -> NormalizedCell { 112 | self.into() 113 | } 114 | 115 | pub fn denormalize(upc: UPC) -> Self { 116 | upc.into() 117 | } 118 | } 119 | 120 | #[allow(non_camel_case_types)] 121 | pub enum Bit { 122 | SI_N, 123 | SI_P, 124 | MOSFET_HORIZONTAL, 125 | MOSFET_VERTICAL, 126 | SI_DIR_UP, 127 | SI_DIR_RIGHT, 128 | SI_DIR_DOWN, 129 | SI_DIR_LEFT, 130 | METAL, 131 | METAL_DIR_UP, 132 | METAL_DIR_RIGHT, 133 | METAL_DIR_DOWN, 134 | METAL_DIR_LEFT, 135 | VIA, 136 | SOCKET, 137 | BOND_PAD, 138 | } 139 | 140 | impl Bit { 141 | #[inline(always)] 142 | pub fn get(upc: UPC, bit: Bit) -> bool { 143 | let upc = upc.0; 144 | match bit { 145 | Bit::SI_N => upc[0] & (1 << 7) > 0, 146 | Bit::SI_P => upc[0] & (1 << 6) > 0, 147 | Bit::MOSFET_HORIZONTAL => upc[0] & (1 << 5) > 0, 148 | Bit::MOSFET_VERTICAL => upc[0] & (1 << 4) > 0, 149 | Bit::SI_DIR_UP => upc[0] & (1 << 3) > 0, 150 | Bit::SI_DIR_RIGHT => upc[0] & (1 << 2) > 0, 151 | Bit::SI_DIR_DOWN => upc[0] & (1 << 1) > 0, 152 | Bit::SI_DIR_LEFT => upc[0] & (1 << 0) > 0, 153 | 154 | Bit::METAL => upc[1] & (1 << 7) > 0, 155 | Bit::METAL_DIR_UP => upc[1] & (1 << 6) > 0, 156 | Bit::METAL_DIR_RIGHT => upc[1] & (1 << 5) > 0, 157 | Bit::METAL_DIR_DOWN => upc[1] & (1 << 4) > 0, 158 | Bit::METAL_DIR_LEFT => upc[1] & (1 << 3) > 0, 159 | Bit::VIA => upc[1] & (1 << 2) > 0, 160 | Bit::SOCKET => upc[1] & (1 << 1) > 0, 161 | Bit::BOND_PAD => upc[1] & (1 << 0) > 0, 162 | } 163 | } 164 | 165 | #[inline(always)] 166 | pub fn set(upc: &mut UPC, bit: Bit, value: bool) { 167 | let upc = &mut upc.0; 168 | if value { 169 | match bit { 170 | Bit::SI_N => upc[0] |= 1 << 7, 171 | Bit::SI_P => upc[0] |= 1 << 6, 172 | Bit::MOSFET_HORIZONTAL => upc[0] |= 1 << 5, 173 | Bit::MOSFET_VERTICAL => upc[0] |= 1 << 4, 174 | Bit::SI_DIR_UP => upc[0] |= 1 << 3, 175 | Bit::SI_DIR_RIGHT => upc[0] |= 1 << 2, 176 | Bit::SI_DIR_DOWN => upc[0] |= 1 << 1, 177 | Bit::SI_DIR_LEFT => upc[0] |= 1 << 0, 178 | 179 | Bit::METAL => upc[1] |= 1 << 7, 180 | Bit::METAL_DIR_UP => upc[1] |= 1 << 6, 181 | Bit::METAL_DIR_RIGHT => upc[1] |= 1 << 5, 182 | Bit::METAL_DIR_DOWN => upc[1] |= 1 << 4, 183 | Bit::METAL_DIR_LEFT => upc[1] |= 1 << 3, 184 | Bit::VIA => upc[1] |= 1 << 2, 185 | Bit::SOCKET => upc[1] |= 1 << 1, 186 | Bit::BOND_PAD => upc[1] |= 1 << 0, 187 | } 188 | } else { 189 | match bit { 190 | Bit::SI_N => upc[0] &= !(1 << 7), 191 | Bit::SI_P => upc[0] &= !(1 << 6), 192 | Bit::MOSFET_HORIZONTAL => upc[0] &= !(1 << 5), 193 | Bit::MOSFET_VERTICAL => upc[0] &= !(1 << 4), 194 | Bit::SI_DIR_UP => upc[0] &= !(1 << 3), 195 | Bit::SI_DIR_RIGHT => upc[0] &= !(1 << 2), 196 | Bit::SI_DIR_DOWN => upc[0] &= !(1 << 1), 197 | Bit::SI_DIR_LEFT => upc[0] &= !(1 << 0), 198 | 199 | Bit::METAL => upc[1] &= !(1 << 7), 200 | Bit::METAL_DIR_UP => upc[1] &= !(1 << 6), 201 | Bit::METAL_DIR_RIGHT => upc[1] &= !(1 << 5), 202 | Bit::METAL_DIR_DOWN => upc[1] &= !(1 << 4), 203 | Bit::METAL_DIR_LEFT => upc[1] &= !(1 << 3), 204 | Bit::VIA => upc[1] &= !(1 << 2), 205 | Bit::SOCKET => upc[1] &= !(1 << 1), 206 | Bit::BOND_PAD => upc[1] &= !(1 << 0), 207 | } 208 | } 209 | } 210 | } 211 | 212 | /// NormalizedCell exists purely as a programming convenience, especially for painting. When editing 213 | /// cells it's easier to deal with the cell as a single struct, instead of as a collection of [0, 4] 214 | /// Atoms. NormalizedCells should be treated as transient and not stored anywhere. 215 | #[derive(Clone, Copy, Default, Debug, Eq, PartialEq)] 216 | #[wasm_bindgen] 217 | pub struct NormalizedCell { 218 | pub metal: Metal, 219 | pub si: Silicon, 220 | } 221 | 222 | #[derive(Serialize, Deserialize, Tsify, Clone, Copy, Debug, Eq, PartialEq)] 223 | #[serde(tag = "type", content = "data")] 224 | #[tsify(into_wasm_abi, from_wasm_abi)] 225 | pub enum Metal { 226 | None, 227 | Trace { 228 | has_via: bool, 229 | has_socket: bool, 230 | has_bond_pad: bool, 231 | placement: Placement, 232 | }, 233 | } 234 | 235 | impl Default for Metal { 236 | fn default() -> Self { 237 | Self::None 238 | } 239 | } 240 | 241 | #[derive(Serialize, Deserialize, Tsify, Clone, Copy, Debug, Eq, PartialEq)] 242 | #[serde(tag = "type", content = "data")] 243 | #[tsify(into_wasm_abi, from_wasm_abi)] 244 | pub enum Silicon { 245 | None, 246 | NP { 247 | is_n: bool, 248 | placement: Placement, 249 | }, 250 | Mosfet { 251 | is_npn: bool, 252 | is_horizontal: bool, 253 | gate_placement: Placement, 254 | ec_placement: Placement, 255 | }, 256 | } 257 | 258 | impl Default for Silicon { 259 | fn default() -> Self { 260 | Self::None 261 | } 262 | } 263 | 264 | impl From for NormalizedCell { 265 | fn from(upc: UPC) -> Self { 266 | let mut cell = NormalizedCell::default(); 267 | 268 | // Metal 269 | if upc.get_bit(Bit::METAL) { 270 | cell.metal = Metal::Trace { 271 | has_via: upc.get_bit(Bit::VIA), 272 | has_socket: upc.get_bit(Bit::SOCKET), 273 | has_bond_pad: upc.get_bit(Bit::BOND_PAD), 274 | placement: Placement { 275 | up: upc.get_bit(Bit::METAL_DIR_UP), 276 | right: upc.get_bit(Bit::METAL_DIR_RIGHT), 277 | down: upc.get_bit(Bit::METAL_DIR_DOWN), 278 | left: upc.get_bit(Bit::METAL_DIR_LEFT), 279 | }, 280 | } 281 | } 282 | 283 | if upc.is_mosfet() { 284 | // MOSFET 285 | let is_horizontal = upc.get_bit(Bit::MOSFET_HORIZONTAL); 286 | cell.si = Silicon::Mosfet { 287 | is_npn: upc.get_bit(Bit::SI_N), 288 | is_horizontal, 289 | gate_placement: Placement { 290 | up: !is_horizontal && upc.get_bit(Bit::SI_DIR_UP), 291 | right: is_horizontal && upc.get_bit(Bit::SI_DIR_RIGHT), 292 | down: !is_horizontal && upc.get_bit(Bit::SI_DIR_DOWN), 293 | left: is_horizontal && upc.get_bit(Bit::SI_DIR_LEFT), 294 | }, 295 | ec_placement: Placement { 296 | up: is_horizontal && upc.get_bit(Bit::SI_DIR_UP), 297 | right: !is_horizontal && upc.get_bit(Bit::SI_DIR_RIGHT), 298 | down: is_horizontal && upc.get_bit(Bit::SI_DIR_DOWN), 299 | left: !is_horizontal && upc.get_bit(Bit::SI_DIR_LEFT), 300 | }, 301 | }; 302 | } else if upc.get_bit(Bit::SI_N) || upc.get_bit(Bit::SI_P) { 303 | // Si trace only (non-mosfet) 304 | cell.si = Silicon::NP { 305 | is_n: upc.get_bit(Bit::SI_N), 306 | placement: Placement { 307 | up: upc.get_bit(Bit::SI_DIR_UP), 308 | right: upc.get_bit(Bit::SI_DIR_RIGHT), 309 | down: upc.get_bit(Bit::SI_DIR_DOWN), 310 | left: upc.get_bit(Bit::SI_DIR_LEFT), 311 | }, 312 | }; 313 | } 314 | 315 | cell 316 | } 317 | } 318 | 319 | impl From for UPC { 320 | fn from(cell: NormalizedCell) -> Self { 321 | let mut upc = Self::default(); 322 | 323 | if let Metal::Trace { 324 | has_via, 325 | has_socket, 326 | has_bond_pad, 327 | placement, 328 | } = cell.metal 329 | { 330 | upc.set_bit(Bit::METAL); 331 | Bit::set(&mut upc, Bit::VIA, has_via); 332 | Bit::set(&mut upc, Bit::SOCKET, has_socket); 333 | Bit::set(&mut upc, Bit::BOND_PAD, has_bond_pad); 334 | Bit::set(&mut upc, Bit::METAL_DIR_UP, placement.up); 335 | Bit::set(&mut upc, Bit::METAL_DIR_RIGHT, placement.right); 336 | Bit::set(&mut upc, Bit::METAL_DIR_DOWN, placement.down); 337 | Bit::set(&mut upc, Bit::METAL_DIR_LEFT, placement.left); 338 | } 339 | 340 | match cell.si { 341 | Silicon::NP { is_n, placement } => { 342 | if is_n { 343 | upc.set_bit(Bit::SI_N); 344 | } else { 345 | upc.set_bit(Bit::SI_P); 346 | } 347 | 348 | Bit::set(&mut upc, Bit::SI_DIR_UP, placement.up); 349 | Bit::set(&mut upc, Bit::SI_DIR_RIGHT, placement.right); 350 | Bit::set(&mut upc, Bit::SI_DIR_DOWN, placement.down); 351 | Bit::set(&mut upc, Bit::SI_DIR_LEFT, placement.left); 352 | } 353 | Silicon::Mosfet { 354 | is_npn, 355 | is_horizontal, 356 | gate_placement, 357 | ec_placement, 358 | } => { 359 | if is_npn { 360 | upc.set_bit(Bit::SI_N); 361 | } else { 362 | upc.set_bit(Bit::SI_P); 363 | } 364 | 365 | Bit::set(&mut upc, Bit::MOSFET_HORIZONTAL, is_horizontal); 366 | Bit::set(&mut upc, Bit::MOSFET_VERTICAL, !is_horizontal); 367 | 368 | Bit::set( 369 | &mut upc, 370 | Bit::SI_DIR_UP, 371 | ec_placement.up || gate_placement.up, 372 | ); 373 | Bit::set( 374 | &mut upc, 375 | Bit::SI_DIR_RIGHT, 376 | ec_placement.right || gate_placement.right, 377 | ); 378 | Bit::set( 379 | &mut upc, 380 | Bit::SI_DIR_DOWN, 381 | ec_placement.down || gate_placement.down, 382 | ); 383 | Bit::set( 384 | &mut upc, 385 | Bit::SI_DIR_LEFT, 386 | ec_placement.left || gate_placement.left, 387 | ); 388 | } 389 | Silicon::None => {} 390 | } 391 | 392 | upc 393 | } 394 | } 395 | 396 | /// Represents the various placements of Metal and Si within a Cell, including the 4 cardinal 397 | /// directions, and the center "self" location (which is implicit when any cardinal direction is 398 | /// set, but can also stand alone). 399 | #[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] 400 | #[wasm_bindgen] 401 | pub struct Placement { 402 | pub up: bool, 403 | pub right: bool, 404 | pub down: bool, 405 | pub left: bool, 406 | } 407 | 408 | impl Placement { 409 | pub const NONE: Self = Self { 410 | up: false, 411 | right: false, 412 | down: false, 413 | left: false, 414 | }; 415 | 416 | pub const CENTER: Self = Self { 417 | up: false, 418 | right: false, 419 | down: false, 420 | left: false, 421 | }; 422 | 423 | pub fn from_cardinal(dir: IVec2) -> Self { 424 | let mut placement = Placement::NONE; 425 | placement.set_cardinal(dir); 426 | placement 427 | } 428 | 429 | pub fn cardinal_vectors(&self) -> ArrayVec { 430 | let mut vec = ArrayVec::<_, 4>::new(); 431 | 432 | if self.up { 433 | vec.push(IVec2::Y) 434 | } 435 | if self.right { 436 | vec.push(IVec2::X) 437 | } 438 | if self.down { 439 | vec.push(-IVec2::Y) 440 | } 441 | if self.left { 442 | vec.push(-IVec2::X) 443 | } 444 | 445 | vec 446 | } 447 | 448 | pub fn set_cardinal(&mut self, dir: IVec2) { 449 | match (dir.x, dir.y) { 450 | (0, 1) => self.up = true, 451 | (1, 0) => self.right = true, 452 | (0, -1) => self.down = true, 453 | (-1, 0) => self.left = true, 454 | _ => panic!("Non-unit cardinal direction vector"), 455 | } 456 | } 457 | 458 | pub fn clear_cardinal(&mut self, dir: IVec2) { 459 | match (dir.x, dir.y) { 460 | (0, 1) => self.up = false, 461 | (1, 0) => self.right = false, 462 | (0, -1) => self.down = false, 463 | (-1, 0) => self.left = false, 464 | _ => panic!("Non-unit cardinal direction vector"), 465 | } 466 | } 467 | 468 | pub fn has_cardinal(&self, dir: IVec2) -> bool { 469 | match (dir.x, dir.y) { 470 | (0, 1) => self.up, 471 | (1, 0) => self.right, 472 | (0, -1) => self.down, 473 | (-1, 0) => self.left, 474 | _ => panic!("Non-unit cardinal direction vector"), 475 | } 476 | } 477 | } 478 | 479 | impl ops::BitOr for Placement { 480 | type Output = Placement; 481 | 482 | fn bitor(self, rhs: Self) -> Self::Output { 483 | Self::Output { 484 | left: self.left | rhs.left, 485 | up: self.up | rhs.up, 486 | right: self.right | rhs.right, 487 | down: self.down | rhs.down, 488 | } 489 | } 490 | } 491 | -------------------------------------------------------------------------------- /src/utils/convert.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use base64::{engine::general_purpose::STANDARD, Engine}; 4 | use serde::{Deserialize, Serialize}; 5 | use wasm_bindgen::prelude::*; 6 | 7 | use crate::{coords::ChunkCoord, substrate::buffer::Buffer, upc::UPC}; 8 | 9 | #[derive(Serialize, Deserialize)] 10 | struct Blueprint { 11 | chunks: HashMap, 12 | #[allow(dead_code)] 13 | #[serde(skip)] 14 | modules: JsValue, 15 | } 16 | 17 | #[derive(bincode::Decode)] 18 | struct Cell { 19 | upc_idx: u16, 20 | flags_1: u8, 21 | flags_2: u8, 22 | } 23 | 24 | /// Convert a legacy blueprint JSON file into a Buffer (which can then be saved into the latest 25 | /// format). Does not support modules, only the substrate is loaded. 26 | #[wasm_bindgen] 27 | pub fn import_legacy_blueprint(json_str: String) -> Result { 28 | let blueprint: Blueprint = 29 | serde_json::from_str(&json_str).map_err(|e| JsValue::from_str(&e.to_string()))?; 30 | 31 | let mut buffer = Buffer::default(); 32 | 33 | for (chunk_coord, cells) in blueprint.chunks { 34 | let cells: Vec = { 35 | if let Ok(bin) = STANDARD.decode(cells) { 36 | if let Ok((cells, _)) = bincode::decode_from_slice(&bin, bincode::config::legacy()) 37 | { 38 | cells 39 | } else { 40 | continue; 41 | } 42 | } else { 43 | continue; 44 | } 45 | }; 46 | 47 | for cell in cells.iter() { 48 | // Chunks were 32 wide originally. We need to convert to cell coord first, as chunks 49 | // are larger. 50 | // Local coords 51 | let x = (cell.upc_idx & (32 - 1)) as i32; 52 | let y = (cell.upc_idx >> 5) as i32; 53 | // Cell coors, converted from the old 32x32 chunks 54 | let c_x = (chunk_coord.0.x << 5) + x; 55 | let c_y = (chunk_coord.0.y << 5) + y; 56 | 57 | buffer.set_cell( 58 | (c_x, c_y).into(), 59 | UPC::from_slice(&[cell.flags_1, cell.flags_2, 0, 0]), 60 | ); 61 | } 62 | } 63 | 64 | Ok(buffer) 65 | } 66 | -------------------------------------------------------------------------------- /src/utils/iter.rs: -------------------------------------------------------------------------------- 1 | pub struct RangeIterator { 2 | i: i32, 3 | end: i32, 4 | step: i32, 5 | } 6 | 7 | impl Iterator for RangeIterator { 8 | type Item = i32; 9 | 10 | fn next(&mut self) -> Option { 11 | if self.i == self.end { 12 | return None; 13 | } 14 | 15 | let i = self.i; 16 | self.i += self.step; 17 | 18 | Some(i) 19 | } 20 | } 21 | 22 | pub fn range_iter(from: i32, to: i32) -> RangeIterator { 23 | RangeIterator { 24 | i: from, 25 | end: to, 26 | step: if from < to { 1 } else { -1 }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/log.rs: -------------------------------------------------------------------------------- 1 | extern crate web_sys; 2 | 3 | // Note: the cfg target_arch is needed for local test paths, non-web targets will never be supported 4 | // for the built binary (web-specific APIs are used heavily). 5 | 6 | // A macro to provide `println!(..)`-style syntax for `console.log` logging. 7 | #[macro_export] 8 | macro_rules! log { 9 | ( $( $t:tt )* ) => { 10 | #[cfg(target_arch = "wasm32")] 11 | web_sys::console::log_1(&format!( $( $t )* ).into()); 12 | 13 | #[cfg(not(target_arch = "wasm32"))] 14 | println!($( $t )*); 15 | } 16 | } 17 | 18 | #[macro_export] 19 | macro_rules! warn { 20 | ( $( $t:tt )* ) => { 21 | #[cfg(target_arch = "wasm32")] 22 | web_sys::console::warn_1(&format!( $( $t )* ).into()); 23 | 24 | #[cfg(not(target_arch = "wasm32"))] 25 | println!($( $t )*); 26 | } 27 | } 28 | 29 | #[macro_export] 30 | macro_rules! error { 31 | ( $( $t:tt )* ) => { 32 | #[cfg(target_arch = "wasm32")] 33 | web_sys::console::error_1(&format!( $( $t )* ).into()); 34 | 35 | #[cfg(not(target_arch = "wasm32"))] 36 | println!($( $t )*); 37 | } 38 | } 39 | 40 | #[macro_export] 41 | macro_rules! unwrap_or_return { 42 | ($e:expr) => { 43 | match $e { 44 | Some(x) => x, 45 | None => { 46 | return; 47 | } 48 | } 49 | }; 50 | } 51 | 52 | #[macro_export] 53 | macro_rules! result_or_log_and_return { 54 | ($e:expr) => { 55 | match $e { 56 | Ok(x) => x, 57 | Err(e) => { 58 | log!("{:#?}", e); 59 | return; 60 | } 61 | } 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub use iter::*; 2 | pub use selection::*; 3 | pub mod convert; 4 | 5 | mod iter; 6 | mod log; 7 | pub mod names; 8 | mod selection; 9 | -------------------------------------------------------------------------------- /src/utils/names.rs: -------------------------------------------------------------------------------- 1 | /// Takes in a name and makes it unique against the `existing` names by either incrementing an 2 | /// existing number at the end of the name (if provided) or appending 1 to the end and incrementing 3 | /// that until the name is unique. 4 | pub fn make_name_unique(name: String, existing: Vec) -> String { 5 | if !existing.contains(&name) { 6 | return name; 7 | } 8 | 9 | let (name, range_start) = if let Some(index) = name.rfind(|c: char| c.is_ascii_digit()) { 10 | // Text ends in digits, so increment those. 11 | ( 12 | name[0..index].to_string(), 13 | name[index..].parse::().unwrap(), 14 | ) 15 | } else { 16 | // Otherwise start the range at 1 17 | (name, 1_usize) 18 | }; 19 | 20 | let mut unique_name = name.clone(); 21 | 22 | for i in range_start.. { 23 | unique_name = format!("{}{}", name, i); 24 | 25 | if !existing.contains(&unique_name) { 26 | // Found a unique name 27 | break; 28 | } 29 | } 30 | unique_name 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/selection.rs: -------------------------------------------------------------------------------- 1 | use glam::IVec2; 2 | use serde::{Deserialize, Serialize}; 3 | use wasm_bindgen::prelude::*; 4 | 5 | use crate::coords::CellCoord; 6 | 7 | #[derive(Clone, Copy, Serialize, Deserialize)] 8 | #[wasm_bindgen] 9 | pub struct Selection { 10 | // The lower left point in the selection, inclusively. 11 | pub lower_left: CellCoord, 12 | 13 | // The lower left point in the selection, exclusively (ie row/col not included). 14 | pub upper_right: CellCoord, 15 | } 16 | 17 | impl Selection { 18 | pub fn from_rectangle_inclusive(first_point: TF, second_point: TS) -> Self 19 | where 20 | TF: Into, 21 | TS: Into, 22 | { 23 | let first_point: CellCoord = first_point.into(); 24 | let second_point: CellCoord = second_point.into(); 25 | 26 | let lower_left = CellCoord(first_point.0.min(second_point.0)); 27 | let upper_right = CellCoord(first_point.0.max(second_point.0) + IVec2::ONE); 28 | 29 | Self { 30 | lower_left, 31 | upper_right, 32 | } 33 | } 34 | 35 | #[inline(always)] 36 | pub fn test_cell_in_selection(&self, point: T) -> bool 37 | where 38 | T: Into, 39 | { 40 | let cell_coord: CellCoord = point.into(); 41 | 42 | cell_coord.0.x >= self.lower_left.0.x 43 | && cell_coord.0.y >= self.lower_left.0.y 44 | && cell_coord.0.x < self.upper_right.0.x 45 | && cell_coord.0.y < self.upper_right.0.y 46 | } 47 | 48 | pub fn is_zero(&self) -> bool { 49 | let diff = self.upper_right.0 - self.lower_left.0; 50 | diff.x <= 0 && diff.y <= 0 51 | } 52 | } 53 | 54 | impl Default for Selection { 55 | fn default() -> Self { 56 | Self { 57 | lower_left: (0, 0).into(), 58 | upper_right: (0, 0).into(), 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/viewport.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use glam::Vec2; 4 | use wasm_bindgen::prelude::*; 5 | use web_sys::{HtmlCanvasElement, WebGl2RenderingContext}; 6 | 7 | use crate::{ 8 | coords::ChunkCoord, 9 | project::Project, 10 | wgl2::{Camera, CellProgram, QuadVao, SetUniformType, Texture}, 11 | }; 12 | 13 | /// Represents only the presentation state of an on or off screen viewport for rendering. 14 | #[wasm_bindgen] 15 | pub struct Viewport { 16 | program: CellProgram, 17 | render_chunks: HashMap, 18 | canvas: HtmlCanvasElement, 19 | gl: WebGl2RenderingContext, 20 | } 21 | 22 | struct RenderChunk { 23 | cell_texture: Texture, 24 | mask_texture: Texture, 25 | vao: QuadVao, 26 | } 27 | 28 | #[wasm_bindgen] 29 | impl Viewport { 30 | #[wasm_bindgen(constructor)] 31 | pub fn new(canvas: HtmlCanvasElement) -> Self { 32 | let options = js_sys::Object::new(); 33 | 34 | let gl = canvas 35 | .get_context_with_context_options("webgl2", &options) 36 | .expect("get webgl2 context from canvas") 37 | .expect("webgl2 context to be non-null") 38 | .dyn_into::() 39 | .expect("cast get_context_with_context_options into WebGL2RenderingContext"); 40 | 41 | let program = CellProgram::compile(&gl).expect("glsl programs to compile"); 42 | 43 | Self { 44 | render_chunks: Default::default(), 45 | program, 46 | canvas, 47 | gl, 48 | } 49 | } 50 | 51 | pub fn draw(&mut self, camera: &mut Camera, editor: &Project) -> Result<(), JsValue> { 52 | let time: f64 = web_sys::window() 53 | .expect("should have a window in this context") 54 | .performance() 55 | .expect("performance should be available") 56 | .now() 57 | / 1000.0; 58 | 59 | // Maintain HTML Canvas size and context viewport. 60 | let w = self.canvas.client_width() as u32; 61 | let h = self.canvas.client_height() as u32; 62 | 63 | if w != self.canvas.width() || h != self.canvas.height() { 64 | self.canvas.set_width(w); 65 | self.canvas.set_height(h); 66 | } 67 | 68 | camera.update((w as f32, h as f32).into()); 69 | 70 | self.gl 71 | .viewport(0, 0, camera.size.x as i32, camera.size.y as i32); 72 | 73 | // Update per-draw uniforms 74 | self.program.use_program(&self.gl); 75 | self.program 76 | .view_proj 77 | .set(&self.gl, camera.get_view_proj_matrix()); 78 | self.program.time.set(&self.gl, time as f32); 79 | self.program 80 | .cell_select_ll 81 | .set(&self.gl, editor.selection.lower_left.0); 82 | self.program 83 | .cell_select_ur 84 | .set(&self.gl, editor.selection.upper_right.0); 85 | self.program.cursor_coord.set( 86 | &self.gl, 87 | editor 88 | .cursor_coord 89 | .map(|c| c.0) 90 | .unwrap_or((i32::MIN, i32::MIN).into()), 91 | ); 92 | 93 | // Compute the lower-left and upper-right chunk coords visible to the camera. 94 | let ll: ChunkCoord = camera 95 | .project_screen_point_to_cell(Vec2::new(-1.0, camera.size.y + 1.0)) 96 | .into(); 97 | let ur: ChunkCoord = camera 98 | .project_screen_point_to_cell(Vec2::new(camera.size.x + 1.0, -1.0)) 99 | .into(); 100 | let width = ur.0.x - ll.0.x; 101 | let height = ur.0.y - ll.0.y; 102 | 103 | // Render the background VAO. 104 | let vao = QuadVao::new(&self.gl, &self.program, &ll, width.max(height) as u32 + 1)?; 105 | // Bind empty cell and mask textures 106 | self.gl.active_texture(WebGl2RenderingContext::TEXTURE0); 107 | self.gl 108 | .bind_texture(WebGl2RenderingContext::TEXTURE_2D, None); 109 | 110 | self.gl.active_texture(WebGl2RenderingContext::TEXTURE1); 111 | self.gl 112 | .bind_texture(WebGl2RenderingContext::TEXTURE_2D, None); 113 | 114 | self.program 115 | .chunk_start_cell_offset 116 | .set(&self.gl, ll.first_cell_coord().0); 117 | 118 | // Bind the VAO and draw 119 | vao.bind(); 120 | self.gl.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, 6); 121 | 122 | // Then render all non-empty chunks 123 | for chunk in &editor.buffer.chunks { 124 | // Frustum cull chunks that aren't visible 125 | let coord = chunk.chunk_coord; 126 | if coord.0.x < ll.0.x || coord.0.x > ur.0.x || coord.0.y < ll.0.y || coord.0.y > ur.0.y 127 | { 128 | continue; 129 | } 130 | 131 | let render_chunk = if let Some(crc) = self.render_chunks.get_mut(&coord) { 132 | crc 133 | } else { 134 | let vao = QuadVao::new(&self.gl, &self.program, &coord, 1)?; 135 | let cell_texture = Texture::new_chunk_texture(&self.gl)?; 136 | let mask_texture = Texture::new_chunk_texture(&self.gl)?; 137 | self.render_chunks.insert( 138 | coord.clone(), 139 | RenderChunk { 140 | cell_texture, 141 | mask_texture, 142 | vao, 143 | }, 144 | ); 145 | self.render_chunks.get_mut(&coord).unwrap() 146 | }; 147 | 148 | // Update and bind the cell texture. 149 | self.gl.active_texture(WebGl2RenderingContext::TEXTURE0); 150 | 151 | // Blit cells 152 | render_chunk 153 | .cell_texture 154 | .set_pixels(&chunk.get_cells()[..])?; 155 | 156 | // Bind the render chunk's texture as it's non-empty. 157 | render_chunk.cell_texture.bind(); 158 | 159 | // Update and bind the mask texture. 160 | self.gl.active_texture(WebGl2RenderingContext::TEXTURE1); 161 | 162 | if let Some(mask_chunk) = editor.mask.get_chunk(coord) { 163 | render_chunk 164 | .mask_texture 165 | .set_pixels(&mask_chunk.cells[..])?; 166 | 167 | // Bind the mask chunk's texture as it's non-empty. 168 | render_chunk.mask_texture.bind(); 169 | } else { 170 | // Bind the empty texture. 171 | self.gl 172 | .bind_texture(WebGl2RenderingContext::TEXTURE_2D, None); 173 | } 174 | 175 | // Update the per-chunk program uniforms 176 | self.program 177 | .chunk_start_cell_offset 178 | .set(&self.gl, coord.first_cell_coord().0); 179 | 180 | // Bind the VAO and draw 181 | render_chunk.vao.bind(); 182 | self.gl.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, 6); 183 | } 184 | 185 | Ok(()) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/wgl2/camera.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use glam::{IVec2, Mat4, Quat, Vec2, Vec3, Vec3Swizzles}; 4 | use serde::{Deserialize, Serialize}; 5 | use wasm_bindgen::prelude::*; 6 | 7 | use crate::coords::{CellCoord, ChunkCoord, CHUNK_SIZE}; 8 | 9 | #[derive(Serialize, Deserialize, Clone, Copy)] 10 | #[wasm_bindgen] 11 | pub struct Camera { 12 | pub translation: Vec2, 13 | pub scale: f32, 14 | pub size: Vec2, 15 | #[wasm_bindgen(skip)] 16 | pub proj_matrix: Mat4, 17 | drag_world_anchor: Option, 18 | } 19 | 20 | impl PartialEq for Camera { 21 | fn eq(&self, other: &Self) -> bool { 22 | self.translation 23 | .abs_diff_eq(other.translation, f32::EPSILON) 24 | && self.size.abs_diff_eq(other.size, f32::EPSILON) 25 | && f32::abs(self.scale - other.scale) < f32::EPSILON 26 | } 27 | } 28 | 29 | impl Default for Camera { 30 | fn default() -> Self { 31 | let mut camera = Self { 32 | translation: Vec2::ZERO, 33 | scale: 1.0, 34 | size: Vec2::ONE, 35 | proj_matrix: Default::default(), 36 | drag_world_anchor: None, 37 | }; 38 | 39 | camera.update_proj_matrix(); 40 | camera 41 | } 42 | } 43 | 44 | // Non-bound methods 45 | impl Camera { 46 | pub fn update(&mut self, size: Vec2) { 47 | if !self.size.abs_diff_eq(size, f32::EPSILON) { 48 | self.size = size; 49 | self.update_proj_matrix(); 50 | } 51 | } 52 | 53 | pub fn get_view_proj_matrix(&self) -> Mat4 { 54 | self.proj_matrix * self.get_view_matrix().inverse() 55 | } 56 | 57 | pub fn get_view_matrix(&self) -> Mat4 { 58 | Mat4::from_scale_rotation_translation( 59 | Vec3::ONE * self.scale, 60 | Quat::IDENTITY, 61 | Vec3::new(self.translation.x, self.translation.y, 0.0), 62 | ) 63 | } 64 | 65 | fn update_proj_matrix(&mut self) { 66 | let scale = 2000.0; 67 | let w = self.size.x / scale; 68 | let h = self.size.y / scale; 69 | self.proj_matrix = Mat4::orthographic_rh(-w, w, -h, h, 0.0, 1.0); 70 | } 71 | } 72 | 73 | #[wasm_bindgen] 74 | impl Camera { 75 | #[wasm_bindgen(constructor)] 76 | pub fn new_translation_scale(translation: Vec2, scale: f32) -> Self { 77 | let mut camera = Self { 78 | translation, 79 | scale, 80 | size: Vec2::ONE, 81 | proj_matrix: Default::default(), 82 | drag_world_anchor: None, 83 | }; 84 | 85 | camera.update_proj_matrix(); 86 | camera 87 | } 88 | 89 | /// Project a screen x,y point into the world. Z axis is ignored because I don't need it. 90 | pub fn project_screen_point_to_world(&self, position: Vec2) -> Vec2 { 91 | let view_matrix = self.get_view_matrix(); 92 | 93 | // Normalized device coordinate cursor position from (-1, -1, -1) to (1, 1, 1). The Y axis 94 | // is flipped (in HTML Y=0 is the top). 95 | let mut cursor_ndc = (position / self.size) * 2.0 - Vec2::ONE; 96 | cursor_ndc.y = -cursor_ndc.y; 97 | let cursor_pos_ndc_near: Vec3 = cursor_ndc.extend(-1.0); 98 | let cursor_pos_ndc_far: Vec3 = cursor_ndc.extend(1.0); 99 | 100 | // Use near and far ndc points to generate a ray in world space 101 | let ndc_to_world: Mat4 = view_matrix * self.proj_matrix.inverse(); 102 | let cursor_pos_near: Vec3 = ndc_to_world.project_point3(cursor_pos_ndc_near); 103 | let cursor_pos_far: Vec3 = ndc_to_world.project_point3(cursor_pos_ndc_far); 104 | let ray_direction = cursor_pos_far - cursor_pos_near; 105 | 106 | // Leaving this in just incase I care about distance some day -shrug- 107 | let distance = 1.0; 108 | let point = cursor_pos_near + (ray_direction * distance); 109 | 110 | point.xy() 111 | } 112 | 113 | /// Project a screen point to a cell location. It's the caller's responsibility to ensure the 114 | /// point is within the visible bounds of the window. 115 | pub fn project_screen_point_to_cell(&self, position: Vec2) -> CellCoord { 116 | let world_point = self.project_screen_point_to_world(position); 117 | 118 | // A single chunk is always 1.0 x 1.0 in world coords, and has CHUNK_SIZE x CHUNK_SIZE cells 119 | // in it. Aka, there are CHUNK_SIZE cells per world-space unit. This makes the math pretty 120 | // easy from world-space then. 121 | CellCoord((world_point * CHUNK_SIZE as f32).floor().as_ivec2()) 122 | } 123 | 124 | pub fn project_cell_coord_to_screen_point(&self, coord: CellCoord) -> Vec2 { 125 | let vec = coord.0.as_vec2() / CHUNK_SIZE as f32; 126 | let p = self 127 | .get_view_proj_matrix() 128 | .project_point3(Vec3::new(vec.x, vec.y, 0.0)); 129 | let half_size = self.size / 2.0; 130 | (Vec2::new(p.x, -p.y) * half_size) + half_size 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/wgl2/cell_program.rs: -------------------------------------------------------------------------------- 1 | use glam::{IVec2, Mat4}; 2 | use web_sys::{WebGl2RenderingContext, WebGlProgram, WebGlShader}; 3 | 4 | use super::{uniform::Uniform, SetUniformType}; 5 | 6 | const CELL_VERT_SRC: &str = include_str!("../shaders/cell.vert"); 7 | const CELL_FRAG_SRC: &str = include_str!("../shaders/cell.frag"); 8 | 9 | pub struct CellProgram { 10 | pub program: WebGlProgram, 11 | pub view_proj: Uniform, 12 | pub time: Uniform, 13 | pub cells_texture_sampler: Uniform, 14 | pub mask_texture_sampler: Uniform, 15 | pub attr_position: u32, 16 | pub attr_uv: u32, 17 | 18 | pub chunk_start_cell_offset: Uniform, 19 | pub cell_select_ll: Uniform, 20 | pub cell_select_ur: Uniform, 21 | pub cursor_coord: Uniform, 22 | } 23 | 24 | impl CellProgram { 25 | pub fn compile(ctx: &WebGl2RenderingContext) -> Result { 26 | let vert_shader = 27 | compile_shader(&ctx, WebGl2RenderingContext::VERTEX_SHADER, CELL_VERT_SRC)?; 28 | let frag_shader = 29 | compile_shader(&ctx, WebGl2RenderingContext::FRAGMENT_SHADER, CELL_FRAG_SRC)?; 30 | let program: WebGlProgram = link_program(&ctx, &vert_shader, &frag_shader)?; 31 | ctx.use_program(Some(&program)); 32 | let attr_position = ctx.get_attrib_location(&program, "position") as u32; 33 | let attr_uv = ctx.get_attrib_location(&program, "uv") as u32; 34 | 35 | let cells_texture_sampler = Uniform::new(&ctx, &program, "cells_texture_sampler"); 36 | let mask_texture_sampler = Uniform::new(&ctx, &program, "mask_texture_sampler"); 37 | let chunk_start_cell_offset = Uniform::new(&ctx, &program, "chunk_start_cell_offset"); 38 | let cell_select_ll = Uniform::new(&ctx, &program, "cell_select_ll"); 39 | let cell_select_ur = Uniform::new(&ctx, &program, "cell_select_ur"); 40 | let cursor_coord = Uniform::new(&ctx, &program, "cursor_coord"); 41 | 42 | chunk_start_cell_offset.set(&ctx, IVec2::new(0, 0)); 43 | cell_select_ll.set(&ctx, IVec2::new(0, 0)); 44 | cell_select_ur.set(&ctx, IVec2::new(0, 0)); 45 | cursor_coord.set(&ctx, IVec2::new(0, 0)); 46 | 47 | // Set defaults for texture samplers 48 | cells_texture_sampler.set(&ctx, 0); 49 | mask_texture_sampler.set(&ctx, 1); 50 | 51 | Ok(Self { 52 | program: program.clone(), 53 | view_proj: Uniform::new(&ctx, &program, "view_proj"), 54 | time: Uniform::new(&ctx, &program, "time"), 55 | cells_texture_sampler, 56 | mask_texture_sampler, 57 | attr_position, 58 | attr_uv, 59 | chunk_start_cell_offset, 60 | cell_select_ll, 61 | cell_select_ur, 62 | cursor_coord, 63 | }) 64 | } 65 | 66 | pub fn use_program(&self, ctx: &WebGl2RenderingContext) { 67 | ctx.use_program(Some(&self.program)); 68 | } 69 | } 70 | 71 | fn compile_shader( 72 | context: &WebGl2RenderingContext, 73 | shader_type: u32, 74 | source: &str, 75 | ) -> Result { 76 | let shader = context 77 | .create_shader(shader_type) 78 | .ok_or_else(|| String::from("Unable to create shader object"))?; 79 | context.shader_source(&shader, source); 80 | context.compile_shader(&shader); 81 | 82 | if context 83 | .get_shader_parameter(&shader, WebGl2RenderingContext::COMPILE_STATUS) 84 | .as_bool() 85 | .unwrap_or(false) 86 | { 87 | Ok(shader) 88 | } else { 89 | Err(context 90 | .get_shader_info_log(&shader) 91 | .unwrap_or_else(|| String::from("Unknown error creating shader"))) 92 | } 93 | } 94 | 95 | fn link_program( 96 | context: &WebGl2RenderingContext, 97 | vert_shader: &WebGlShader, 98 | frag_shader: &WebGlShader, 99 | ) -> Result { 100 | let program = context 101 | .create_program() 102 | .ok_or_else(|| String::from("Unable to create shader object"))?; 103 | 104 | context.attach_shader(&program, vert_shader); 105 | context.attach_shader(&program, frag_shader); 106 | context.link_program(&program); 107 | 108 | if context 109 | .get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS) 110 | .as_bool() 111 | .unwrap_or(false) 112 | { 113 | Ok(program) 114 | } else { 115 | Err(context 116 | .get_program_info_log(&program) 117 | .unwrap_or_else(|| String::from("Unknown error creating program object"))) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/wgl2/mod.rs: -------------------------------------------------------------------------------- 1 | pub use camera::Camera; 2 | pub use cell_program::CellProgram; 3 | pub use quad_vao::QuadVao; 4 | pub use texture::*; 5 | pub use uniform::*; 6 | 7 | mod camera; 8 | mod cell_program; 9 | mod quad_vao; 10 | mod texture; 11 | mod uniform; 12 | -------------------------------------------------------------------------------- /src/wgl2/quad_vao.rs: -------------------------------------------------------------------------------- 1 | use glam::UVec2; 2 | use web_sys::{WebGl2RenderingContext, WebGlBuffer, WebGlVertexArrayObject}; 3 | 4 | use crate::coords::{ChunkCoord, CHUNK_SIZE}; 5 | 6 | use super::CellProgram; 7 | 8 | pub struct QuadVao { 9 | ctx: WebGl2RenderingContext, 10 | vao: WebGlVertexArrayObject, 11 | buffers: (WebGlBuffer, WebGlBuffer), 12 | } 13 | 14 | impl QuadVao { 15 | pub fn new( 16 | ctx: &WebGl2RenderingContext, 17 | program: &CellProgram, 18 | chunk_coord: &ChunkCoord, 19 | // The number of CHUNKs (both width and height) this VAO should span. 20 | size: u32, 21 | ) -> Result { 22 | let l = chunk_coord.0.as_vec2(); 23 | let s = size as f32; 24 | let vertices: [f32; 12] = [ 25 | l.x, 26 | l.y, 27 | l.x + s, 28 | l.y, 29 | l.x + s, 30 | l.y + s, 31 | l.x, 32 | l.y, 33 | l.x + s, 34 | l.y + s, 35 | l.x, 36 | l.y + s, 37 | ]; 38 | 39 | let c = (size * CHUNK_SIZE as u32) as f32; 40 | let uvs: [f32; 12] = [0.0, 0.0, c, 0.0, c, c, 0.0, 0.0, c, c, 0.0, c]; 41 | 42 | let vao = ctx 43 | .create_vertex_array() 44 | .ok_or("Could not create vertex array object")?; 45 | ctx.bind_vertex_array(Some(&vao)); 46 | 47 | // Positions buffer. 48 | let position_buffer = ctx.create_buffer().ok_or("Failed to create buffer")?; 49 | ctx.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&position_buffer)); 50 | 51 | unsafe { 52 | let position_array_buf_view = js_sys::Float32Array::view(&vertices); 53 | ctx.buffer_data_with_array_buffer_view( 54 | WebGl2RenderingContext::ARRAY_BUFFER, 55 | &position_array_buf_view, 56 | WebGl2RenderingContext::STATIC_DRAW, 57 | ); 58 | } 59 | 60 | ctx.enable_vertex_attrib_array(program.attr_position); 61 | ctx.vertex_attrib_pointer_with_i32( 62 | program.attr_position, 63 | 2, 64 | WebGl2RenderingContext::FLOAT, 65 | false, 66 | 0, 67 | 0, 68 | ); 69 | 70 | // UV buffer. 71 | let uv_buffer = ctx.create_buffer().ok_or("Failed to create buffer")?; 72 | ctx.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&uv_buffer)); 73 | 74 | unsafe { 75 | let uv_array_buf_view = js_sys::Float32Array::view(&uvs); 76 | ctx.buffer_data_with_array_buffer_view( 77 | WebGl2RenderingContext::ARRAY_BUFFER, 78 | &uv_array_buf_view, 79 | WebGl2RenderingContext::STATIC_DRAW, 80 | ); 81 | } 82 | 83 | ctx.enable_vertex_attrib_array(program.attr_uv); 84 | ctx.vertex_attrib_pointer_with_i32( 85 | program.attr_uv, 86 | 2, 87 | WebGl2RenderingContext::FLOAT, 88 | false, 89 | 0, 90 | 0, 91 | ); 92 | 93 | Ok(Self { 94 | ctx: ctx.clone(), 95 | vao, 96 | buffers: (position_buffer, uv_buffer), 97 | }) 98 | } 99 | 100 | pub fn bind(&self) { 101 | self.ctx.bind_vertex_array(Some(&self.vao)); 102 | } 103 | } 104 | 105 | impl Drop for QuadVao { 106 | fn drop(&mut self) { 107 | self.ctx.delete_buffer(Some(&self.buffers.0)); 108 | self.ctx.delete_buffer(Some(&self.buffers.1)); 109 | self.ctx.delete_vertex_array(Some(&self.vao)); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/wgl2/texture.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::JsValue; 2 | use web_sys::{WebGl2RenderingContext, WebGlTexture}; 3 | 4 | use crate::coords::CHUNK_SIZE; 5 | 6 | pub struct Texture { 7 | texture: WebGlTexture, 8 | gl: WebGl2RenderingContext, 9 | } 10 | 11 | impl Texture { 12 | pub fn new_chunk_texture(gl: &WebGl2RenderingContext) -> Result { 13 | let texture = gl.create_texture().expect("Cannot create gl texture"); 14 | gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&texture)); 15 | 16 | // Integer texture types require NEAREST filtering. Also clamp to texture edges. 17 | gl.tex_parameteri( 18 | WebGl2RenderingContext::TEXTURE_2D, 19 | WebGl2RenderingContext::TEXTURE_WRAP_S, 20 | WebGl2RenderingContext::CLAMP_TO_EDGE as i32, 21 | ); 22 | gl.tex_parameteri( 23 | WebGl2RenderingContext::TEXTURE_2D, 24 | WebGl2RenderingContext::TEXTURE_WRAP_T, 25 | WebGl2RenderingContext::CLAMP_TO_EDGE as i32, 26 | ); 27 | gl.tex_parameteri( 28 | WebGl2RenderingContext::TEXTURE_2D, 29 | WebGl2RenderingContext::TEXTURE_MIN_FILTER, 30 | WebGl2RenderingContext::NEAREST as i32, 31 | ); 32 | gl.tex_parameteri( 33 | WebGl2RenderingContext::TEXTURE_2D, 34 | WebGl2RenderingContext::TEXTURE_MAG_FILTER, 35 | WebGl2RenderingContext::NEAREST as i32, 36 | ); 37 | 38 | gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array( 39 | WebGl2RenderingContext::TEXTURE_2D, 40 | 0, 41 | WebGl2RenderingContext::RGBA8UI as i32, 42 | CHUNK_SIZE as i32, 43 | CHUNK_SIZE as i32, 44 | 0, 45 | WebGl2RenderingContext::RGBA_INTEGER, 46 | WebGl2RenderingContext::UNSIGNED_BYTE, 47 | None, 48 | )?; 49 | 50 | Ok(Self { 51 | texture, 52 | gl: gl.clone(), 53 | }) 54 | } 55 | 56 | pub fn bind(&self) { 57 | self.gl 58 | .bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&self.texture)); 59 | } 60 | 61 | pub fn set_pixels(&self, pixels: &[u8]) -> Result<(), JsValue> { 62 | self.bind(); 63 | self.gl 64 | .tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array( 65 | WebGl2RenderingContext::TEXTURE_2D, 66 | 0, 67 | WebGl2RenderingContext::RGBA8UI as i32, 68 | CHUNK_SIZE as i32, 69 | CHUNK_SIZE as i32, 70 | 0, 71 | WebGl2RenderingContext::RGBA_INTEGER, 72 | WebGl2RenderingContext::UNSIGNED_BYTE, 73 | Some(pixels), 74 | ) 75 | } 76 | } 77 | 78 | impl Drop for Texture { 79 | fn drop(&mut self) { 80 | self.gl.delete_texture(Some(&self.texture)); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/wgl2/uniform.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use glam::{IVec2, IVec3, IVec4, Mat4, Vec2, Vec3, Vec4}; 4 | use web_sys::{WebGl2RenderingContext, WebGlProgram, WebGlUniformLocation}; 5 | 6 | use crate::warn; 7 | 8 | pub struct Uniform { 9 | location: Option, 10 | _phantom: PhantomData, 11 | } 12 | 13 | impl Uniform { 14 | pub fn new(ctx: &WebGl2RenderingContext, program: &WebGlProgram, name: &str) -> Self { 15 | let location = ctx.get_uniform_location(&program, name); 16 | 17 | if location.is_none() { 18 | warn!(r#"Failed to find uniform "{}". It will be ignored."#, name); 19 | } 20 | 21 | Self { 22 | location, 23 | _phantom: Default::default(), 24 | } 25 | } 26 | } 27 | 28 | pub trait SetUniformType { 29 | fn set(&self, ctx: &WebGl2RenderingContext, v: T); 30 | } 31 | 32 | impl SetUniformType for Uniform { 33 | fn set(&self, ctx: &WebGl2RenderingContext, v: u32) { 34 | if let Some(location) = &self.location { 35 | ctx.uniform1ui(Some(&location), v); 36 | } else { 37 | warn!("Program uniform location missing"); 38 | } 39 | } 40 | } 41 | 42 | impl SetUniformType for Uniform { 43 | fn set(&self, ctx: &WebGl2RenderingContext, v: i32) { 44 | if let Some(location) = &self.location { 45 | ctx.uniform1i(Some(&location), v); 46 | } else { 47 | warn!("Program uniform location missing"); 48 | } 49 | } 50 | } 51 | 52 | impl SetUniformType for Uniform { 53 | fn set(&self, ctx: &WebGl2RenderingContext, v: f32) { 54 | if let Some(location) = &self.location { 55 | ctx.uniform1f(Some(&location), v); 56 | } else { 57 | warn!("Program uniform location missing"); 58 | } 59 | } 60 | } 61 | 62 | impl SetUniformType for Uniform { 63 | fn set(&self, ctx: &WebGl2RenderingContext, v: Vec2) { 64 | if let Some(location) = &self.location { 65 | ctx.uniform2f(Some(&location), v.x, v.y); 66 | } else { 67 | warn!("Program uniform location missing"); 68 | } 69 | } 70 | } 71 | 72 | impl SetUniformType for Uniform { 73 | fn set(&self, ctx: &WebGl2RenderingContext, v: IVec2) { 74 | if let Some(location) = &self.location { 75 | ctx.uniform2i(Some(&location), v.x, v.y); 76 | } else { 77 | warn!("Program uniform location missing"); 78 | } 79 | } 80 | } 81 | 82 | impl SetUniformType for Uniform { 83 | fn set(&self, ctx: &WebGl2RenderingContext, v: Vec3) { 84 | if let Some(location) = &self.location { 85 | ctx.uniform3f(Some(&location), v.x, v.y, v.z); 86 | } else { 87 | warn!("Program uniform location missing"); 88 | } 89 | } 90 | } 91 | 92 | impl SetUniformType for Uniform { 93 | fn set(&self, ctx: &WebGl2RenderingContext, v: IVec3) { 94 | if let Some(location) = &self.location { 95 | ctx.uniform3i(Some(&location), v.x, v.y, v.z); 96 | } else { 97 | warn!("Program uniform location missing"); 98 | } 99 | } 100 | } 101 | 102 | impl SetUniformType for Uniform { 103 | fn set(&self, ctx: &WebGl2RenderingContext, v: Vec4) { 104 | if let Some(location) = &self.location { 105 | ctx.uniform4f(Some(&location), v.x, v.y, v.z, v.w); 106 | } else { 107 | warn!("Program uniform location missing"); 108 | } 109 | } 110 | } 111 | 112 | impl SetUniformType for Uniform { 113 | fn set(&self, ctx: &WebGl2RenderingContext, v: IVec4) { 114 | if let Some(location) = &self.location { 115 | ctx.uniform4i(Some(&location), v.x, v.y, v.z, v.w); 116 | } else { 117 | warn!("Program uniform location missing"); 118 | } 119 | } 120 | } 121 | 122 | impl SetUniformType for Uniform { 123 | fn set(&self, ctx: &WebGl2RenderingContext, v: Mat4) { 124 | if let Some(location) = &self.location { 125 | ctx.uniform_matrix4fv_with_f32_array(Some(&location), false, v.as_ref()); 126 | } else { 127 | warn!("Program uniform location missing"); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /templates/font_file.lpbp: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": { 3 | "5:0": "OwAAAAAAAAAAAIUAAQCJAAIAjAADAIUABACJAAUAjAAGAI0ABwCJAAgAgAAKAIAADACIAA4AjAAPAIUAEACBABIAjAATAIEAFgCAABcAhAAYAIkAHQCEAB4AhQAfAIEAIQCKACIAigAkAIoAJQCKACYAigAnAIoAKQCAACsAjAAsAIcALQCJAC4AhgAvAIUAMACJADIAigA1AIAAOACKADoAgAA8AIAAQQCCAEIAggBEAIIARQCCAEYAggBHAIIASACAAEoAgABLAIIATQCCAE4AhABPAIUAUACDAFIAhgBTAIEAVACAAFcAhABYAIMAWwCAAA==", 4 | "6:0": "TQAAAAAAAAADAIgABQCIAAYAjAAHAIUACACJAAkAjAAKAIUACwCBAAwAjAANAIUADgCJAA8AjAAQAIUAEQCBABIAiAAVAIwAFgCFABcAiQAYAIgAGgCIABsAhAAcAI0AHQCBAB4AhAAfAIkAIQCAACMAjgAkAIUAJQCLACYAjgAnAIUAKACLACkAigAsAIYALQCFAC4AiwAvAI4AMACFADEAgQAyAI4AMwCFADQAgQA1AIoANgCEADcAgwA4AI4AOQCFADoAiwA8AIoAPwCKAEAAgABDAIYARACFAEUAgwBGAIYARwCFAEgAgwBJAIYASgCFAEsAgQBOAIIATwCGAFAAhQBRAIEAUgCGAFMAhQBUAIEAVQCGAFYAhQBXAIEAWACCAFoAggBbAIQAXACHAF0AgQBeAIQAXwCHAA==", 5 | "4:0": "SwAAAAAAAAABAIgAAwCAAAQAjAAFAIUABgCBAAcAiAAIAIgACQCIAAoAiAAMAIgADQCMAA4AhQAPAIkAEACIABUAiAAWAIgAFwCIABkAhAAaAIUAGwCJAB0AiAAfAIwAIQCOACIAgQAkAIoAJwCKACgAigApAIoAKgCKACwAigAtAIoALwCKADAAjgAxAIUAMgCJADMAjAA0AIUANQCLADYAjgA3AIcAOACJADkAjAA6AIUAOwCDAD0AigA/AIoAQACBAEEAggBDAIAARACCAEcAhgBIAIcASQCDAEoAhgBLAIUATACDAE0AhgBOAIUATwCDAFAAhgBRAIUAUgCDAFMAhgBUAIUAVQCDAFYAhgBXAIUAWACDAFkAhgBaAIUAWwCBAFwAhABdAIcAXgCBAF8AggA=", 6 | "1:0": "OAAAAAAAAAACAIgABQCAAAsAgAANAIAAEACMABEAhQASAIkAEwCEABQAjQAVAIEAFgCMABcAhQAYAIEAGQCEABoAhQAbAIkAHgCIAB8AhAAgAIAAIQCEACIAjwAjAIEAJwCEACgAhQApAIEALgCAADAAigAyAIoANACKADYAhgA3AIUAOACJADkAhAA6AIUAOwCLADwAjAA9AIUAPgCLAD8AjABAAIAAQgCCAE8AgABQAIYAUQCFAFIAgwBTAIQAVACDAFYAhABXAIUAWACDAFkAhABaAIUAWwCDAFwAggBeAIIAXwCGAA==", 7 | "3:0": "UwAAAAAAAAAAAIwAAQCFAAIAiQADAIgABQCIAAYAjAAHAIUACACJAAkAjAAKAIUACwCBAAwAjAANAIUADgCJAA8AjAAQAIUAEQCBABIAiAAVAIwAFgCFABcAiQAYAIgAGgCIABsAhAAcAI0AHQCBAB4AhAAfAIkAIACKACIAigAjAI4AJACFACUAiwAmAI4AJwCFACgAiwApAIoALACGAC0AhQAuAIsALwCOADAAhQAxAIEAMgCOADMAhQA0AIEANQCKADYAhAA3AIMAOACOADkAhQA6AIsAPACKAD8AigBAAIYAQQCFAEIAgwBDAIYARACFAEUAgwBGAIYARwCFAEgAgwBJAIYASgCFAEsAgQBOAIIATwCGAFAAhQBRAIEAUgCGAFMAhQBUAIEAVQCGAFYAhQBXAIEAWACCAFoAggBbAIQAXACHAF0AgQBeAIQAXwCHAA==", 8 | "8:0": "PgAAAAAAAAAAAIUAAQCJAAIAjAADAIUABACJAAUAjAAGAI0ABwCJAAgAgAAKAIAADACIAA4AjAAPAIUAEACBABIAjAATAIEAFQCIABcAhAAYAIkAGwCAABwAgAAhAIoAIgCKACQAigAlAIoAJgCKACcAigApAIAAKwCMACwAhwAtAIkALgCGAC8AhQAwAIkAMQCEADIAiwA1AIoAOACOADkAgQA6AIQAOwCFADwAgQBBAIIAQgCCAEQAggBFAIIARgCCAEcAggBIAIAASgCAAEsAggBNAIIATgCEAE8AhQBQAIMAUgCGAFMAgQBVAIIAVwCEAFgAgwBaAIAAWwCAAA==", 9 | "2:0": "PAAAAAAAAAAAAIUAAQCJAAIAjAADAIUABACJAAcAiAAIAIwACQCFAAoAiQANAIgADwCAABEAhAASAIkAFgCAABcAhAAYAIUAGQCBABoAgAAeAIAAIACFACEAgwAiAI4AIwCFACQAgwAnAIoAKACOACkAhQAqAIsAKwCMACwAhQAtAIsAMgCCADUAgAA7AIAAPgCEAD8AiQBAAIUAQQCBAEIAhgBDAIUARACBAEUAhABGAIUARwCDAEgAhgBJAIUASgCDAEsAhgBMAIUATQCDAE8AgABSAIAAVgCAAFcAhABYAIUAWQCBAFoAgABdAIQAXgCFAF8AgwA=", 10 | "0:0": "OAAAAAAAAAADAIAACQCMAAoAhQALAIkADACEAA0AjQAOAIkADwCAABAAjAARAIkAEgCMABMAhQAUAIkAGQCMABoAgQAbAIQAHACJACMAiAAmAIgAJwCIACkAigArAIoALACMAC0AjwAuAIMALwCMADAAjwAxAIMAMgCKADMAgAA0AIIANgCIADkAigA8AIoAPwCAAEMAggBGAIIARwCCAEkAhgBKAIUASwCDAEwAhgBNAIcATgCBAE8AhgBQAIMAUQCAAFIAhgBTAIUAVACBAFYAggBZAIYAWgCBAFsAhABcAIMAXwCAAA==", 11 | "7:0": "SwAAAAAAAAABAIgAAwCAAAQAjAAFAIUABgCBAAcAiAAIAIgACQCIAAoAiAAMAIgADQCMAA4AhQAPAIkAEACIABUAiAAWAIgAFwCIABkAhAAaAIUAGwCJAB0AiAAfAIwAIQCOACIAgQAkAIoAJwCKACgAigApAIoAKgCKACwAigAtAIoALwCKADAAjgAxAIUAMgCJADMAjAA0AIUANQCLADYAjgA3AIcAOACJADkAjAA6AIUAOwCDAD0AigA/AIoAQACBAEEAggBDAIAARACCAEcAhgBIAIcASQCDAEoAhgBLAIUATACDAE0AhgBOAIUATwCDAFAAhgBRAIUAUgCDAFMAhgBUAIUAVQCDAFYAhgBXAIUAWACDAFkAhgBaAIUAWwCBAFwAhABdAIcAXgCBAF8AggA=" 12 | }, 13 | "modules": {} 14 | } 15 | --------------------------------------------------------------------------------