commit ffc1e47162bcdae6ef711fc9c660eba6482ba30e Author: Kevin Hamacher Date: Tue Aug 27 16:40:06 2019 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53eaa21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ae4e043 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,28 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution; +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. + +## Community Guidelines + +This project follows [Google's Open Source Community +Guidelines](https://opensource.google.com/conduct/). \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7b54f9c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "minetest_pnr" +version = "0.1.0" +authors = ["Kevin Hamacher "] +edition = "2018" + +[dependencies] +bitmap = "3.1.3" +byteorder = "1" +clap = "2.33.0" +deflate = "0.7.19" +itertools = "0.8.0" +rayon = "1.0.3" +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1", features = ["preserve_order"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..656f98c --- /dev/null +++ b/README.md @@ -0,0 +1,199 @@ +# MinetestPnR +Simple 2D placer & router for [minetest] developed as part of a [Google CTF] challenge ([video writeup](https://www.youtube.com/watch?v=nI8Q1bqT8QU)). +This also means that the project is mostly aimed towards creating this challenge, +not towards being feature complete and being well engineered, so don't expect +optimal/minimal output. + +It currently supports only basic gates (i.e. **no DFF / latches**), so I'm afraid you +can't just place your RISC-V CPU in minetest, feel free to send pull requests though ;). + +Supported gates: + - AND + - NAND + - ANDNOT + - OR + - NOR + - ORNOT + - NOT + - XNOR + - XOR + +The result will look similar to this (text version): +```Text + ░──────────────────¬─────────────┐┌───┐ ┌───────┐ ┌─────┐ ┌──┐ ┌─┐ + ░────────┬─────────¬─────────┐ ││ ^──┘ ^──┘ ^─────»──┘ ^──┘ ^────▓ + ░────────╂──────┬──¬─────────╂───╂┘┌──┘ ┌───────┘ ┌─────┘ ┌──┘ ┌─┘ + ░────────╂──────╂┬─¬─────┬───╂───╂─╂──┐ │┌──────┐ │┌────┐ │┌─┐ │ + ░──────┬─╂──────╂╂─¬─────╂───╂───╂─╂┐ ^──┘│ ^──┘│ v─────¬──┘│ ^──┘ + ░──────╂─╂─┬────╂╂─»──┐ │ │ │ │├─┘ │┌─────┘ ┌╂────┘ ┌╂─┘ + ░──────╂─╂─╂┬───╂╂─¬──╂──╂───╂──┐└─╂╂─┐ ┌╂╂─────┐ ││┌───┐ ││ + ░──────╂┐└─╂╂───╂╂─»──╂──╂───╂──╂──┤│ ^──╂┘│ v──╂┘│ ^─────»──╂┘ + ░──────╂╂──╂╂───╂╂─»──╂──╂───╂──╂──╂╂─┘ │ │┌────┘ │ │┌──┘ ┌──┐ │ + ░──────╂╂──╂╂──┬╂╂─¬──╂──╂───╂┐┌╂──╂╂─┐ │┌╂╂────┐ │┌╂╂──┐ │ ^──┘ + ░──────╂╂──╂╂─┐└╂╂─»──╂┐ │ ││││ ││ v──┘│││ v──┘│││ ^──┘┌─┘ + ░────┬─╂╂──╂╂─╂─╂╂─¬──╂╂─╂───╂╂╂╂┐┌╂╂─┘ │││┌───┘ ┌╂╂╂──┘ │ + ░────╂─╂╂──╂╂┬╂─╂╂─¬──╂╂┐│ ││││││││ ││││┌──┐ ││││┌─┐ │ + ░────╂─╂╂┐ ├╂╂╂─╂╂─┐ ││││ ││││││││ │││││ ^──╂╂┘││ ^───┘ + ░────╂─╂╂╂┐││││ ││ v──╂╂╂╂───╂╂╂╂╂╂╂╂─»───╂┘│││┌─┘ ││┌╂╂─┘ + ░────╂┐││├╂╂╂╂╂─╂╂─┘ └╂╂╂───╂╂╂╂╂╂╂╂─┐ ┌╂─╂╂╂╂─┐ │││││ + ░───┐└╂╂╂╂╂╂╂╂╂─╂╂─»───╂╂╂─┐ ││││││││ v──╂┘ ││││ ^──╂┘│││ + ░───╂─╂╂╂╂╂╂╂╂╂─╂╂─»───╂╂╂─╂─╂╂┘└╂╂╂╂─┘ │ ┌╂╂╂╂─┘ │ │││ + ░──┐│ │└╂╂╂╂╂╂╂─╂╂─»───╂╂╂─╂┐└╂──╂╂╂╂─┐ │ │││││ │ │││ + ░──╂╂─╂─╂╂╂╂╂╂╂─╂╂─¬───╂╂╂─╂╂─╂──╂┘││ v──╂─╂╂╂╂╂─»──╂─╂┘│ + │└─╂─╂╂╂╂╂╂╂─╂╂─┐ └╂╂─╂╂─╂──╂─╂╂─┘ │┌╂╂╂╂╂─┐ │ │ │ + │ │ │││││││ ││ v────╂╂─╂╂─╂──╂─╂╂─»──╂╂╂┘│││ ^──┘ │ │ + └──╂─╂╂╂╂╂╂╂─╂╂─┘ ││ ││ │ │ ││ │││┌╂╂╂─┘ │ │ + │ └╂╂╂╂╂╂─╂╂─┐ ││ ││ │ │ ││ │││││││ │ │ + │ ││││││ ││ v────╂╂─╂╂─╂──╂─╂╂─»──╂╂╂╂┘││ │ │ + └──╂╂╂╂╂╂─╂╂─┘ ││ ││ │ │ ││ ││││ ││ │ │ + ││││││ ├╂─┐ ││ ││ │ │ ││ ││││ ││ │ │ + ││││││ ││ ^────╂╂─╂╂─╂──╂─╂╂─¬──┘│││ ││ │ │ + │││││├─╂╂─┘ │└─╂╂─╂──╂─╂╂─┐ │││ ││ │ │ + ││└╂╂╂─╂╂─┐ │ ││ │ │ ││ v───╂╂╂─┘│ │ │ + ││ │││ ││ ^────╂─┐└╂─╂──╂─╂╂─┘ │││ │ │ │ + └╂─╂╂╂─╂╂─┘ │ │ │ │ │ └╂─┐ │││ │ │ │ + │ ├╂╂─╂╂─┐ │ │ │ │ │ │ v───╂╂╂──┘ │ │ + │ │││ ││ ^────╂┐│ │ └──╂──╂─┘ │││ │ │ + ├─╂╂╂─╂╂─┘ └╂╂─╂────╂──╂─┐ │││ │ │ + │ └╂╂─╂╂─┐ ││ │ │ │ v───╂┘│ │ │ + │ ││ ││ v────┐││ └────╂──╂─┘ │ │ │ │ + └──╂╂─╂╂─┘ │││ │ └─┐ │ │ │ │ + ││ └╂─┐ │││ │ v───┘ │ │ │ + ││ │ v──┐┌╂╂╂──────╂────┘ │ │ │ + │└──╂─┘ │││││ └────┐ │ │ │ + └───╂─»──╂┘│││ v─────┘ │ │ + └─»──╂─╂╂╂───────────┘ │ │ + │ ││└───────────┐ │ │ + │ ││ v──────────¬────╂─┘ + │ │└────────────┘ │ + │ └─────────────┐ │ + │ ^──────────»────┘ + └───────────────┘ +``` + +For ingame footage, look at the CTF writeup video mentioned above. + +## What does it do? +MinetestPnR takes a synthesized circuit, places the components and routes them (= wires things together). +This means that it'll allow you to write your circuit in a HDL (e.g. `verilog`), synthesize it (e.g. using `yosys`) +and then use it inside your minetest world. + +## How can I use it? +Putting your circuit in minetest takes three steps: + - Generate the minetest schematic file + - Place it in minetest + - Fix the mesecon parts. More on this below. + +### Generating minetest schematic +- Synthesize your circuit and create a json file containing the basic blocks using [yosys]. + Example command: `yosys -p 'synth; abc -g AND,OR,XOR,XNOR,ANDNOT,ORNOT; write_json schematic.json' schematic.v` +- Place & route the resulting `schematic.json` file, creating a MTS(minetest schematic) file using this project: + `cargo run --release -- ./schematic.json --write_mts schematic.mts` + +### Placing MTS in minetest using worldedit +- Install [mesecons] + [worldedit] +- Create world in minetest (`type=single node` if you only want to have the circuit in the world) +- Remember to enable both mods +- Create `schems` directory in the world directory (`worlds/${worldname}/schems`) +- Generate MTS file using the project, place it in that folder +- Start minetest + load your map +- Set pos1 (`//1` / `//fixedpos set1 0 1 0`) where the schematic should be placed +- `//mtschemplace ` to place the schematic + +### Fixing mesecon wires / gates +- Fix mesecon wires + gates - those blocks introduce some internal state that is not created when + placed using worldedit. This can be done by adding a function to the mesecons code that will + set up that internal state. This can be done by adding this function: + +```lua +local function fix_single_chunk(pos) + local found_nodes = minetest.find_nodes_in_area(pos, vector.add(pos, { x = 16, y = 16, z = 16 }), { + "mesecons_gates:diode_off", + "mesecons_gates:not_off", + "mesecons_gates:and_off", + "mesecons_gates:nand_off", + "mesecons_gates:xor_off", + "mesecons_gates:nor_off", + "mesecons_gates:or_off", + "mesecons:mesecons_off", + "mesecons:wire_00000000_off", + }) + local cnt = 0 + + for i=0, #found_nodes do + if (found_nodes[i] ~= nil) then + mesecon.on_placenode(found_nodes[i], minetest.get_node(found_nodes[i])) + cnt = cnt + 1 + end + end + + return cnt +end + +-- Adapted from: +-- https://rubenwardy.com/minetest_modding_book/en/map/environment.html +local function emerge_callback(pos, action, + num_calls_remaining, context) + -- On first call, record number of blocks + if not context.total_blocks then + context.total_blocks = num_calls_remaining + 1 + context.loaded_blocks = 0 + context.nodes_fixed = 0 + end + + context.loaded_blocks = context.loaded_blocks + 1 + local perc = 100 * context.loaded_blocks / context.total_blocks + local msg = string.format("Handling block %d/%d (%.2f%%)", + context.loaded_blocks, context.total_blocks, perc) + context.nodes_fixed = context.nodes_fixed + fix_single_chunk(pos) + minetest.chat_send_all(msg) + + -- Are we done yet? + if context.total_blocks == context.loaded_blocks then + minetest.chat_send_all("Done, " .. context.nodes_fixed .. " nodes fixed") + end +end + +minetest.register_chatcommand("fix_gates", { + params = "", + description = "Fix gates by triggering I/O reevaluation", + func = function(name, param) + local context = {} + minetest.emerge_area({x=0, y=2, z=0}, {x=2000, y=2, z=2000}, emerge_callback, context) + return true, "Emerge started" + end, +}) +``` + +e.g. at the end of `mesecons_gates/init.lua`. +Then load up the map and send `/fix_gates` in chat. +This sometimes does not seem to work reliably, so you might need to use it multiple +times when standing at different locations to cover different chunks. + +**Note that the snippet contains the area where the wires should be fixed** (`minetest.emerge_area({x=0, y=2, z=0}, {x=2000, y=2, z=2000}`) +so you might want to adjust this. + +## Caveats +### My circuit is too large! +The application uses a 8k * 8k tiles canvas where the circuit gets placed onto. +The maximum size that the MTS format supports is 64k * 64k, so depending on how +big your circuit is you can place your bigger circuit by changing the canvas +size in `src/canvas.rs`: +```rust +const CANVAS_MAX_W: usize = 8 * 1024; +const CANVAS_MAX_H: usize = 8 * 1024; +``` + +Why not using 64k * 64k by default? 64k * 64k = 4G - that's a lot of ram ;). +**NOTE**: The minetest source code is not consistent whether the dimensions are +stored as `u16` or `i16`, so the code might need additional patches in +`serialize_to_mts` if your circuit is bigger than 32k * 32k. + +## Disclaimer +This is not an officially supported Google product. + +[minetest]: http://www.minetest.net/ +[worldedit]: https://github.com/Uberi/Minetest-WorldEdit +[mesecons]: http://mesecons.net/ +[yosys]: http://www.clifford.at/yosys/ +[Google CTF]: https://capturetheflag.withgoogle.com diff --git a/src/canvas.rs b/src/canvas.rs new file mode 100644 index 0000000..f213297 --- /dev/null +++ b/src/canvas.rs @@ -0,0 +1,515 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::channel_router::{ChannelOp, ChannelSubState, WireConnection}; +use crate::CircuitType; +use byteorder::{BigEndian, WriteBytesExt}; +use deflate::write::ZlibEncoder; +use deflate::Compression; +use std::fs::File; +use std::io::{BufWriter, Write}; + +const BLOCK_IDS: &[&str; 16] = &[ + "air", + "stone", + "mesecons_lamp:lamp_off", + "mesecons_walllever:wall_lever_off", + // Gates + "mesecons_gates:and_off", + "mesecons_gates:nand_off", + "mesecons_gates:nor_off", + "mesecons_gates:not_off", + "mesecons_gates:or_off", + "mesecons_gates:xor_off", + // Regular wire + "mesecons:mesecon_off", + // Insulated wires + "mesecons_insulated:insulated_off", + "mesecons_extrawires:corner_off", + "mesecons_extrawires:tjunction_off", + "mesecons_extrawires:crossover_off", + // For constant inputs + "mesecons_torch:mesecon_torch_off", +]; + +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum CornerOrientation { + LeftUp, + LeftDown, + DownRight, + UpRight, +} + +impl CornerOrientation { + pub fn get_param2(self) -> u8 { + use self::CornerOrientation::*; + match self { + LeftUp => 0, + LeftDown => 3, + DownRight => 2, + UpRight => 1, + } + } +} + +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum TRotation { + LeftRightDown, + LeftRightUp, + RightUpDown, + LeftUpDown, +} + +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum BlockType { + Air, + WireH, + WireV, + WireCrossing, + WireT(TRotation), + WireCorner(CornerOrientation), + WireStar, + Gate(CircuitType), + + Input, + Output, + Constant, +} + +impl Default for BlockType { + fn default() -> Self { + BlockType::Air + } +} + +impl BlockType { + pub fn c(self) -> char { + use self::BlockType::*; + match self { + Air => ' ', + WireH => '─', + WireV => '│', + WireCrossing => '╂', + WireCorner(CornerOrientation::DownRight) => '┌', + WireCorner(CornerOrientation::LeftUp) => '┘', + WireCorner(CornerOrientation::LeftDown) => '┐', + WireCorner(CornerOrientation::UpRight) => '└', + WireT(TRotation::LeftRightDown) => '┬', + WireT(TRotation::LeftRightUp) => '┴', + WireT(TRotation::RightUpDown) => '├', + WireT(TRotation::LeftUpDown) => '┤', + WireStar => '┼', + + Gate(CircuitType::INPUT) => '░', + Gate(CircuitType::FORWARD) => '»', + Gate(CircuitType::NOT) => '¬', + Gate(CircuitType::OR) => 'v', + Gate(CircuitType::AND) => '^', + Gate(_) => '▓', + + Input => '─', + Output => '─', + Constant => 'o', + } + } + + pub fn minetest_type(self) -> &'static str { + use self::BlockType::*; + match self { + Air => "air", + WireH => "mesecons_insulated:insulated_off", + WireV => "mesecons_insulated:insulated_off", + WireCrossing => "mesecons_extrawires:crossover_off", + WireCorner(_) => "mesecons_extrawires:corner_off", + WireT(_) => "mesecons_extrawires:tjunction_off", + WireStar => "mesecons:mesecon_off", + Gate(gate) => gate.mesecon_id(), + + Input => "mesecons_insulated:insulated_off", + Output => "mesecons_insulated:insulated_off", + Constant => "mesecons_torch:mesecon_torch_off", + } + } + + pub fn get_param2(self) -> u8 { + use self::BlockType::*; + match self { + WireV => 0, + WireH => 3, + + WireCorner(co) => co.get_param2(), + + WireT(TRotation::LeftUpDown) => 0, + WireT(TRotation::LeftRightUp) => 1, + WireT(TRotation::RightUpDown) => 2, + WireT(TRotation::LeftRightDown) => 3, + + // gate 'input / output' pseudo types (are actually wires) + Input => 3, + Output => 3, + + Gate(CircuitType::OUTPUT) => 0, + Gate(CircuitType::INPUT) => 0, + Gate(_) => 3, + + // Can be rotated in any way. + Air => 0, + WireCrossing => 0, + WireStar => 0, + Constant => 0, + } + } +} + +// Note that they do not need to be bigger than 64 * 1024 as that's a limitation +// of minetest. +const CANVAS_MAX_W: usize = 8 * 1024; +const CANVAS_MAX_H: usize = 8 * 1024; + +pub struct Canvas { + data: Box<[[BlockType; CANVAS_MAX_H]]>, + width: usize, + height: usize, +} + +impl Canvas { + pub fn new() -> Self { + Self { + data: vec![[BlockType::Air; CANVAS_MAX_H]; CANVAS_MAX_W].into_boxed_slice(), + height: 0, + width: 0, + } + } + + pub fn set(&mut self, x: usize, y: usize, c: BlockType) { + if x >= std::u16::MAX as _ || y >= std::u16::MAX as _ { + panic!("Sorry, circuit too large for minetest (MTS format limitation)"); + } + if x >= CANVAS_MAX_W || y >= CANVAS_MAX_H { + panic!("Sorry, circuit too large for the internal canvas, consider increasing CANVAS_MAX_{{W/H}}"); + } + if x > self.width { + self.width = x; + } + if y > self.height { + self.height = y; + } + self.data[x][y] = c; + } + + pub fn get(&self, x: usize, y: usize) -> BlockType { + if x >= std::u16::MAX as _ || y >= std::u16::MAX as _ { + panic!("Sorry, circuit too large for minetest (MTS format limitation)"); + } + if x >= CANVAS_MAX_W || y >= CANVAS_MAX_H { + panic!("Sorry, circuit too large for the internal canvas, consider increasing CANVAS_MAX_{{W/H}}"); + } + self.data[x][y] + } + + pub fn dimensions(&self) -> (usize, usize) { + (self.width + 1, self.height + 1) + } + + pub fn draw(&self) { + let d = self.dimensions(); + let stdout = std::io::stdout(); + let mut lock = stdout.lock(); + for line in 0..d.1 { + for c in 0..d.0 { + let c = &self.get(c, line).c(); + let mut buf = [0u8; 4]; + c.encode_utf8(&mut buf); + let buf = &buf[0..c.len_utf8()]; + lock.write_all(&buf).unwrap(); + } + lock.write_all(b"\n").unwrap(); + } + } + + pub fn set_channel_wires(&mut self, ops: &[ChannelSubState], x: &mut u32) { + // Draw actual channel. + // Defines how many additional blocks of space should be next to the wires. + const CHANNEL_WIRE_PADDING: usize = 0; + for state in ops.iter() { + for xi in 0..(1 + CHANNEL_WIRE_PADDING) { + // Go through the current channel state. + for channel_idx in 0..state.occupancy_map.len() { + if state.occupancy_map.get(channel_idx).unwrap() != 0 { + // Channel is occupied, draw a wire (or crossing). + let x = *x as usize; + if self.get(x + xi, channel_idx) == BlockType::WireV { + self.set(x + xi, channel_idx, BlockType::WireCrossing); + } else { + self.set(x + xi, channel_idx, BlockType::WireH); + } + } + } + } + + for op in state.wires.iter() { + let WireConnection { + from: source, + to: mut destination, + mode: op, + } = op.clone(); + + if op == ChannelOp::Copy { + destination.push(source); + } + + // Connect all destination pins of the same net if there is more than one. + let mi = *destination.iter().min().unwrap(); + let ma = *destination.iter().max().unwrap(); + + if mi != ma { + for y in mi..=ma { + if y == mi { + self.set( + *x as _, + y as _, + BlockType::WireCorner(CornerOrientation::DownRight), + ); + } else if y == ma { + self.set( + *x as _, + y as _, + BlockType::WireCorner(CornerOrientation::UpRight), + ); + } else if destination.contains(&y) { + self.set(*x as _, y as _, BlockType::WireT(TRotation::RightUpDown)); + } else if self.get(*x as _, y as _) == BlockType::WireH { + self.set(*x as _, y as _, BlockType::WireCrossing); + } else if self.get(*x as _, y as _) == BlockType::Air { + self.set(*x as _, y as _, BlockType::WireV); + } + } + } + + #[derive(Debug, PartialEq, Copy, Clone)] + enum SourcePosition { + Above, + Below, + } + + let (start, end, spos) = if source < mi { + (source, mi, SourcePosition::Above) + } else if source > ma { + (ma, source, SourcePosition::Below) + } else { + if destination.contains(&source) { + if source == mi { + self.set(*x as _, source, BlockType::WireT(TRotation::LeftRightDown)); + } else if source == ma { + self.set(*x as _, source, BlockType::WireT(TRotation::LeftRightUp)); + } else { + self.set(*x as _, source, BlockType::WireStar); + } + } else { + self.set(*x as _, source, BlockType::WireT(TRotation::LeftUpDown)); + } + continue; + }; + + // Connect input to ranges. + for y in start..=end { + if y == start || y == end { + let x = *x as usize; + let y = y as usize; + + if y == source && op == ChannelOp::Copy { + continue; + } + + let block_type = match (y == source, spos, self.get(x, y)) { + // @ Source - should be empty. + (true, SourcePosition::Above, BlockType::Air) => { + BlockType::WireCorner(CornerOrientation::LeftDown) + } + (true, SourcePosition::Below, BlockType::Air) => { + BlockType::WireCorner(CornerOrientation::LeftUp) + } + + // Destination, should not be empty. + // If we're coming from above, we either have already the correct connection (single pin) + ( + false, + SourcePosition::Above, + BlockType::WireCorner(CornerOrientation::DownRight), + ) => BlockType::WireT(TRotation::RightUpDown), + ( + false, + SourcePosition::Below, + BlockType::WireCorner(CornerOrientation::UpRight), + ) => BlockType::WireT(TRotation::RightUpDown), + (false, SourcePosition::Below, BlockType::WireH) => { + BlockType::WireCorner(CornerOrientation::DownRight) + } + (false, SourcePosition::Above, BlockType::WireH) => { + BlockType::WireCorner(CornerOrientation::UpRight) + } + (is_start, pos, prev) => { + println!( + "Unexpected block type {:?} is_start={} pos={:?} - {}:{}", + prev, is_start, pos, x, y + ); + BlockType::Constant + } + }; + self.set(x as _, y as _, block_type); + } else if self.get(*x as _, y as _) == BlockType::Air { + self.set(*x as _, y as _, BlockType::WireV); + } else if self.get(*x as _, y as _) == BlockType::WireH { + self.set(*x as _, y as _, BlockType::WireCrossing); + } + } + + if op == ChannelOp::Copy { + self.set( + *x as _, + source as usize, + BlockType::WireT(match spos { + SourcePosition::Below => TRotation::LeftRightUp, + SourcePosition::Above => TRotation::LeftRightDown, + }), + ); + } + } + *x += 1 + CHANNEL_WIRE_PADDING as u32; + } + } + + pub fn generate_lua_schematic(&self, fname: &str) -> std::io::Result<()> { + let d = self.dimensions(); + let mut file = File::create(fname)?; + file.write_all("schematic = {\n".as_bytes())?; + file.write_all(format!("\tsize = {{x={}, y=2, z={}}},\n", d.1, d.0).as_bytes())?; + file.write_all("\tdata = {\n".as_bytes())?; + + for x in 0..d.0 { + for z in 0..2 { + for y in 0..d.1 { + file.write_all( + if z == 0 { + "\t\t{name=\"stone\"},\n".to_string() + } else { + format!( + "\t\t{{name=\"{}\", param2=param2}},\n", + self.get(x, y).minetest_type() + ) + } + .as_bytes(), + )?; + } + } + } + + file.write_all("\t}\n".as_bytes())?; + file.write_all("}\n".as_bytes())?; + Ok(()) + } + + pub fn serialize_to_mts(&self, fname: &str) -> std::io::Result<()> { + // Map size. + let d = self.dimensions(); + // The minetest source is not consistent when it comes to the type of + // this field (i16 vs u16), so picking the conservative option here. + if d.1 > std::i16::MAX as _ || d.0 >= std::i16::MAX as _ { + panic!("Schematic too big to export to a mts :/"); + } + + let mut file = File::create(fname)?; + + file.write_all(b"MTSM")?; // MTSCHEM_FILE_SIGNATURE + file.write_u16::(1)?; // Version 1 + + file.write_i16::(d.1 as i16)?; + file.write_i16::(2)?; + file.write_i16::(d.0 as i16)?; + + // No need to do the prob table as we're totally old. + // Write # node names. + file.write_u16::(BLOCK_IDS.len() as u16)?; + + let serialize_string = |f: &mut File, s: &str| -> std::io::Result<()> { + if s.len() > std::u16::MAX as usize { + panic!("String too large to serialize."); + } + let s = s.as_bytes(); + f.write_u16::(s.len() as u16)?; + f.write_all(s)?; + Ok(()) + }; + + for b in BLOCK_IDS.iter() { + serialize_string(&mut file, b)?; + } + + let mut encoder = BufWriter::new(ZlibEncoder::new(file, Compression::Best)); + + // Generate reverse lookup table for block ids. + let mut block_lookup_table: std::collections::HashMap<&'static str, usize> = + std::collections::HashMap::new(); + for (idx, val) in BLOCK_IDS.iter().enumerate() { + block_lookup_table.insert(val, idx); + } + + println!(" [+] Writing node types"); + // Write node types. + for x in 0..d.0 { + for z in 0..2 { + for y in 0..d.1 { + let t = if z == 0 { + "stone" + } else { + self.get(x, y).minetest_type() + }; + + encoder.write_u16::( + *block_lookup_table + .get(t) + .unwrap_or_else(|| panic!("Block type {:?} not found?", t)) + as u16, + )?; + } + } + } + + println!(" [+] Writing param1"); + // Write param1 + for _ in 0..2 { + for _ in 0..d.0 { + for _ in 0..d.1 { + encoder.write_u8(0)?; + } + } + } + + println!(" [+] Writing param2"); + // Write param2 + for x in 0..d.0 { + for z in 0..2 { + for y in 0..d.1 { + encoder.write_u8(if z == 0 { + 0 + } else { + self.get(x, y).get_param2() + })?; + } + } + } + + Ok(()) + } +} diff --git a/src/channel_router.rs b/src/channel_router.rs new file mode 100644 index 0000000..29322cd --- /dev/null +++ b/src/channel_router.rs @@ -0,0 +1,350 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use bitmap; +use std::cmp; +use std::collections::HashMap; + +struct Ranges { + ranges: Vec>, +} + +impl Ranges { + fn new() -> Self { + Ranges { ranges: Vec::new() } + } + + fn add(&mut self, start: usize, end: usize) { + let (start, end) = (cmp::min(start, end), cmp::max(start, end) + 1); + self.ranges.push(std::ops::Range { start, end }); + } + + fn contains(&self, start: usize, end: usize) -> bool { + let (start, end) = (cmp::min(start, end), cmp::max(start, end)); + (start..=end).any(|v| self.ranges.iter().any(|r| r.contains(&v))) + } + + fn contains_range(&self, range: &std::ops::Range) -> bool { + self.contains(range.start, range.end) + } + + fn range_sum(&self) -> usize { + self.ranges.iter().map(|r| r.end - r.start).sum() + } +} + +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum ChannelState { + Free, + // Occupied means no connection. This is the same as a constant false. + Occupied, + // Constant true. + Constant, + Net(usize), +} +pub type ChannelLayout = [ChannelState]; + +impl ChannelState { + pub fn is_free(&self) -> bool { + self == &ChannelState::Free + } + + pub fn contains_net(&self) -> bool { + match self { + ChannelState::Net(_) => true, + _ => false, + } + } + + pub fn is_constant_on(&self) -> bool { + match self { + ChannelState::Constant => true, + _ => false, + } + } +} + +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum ChannelOp { + Move, + Copy, +} + +#[derive(Debug, Clone)] +pub struct WireConnection { + pub from: usize, + pub to: Vec, + pub mode: ChannelOp, +} + +#[derive(Debug)] +pub struct ChannelSubState { + pub wires: Vec, + pub occupancy_map: bitmap::Bitmap, bitmap::OneBit>, +} + +#[derive(Debug)] +struct Task { + net: usize, + from: usize, + to: Vec, +} + +impl Task { + fn channel_range_required(&self) -> std::ops::Range { + let from = [self.from]; + let min = self.to.iter().chain(&from).min().unwrap(); + let max = self.to.iter().chain(&from).max().unwrap(); + std::ops::Range { + start: *min, + end: max + 1, + } + } + + fn channel_width_required(&self) -> usize { + let r = self.channel_range_required(); + r.end - r.start + } + + fn occupied_target_pins(&self, layout: &ChannelLayout) -> Vec { + let mut occupied = Vec::new(); + for &idx in &self.to { + if layout[idx].contains_net() && layout[idx] != ChannelState::Net(self.net) { + occupied.push(idx); + } + } + occupied + } + + // Returns how 'good' a new 'from' position is for this task (when evicting) + // so that we can prefer nice spots. + fn eviction_cost(&self, new_pos: usize) -> usize { + let min = self.to.iter().min().unwrap(); + let max = self.to.iter().max().unwrap(); + + let dist = (self.from as isize - new_pos as isize).abs() as usize; + + if new_pos > *max { + 2 * (new_pos - *max) + dist + } else if new_pos < *min { + 2 * (*min - new_pos) + dist + } else { + dist + } + } +} + +#[derive(Default)] +struct RouteTasks { + // source idx -> vec + tasks: HashMap>, +} + +impl RouteTasks { + fn add(&mut self, from: usize, to: usize) { + if let Some(k) = self.tasks.get_mut(&from) { + k.push(to); + } else { + self.tasks.insert(from, vec![to]); + } + } + + fn into_tasks(mut self, src: &ChannelLayout) -> Vec { + self.tasks + .drain() + .map(|(k, v)| { + let net = match src[k] { + ChannelState::Net(i) => i, + _ => unreachable!(), + }; + Task { + net, + from: k, + to: v, + } + }) + .collect::>() + } +} + +pub fn route_channel(start: &ChannelLayout, end: &ChannelLayout) -> Vec { + let mut state = start.to_owned(); + // Expand the state to be at least end.len() wide. + while state.len() < end.len() { + state.push(ChannelState::Free); + } + + let mut tasks = RouteTasks::default(); + + for end_idx in 0..end.len() { + if !end[end_idx].contains_net() || end[end_idx] == state[end_idx] { + continue; + } + + let state_idx = state + .iter() + .position(|v| v == &end[end_idx]) + .unwrap_or_else(|| panic!("Required field '{:?}' not found", end[end_idx])); + tasks.add(state_idx, end_idx); + } + + let mut tasks = tasks.into_tasks(&state); + // Order by how much of the channel this task occupies. + tasks.sort_by(|a, b| a.channel_width_required().cmp(&b.channel_width_required())); + + let mut steps: Vec = Vec::new(); + + loop { + // Ranges of the channel that is currently occupied. + let mut ranges = Ranges::new(); + // Instruction on how to connect pins in the current part of the channel. + let mut wires = Vec::new(); + // To detect if we were unable to do anything due to blocked pins. + let old_task_len = tasks.len(); + + tasks = tasks + .drain(0..tasks.len()) + .filter(|task| { + // Speed things up by only 'enforcing' 50% channel utilization. + if ranges.range_sum() > (cmp::max(state.len(), end.len()) / 2) { + return true; + } + + // Do we have the required part of the channel available? + if ranges.contains_range(&task.channel_range_required()) { + return true; + } + + let blocking_pins = task.occupied_target_pins(&state); + if blocking_pins.is_empty() { + // Targets are free, directly move (or copy) it there. + + let keep = if task.from >= end.len() || state[task.from] != end[task.from] { + state[task.from] = ChannelState::Free; + false + } else { + true + }; + + wires.push(WireConnection { + from: task.from, + to: task.to.clone(), + mode: if keep { + ChannelOp::Copy + } else { + ChannelOp::Move + }, + }); + + let r = task.channel_range_required(); + // -1 here since .add() + channel_range_required() will do +1. + ranges.add(r.start, r.end - 1); + + for &to in &task.to { + state[to] = ChannelState::Net(task.net); + } + + // We successfully handled this one. + return false; + } + + true + }) + .collect::>(); + + // We were unable to handle any tasks -> we need to evict some channels. + if old_task_len == tasks.len() { + // Find available positions where we can evict to. + let mut free_positions = state + .iter() + .enumerate() + .filter(|(_, v)| !v.contains_net()) + .map(|(k, _)| k) + .filter(|&k| k >= end.len() || !end[k].contains_net()) + .collect::>(); + + if free_positions.is_empty() { + println!("[!] No free positions found, expanding channel"); + // Make sure that we have some room, scaling with the number of + // remaining tasks as a random tradeoff. + for _ in 0..(tasks.len() / 10 + 1) { + state.push(ChannelState::Free); + free_positions.push(state.len() - 1); + } + } + + for task_idx in 0..tasks.len() { + let blocking_pins = tasks[task_idx].occupied_target_pins(&state); + for to_evict in blocking_pins { + // Find corresponding task. + let task_idx_to_evict = tasks + .iter() + .position(|t| t.from == to_evict) + .unwrap_or_else(|| panic!("Could not find task blocking {}", to_evict)); + + // Find a good place for this task to evict to. + free_positions.sort_by(|&a, &b| { + // Comparing in the opposite order on purpose here so + // that we can use pop() later. + tasks[task_idx_to_evict] + .eviction_cost(b) + .cmp(&tasks[task_idx_to_evict].eviction_cost(a)) + }); + + let from = tasks[task_idx_to_evict].from; + let new_pos = *free_positions.last().unwrap(); + + // Check whether the space is actually available. + let req_range = std::ops::Range { + start: cmp::min(from, new_pos), + end: cmp::max(from, new_pos) + 1, + }; + + if !ranges.contains_range(&req_range) { + free_positions.pop(); + ranges.add(from, new_pos); + wires.push(WireConnection { + from, + to: vec![new_pos], + mode: ChannelOp::Move, + }); + tasks[task_idx_to_evict].from = new_pos; + state[new_pos] = ChannelState::Net(tasks[task_idx_to_evict].net); + state[to_evict] = ChannelState::Free; + } + } + } + } + + let mut bitmap = + bitmap::Bitmap::from_storage(state.len(), (), vec![0; (state.len() + 63) / 64]) + .unwrap(); + for idx in state + .iter() + .enumerate() + .filter(|(_, v)| v.contains_net()) + .map(|(k, _)| k) + { + bitmap.set(idx, 1); + } + + steps.push(ChannelSubState { + wires, + occupancy_map: bitmap, + }); + if tasks.is_empty() { + return steps; + } + } +} diff --git a/src/circuit.rs b/src/circuit.rs new file mode 100644 index 0000000..7589eee --- /dev/null +++ b/src/circuit.rs @@ -0,0 +1,428 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +extern crate itertools; +use itertools::Itertools; + +use crate::canvas::{BlockType, Canvas, CornerOrientation}; +use crate::channel_router::{ChannelLayout, ChannelState}; +use crate::loader::{YosysJsonCell, YosysJsonPortDirection}; +use serde_json::Value; +use std::convert::TryFrom; + +#[derive(PartialEq, Debug)] +pub enum IOType { + Input, + Output, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Position2D(pub u32, pub u32); + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum PortConnection { + Net(usize), + Constant(bool), +} + +impl PortConnection { + pub fn is_constant(&self) -> bool { + match self { + PortConnection::Constant(_) => true, + _ => false, + } + } + + pub fn get_net(&self) -> Option { + match self { + PortConnection::Net(n) => Some(*n), + _ => None, + } + } +} + +impl Into for PortConnection { + fn into(self) -> ChannelState { + match self { + PortConnection::Net(n) => ChannelState::Net(n), + PortConnection::Constant(false) => ChannelState::Occupied, + PortConnection::Constant(true) => ChannelState::Constant, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq)] +pub struct Port { + pub connection: PortConnection, + pub position: Option, +} + +impl Port { + pub fn new_unplaced(connection: PortConnection) -> Self { + Self { + connection, + position: None, + } + } + + pub fn from(s: &Value) -> Self { + let connection = match s { + Value::Number(n) => PortConnection::Net(n.as_u64().unwrap() as usize), + Value::String(ref s) => match s.as_ref() { + "0" | "x" | "y" => PortConnection::Constant(false), + "1" => PortConnection::Constant(true), + _ => panic!("Unknown constant value '{}'", s), + }, + _ => unreachable!(), + }; + + Self { + connection, + position: None, + } + } +} + +impl std::cmp::Ord for Port { + fn cmp(&self, other: &Port) -> std::cmp::Ordering { + use PortConnection::*; + match (self.connection, other.connection) { + (Net(a), Net(b)) => a.cmp(&b), + (_, _) => std::cmp::Ordering::Equal, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum CircuitType { + INPUT, + OUTPUT, + FORWARD, + + AND, + NAND, + ANDNOT, + OR, + NOR, + ORNOT, + NOT, + XNOR, + XOR, +} + +impl CircuitType { + pub fn from_verilog(s: &str) -> Self { + match s { + "$_ANDNOT_" => CircuitType::ANDNOT, // A & (~B) + "$_NAND_" => CircuitType::NAND, // and but inverted + "$_NOR_" => CircuitType::NOR, // or but inverted + "$_OR_" => CircuitType::OR, + "$_AND_" => CircuitType::AND, + "$_ORNOT_" => CircuitType::ORNOT, // A | (~B) + "$_XNOR_" => CircuitType::XNOR, // ~(XOR) + "$_XOR_" => CircuitType::XOR, // XOR + "$_NOT_" => CircuitType::NOT, + _ => panic!( + "Unsupported gate type '{}', make sure to run an abc pass", + s + ), + } + } + + pub fn mesecon_id(self) -> &'static str { + use self::CircuitType::*; + match self { + INPUT => "mesecons_walllever:wall_lever_off", + OUTPUT => "mesecons_lamp:lamp_off", + FORWARD => "mesecons_insulated:insulated_off", + OR => "mesecons_gates:or_off", + XOR => "mesecons_gates:xor_off", + AND => "mesecons_gates:and_off", + NAND => "mesecons_gates:nand_off", + NOR => "mesecons_gates:nor_off", + NOT => "mesecons_gates:not_off", + _ => panic!("{:?} not a mesecon thing", &self), + } + } +} + +pub trait CircuitT { + fn get_type(&self) -> CircuitType; + + fn width(&self) -> u32 { + use self::CircuitType::*; + match self.get_type() { + INPUT | NOT | OUTPUT | FORWARD => 1, + AND | OR | XOR | NAND | ANDNOT | NOR | ORNOT | XNOR => 2, + } + } + + fn height(&self) -> u32 { + use self::CircuitType::*; + match self.get_type() { + INPUT | NOT | OUTPUT | FORWARD => 1, + AND | OR | XOR | NAND | ANDNOT | NOR | ORNOT | XNOR => { + assert!(self.inputs().len() == 2 || self.inputs().len() == 1); + 3 + } + } + } + + fn inputs(&self) -> &Vec; + fn outputs(&self) -> &Vec; + + fn input_positions(&self) -> Vec<(Position2D, Port)>; + fn reposition(&mut self, px: u32); + fn draw(&self, canvas: &mut Canvas); + fn place(&mut self, position: Position2D); +} + +#[derive(Debug)] +pub struct Circuit { + pub circuit_type: CircuitType, + pub inputs: Vec, + pub outputs: Vec, + + pub position: Option, +} + +impl Circuit { + pub fn new_external_input_pin(net: Port) -> Self { + Self { + circuit_type: CircuitType::INPUT, + inputs: Vec::new(), + outputs: vec![net], + position: None, + } + } + + pub fn new_external_output_pin(net: Port) -> Self { + Self { + circuit_type: CircuitType::OUTPUT, + inputs: vec![net], + outputs: Vec::new(), + position: None, + } + } + + pub fn new_forwarding_pin(net: Port) -> Self { + Self { + circuit_type: CircuitType::FORWARD, + inputs: vec![net], + outputs: vec![net], + position: None, + } + } + + pub fn width(&self) -> u32 { + use self::CircuitType::*; + match self.circuit_type { + INPUT | NOT | OUTPUT | FORWARD => 1, + AND | OR | XOR | NAND | ANDNOT | NOR | ORNOT | XNOR => 2, + } + } + + pub fn height(&self) -> u32 { + use self::CircuitType::*; + match self.circuit_type { + INPUT | NOT | OUTPUT | FORWARD => 1, + AND | OR | XOR | NAND | ANDNOT | NOR | ORNOT | XNOR => { + assert!(self.inputs.len() == 2 || self.inputs.len() == 1); + 3 + } + } + } + + pub fn can_swap_inputs(&self) -> bool { + use self::CircuitType::*; + match self.circuit_type { + INPUT | NOT | OUTPUT | FORWARD | ANDNOT | ORNOT => false, + AND | OR | XOR | NAND | NOR | XNOR => { + assert!(self.inputs.len() == 2); + true + } + } + } + + pub fn swap_inputs(&mut self) { + assert!(self.can_swap_inputs()); + self.inputs.swap(0, 1); + } + + pub fn reposition(&mut self, px: u32) { + assert!(self.position.is_some()); + if let Some(ref mut p) = self.position { + p.0 = px + 1; + } + for i in self.inputs.iter_mut() { + if let Some(ref mut p) = i.position { + p.0 = px; + } + } + let w = self.width(); + for i in self.outputs.iter_mut() { + if let Some(ref mut p) = i.position { + p.0 = px + w + 1; + } + } + } + + pub fn draw(&self, canvas: &mut Canvas) { + if self.circuit_type == CircuitType::INPUT + || self.circuit_type == CircuitType::NOT + || self.circuit_type == CircuitType::OUTPUT + || self.circuit_type == CircuitType::FORWARD + { + // Nothing to do. + } else { + // We need to do the input/output wiring. + let p = self.position.unwrap(); + let w = self.width(); + + // Input + canvas.set( + p.0 as usize, + p.1 as usize - 1, + BlockType::WireCorner(CornerOrientation::LeftDown), + ); + canvas.set( + p.0 as usize, + p.1 as usize + 1, + BlockType::WireCorner(CornerOrientation::LeftUp), + ); + + // Output: + canvas.set( + p.0 as usize + w as usize - 1, + p.1 as usize, + BlockType::WireH, + ); + + // XNOR / ORNOT / ANDNOT are not mesecon things and need to be monkeypatched. + let invert_second = match self.circuit_type { + CircuitType::XNOR => { + canvas.set( + p.0 as usize, + p.1 as usize, + BlockType::Gate(CircuitType::XOR), + ); + canvas.set( + p.0 as usize + w as usize - 1, + p.1 as usize, + BlockType::Gate(CircuitType::NOT), + ); + false + } + CircuitType::ORNOT => { + canvas.set(p.0 as usize, p.1 as usize, BlockType::Gate(CircuitType::OR)); + true + } + CircuitType::ANDNOT => { + canvas.set( + p.0 as usize, + p.1 as usize, + BlockType::Gate(CircuitType::AND), + ); + true + } + _ => false, + }; + if invert_second { + // Invert second input. + canvas.set( + p.0 as usize - 1, + p.1 as usize + 1, + BlockType::Gate(CircuitType::NOT), + ); + } + } + } + + pub fn place(&mut self, position: Position2D) { + if self.position.is_some() { + // This was already placed. + panic!("Circuit was already placed"); + } + self.position = Some(Position2D(position.0 + 1, position.1 + self.height() / 2)); + let mut off_y = 0; + for i in self.inputs.iter_mut() { + i.position = Some(Position2D(position.0, position.1 + off_y)); + off_y += 2; + } + let w = self.width(); + if !self.outputs.is_empty() { + assert!(self.outputs.len() == 1); + if self.circuit_type == CircuitType::FORWARD + || self.circuit_type == CircuitType::INPUT + || self.circuit_type == CircuitType::NOT + { + self.outputs[0].position = Some(Position2D(position.0 + w + 1, position.1)); + } else { + self.outputs[0].position = Some(Position2D(position.0 + w + 2, position.1 + 1)); + } + } + } +} + +impl TryFrom<&YosysJsonCell> for Circuit { + type Error = (); + fn try_from(cell: &YosysJsonCell) -> Result { + for i in cell.connections.iter() { + assert!(i.1.len() == 1); + } + let inputs: Vec<_> = cell + .connections + .iter() + .filter(|(k, _)| cell.port_directions[*k] == YosysJsonPortDirection::Input) + .sorted_by(|(k0, _), (k1, _)| k0.cmp(k1)) + .map(|(_, v)| Port::from(&v[0])) + .collect(); + + let outputs: Vec<_> = cell + .connections + .iter() + .filter(|(k, _)| cell.port_directions[*k] == YosysJsonPortDirection::Output) + .map(|(_, v)| Port::from(&v[0])) + .collect(); + + Ok(Self { + circuit_type: CircuitType::from_verilog(&cell.cell_type), + inputs, + outputs, + position: None, + }) + } +} + +// Determine the channel layout for the given list of circuits. +pub fn determine_channel_layout<'a, T: Iterator>( + circuits: T, + io: IOType, +) -> Box { + let mut res = Vec::new(); + for c in circuits { + let it = match io { + IOType::Input => c.inputs.iter(), + IOType::Output => c.outputs.iter(), + }; + for i in it { + let off = i.position.unwrap().1 as usize; + while off >= res.len() { + res.push(ChannelState::Occupied); + } + res[off] = i.connection.into(); + } + } + res.into_boxed_slice() +} diff --git a/src/loader.rs b/src/loader.rs new file mode 100644 index 0000000..c74dbef --- /dev/null +++ b/src/loader.rs @@ -0,0 +1,56 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +// Structured - at least a little. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum YosysJsonPortDirection { + #[serde(rename = "input")] + Input, + #[serde(rename = "output")] + Output, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct YosysJsonPort { + pub direction: YosysJsonPortDirection, + pub bits: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct YosysJsonCell { + hide_name: usize, + #[serde(rename = "type")] + pub cell_type: String, + parameters: Value, // can be ignored + attributes: Value, // can be ignored + pub port_directions: std::collections::HashMap, + pub connections: std::collections::HashMap>, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct YosysJsonModule { + attributes: Value, + pub ports: std::collections::HashMap, + pub cells: std::collections::HashMap, + netnames: Value, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct YosysJson { + creator: String, + pub modules: std::collections::HashMap, +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..dbfec0d --- /dev/null +++ b/src/main.rs @@ -0,0 +1,368 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![feature(drain_filter)] + +mod canvas; +mod channel_router; +mod circuit; +mod loader; +mod placer; + +extern crate rayon; + +use crate::canvas::*; +use crate::channel_router::*; +use crate::circuit::*; +use crate::loader::*; +use crate::placer::place_gates; +use clap::{App, Arg}; +use core::convert::TryFrom; +use rayon::prelude::*; +use std::fs::File; +use std::io::Read; + +fn resolve_gate_dependencies( + mut circuits: Vec, + output_ports: Vec, +) -> Vec> { + let mut gate_hierarchy = Vec::new(); + let mut nets_available = Vec::new(); + let mut required_nets = Vec::new(); + + while !circuits.is_empty() { + // Find circuits where all inputs are satisfied. + let mut placable_gates: Vec = circuits + .drain_filter(|c| { + c.inputs.iter().all(|input| match input.connection { + PortConnection::Constant(_) => true, + PortConnection::Net(net) => nets_available.contains(&net), + }) + }) + .collect(); + + if placable_gates.is_empty() { + panic!("Circular dependency detected"); + } + + // Sort gates by output net number. + placable_gates.sort_by(|a, b| a.outputs[0].cmp(&b.outputs[0])); + + for g in placable_gates.iter() { + for o in g.outputs.iter() { + if let PortConnection::Net(net) = o.connection { + if !nets_available.contains(&net) { + nets_available.push(net); + } + } + } + for i in g.inputs.iter() { + if let PortConnection::Net(net) = i.connection { + if !required_nets.contains(&net) { + required_nets.push(net); + } + } + } + } + + gate_hierarchy.push(placable_gates); + } + + gate_hierarchy.push( + output_ports + .iter() + .map(|&bit| Circuit::new_external_output_pin(bit)) + .collect(), + ); + + let output_nets = output_ports + .iter() + .filter(|n| !n.connection.is_constant()) + .map(|n| match n.connection { + PortConnection::Net(i) => i, + _ => unreachable!(), + }) + .collect::>(); + + for n in nets_available + .iter() + .filter(|v| !required_nets.contains(v) && !output_nets.contains(v)) + { + panic!("[!] Net {} seems to be not used - bug?", n); + } + + gate_hierarchy +} + +fn parse_json(filepath: &str) -> std::io::Result>> { + let mut file = File::open(filepath)?; + let mut buf = String::new(); + file.read_to_string(&mut buf)?; + + let v: YosysJson = serde_json::from_str(&*buf)?; + if v.modules.len() != 1 { + panic!("Input file contained no or more than one module"); + } + let m = &v.modules.values().next().unwrap(); + + // Convert cells to `Circuit`s. + let mut circuits: Vec<_> = m + .cells + .values() + .map(|v| Circuit::try_from(v).unwrap()) + .collect(); + + // Add input connections. + for bit in m + .ports + .iter() + .map(|p| p.1) + .filter(|p| p.direction == YosysJsonPortDirection::Input) + .flat_map(|p| &p.bits) + { + circuits.push(Circuit::new_external_input_pin(Port::from(bit))); + } + + let mut output_pins = m + .ports + .iter() + .map(|p| p.1) + .filter(|p| p.direction == YosysJsonPortDirection::Output) + .flat_map(|p| &p.bits) + .map(|bits| Port::from(&bits)) + .collect::>(); + output_pins.sort(); + + // Let's keep the input layout consistent. + circuits.sort_by(|a, b| match (a.circuit_type, b.circuit_type) { + (CircuitType::INPUT, CircuitType::INPUT) => a.outputs[0].cmp(&b.outputs[0]), + (CircuitType::INPUT, _) => std::cmp::Ordering::Less, + (_, _) => a.inputs[0].cmp(&b.inputs[0]), + }); + + println!("[*] Calculating gate layout."); + Ok(resolve_gate_dependencies(circuits, output_pins)) +} + +fn main() -> std::io::Result<()> { + let parameters = App::new("Minetest HDL") + .version("0.1") + .author("Kevin Hamacher ") + .about("Converts synthesized circuit (yosys json output) to a minetest schematic that can be placed in minetest") + .arg( + Arg::with_name("text") + .short("t") + .long("text") + .help("Print text overview on STDOUT"), + ) + .arg( + Arg::with_name("write_lua") + .short("l") + .long("write_lua") + .help("Writes a lua blueprint") + .takes_value(true), + ) + .arg( + Arg::with_name("write_mts") + .short("m") + .long("write_mts") + .help("Writes a MTS blueprint (binary format)") + .takes_value(true), + ) + .arg( + Arg::with_name("INPUT") + .help("Sets the input file to use") + .required(true) + .index(1), + ) + .get_matches(); + + let lua_filename = parameters.value_of("write_lua"); + let mts_filename = parameters.value_of("write_mts"); + + let mut gate_hierarchy = parse_json(parameters.value_of("INPUT").unwrap())?; + + println!("[*] Adding 'forwarding' gates to keep unused nets."); + { + // Starting with 2 here since we will always have all inputs at the 1st + // stage (0th = input bits for the whole circuitry), so we need to start + // checking that the 1st stage will 'reexport' the required bits. + for idx in (2..gate_hierarchy.len()).rev() { + let required_inputs: Vec<_> = gate_hierarchy[idx] + .iter() + .flat_map(|c| c.inputs.iter().map(|i| i.connection)) + .filter(|v| !v.is_constant()) + .map(|v| v.get_net().unwrap()) + .collect(); + + let mut outputs_available: Vec<_> = gate_hierarchy[idx - 1] + .iter() + .flat_map(|c| c.outputs.iter().map(|o| o.connection)) + .filter(|v| !v.is_constant()) + .map(|v| v.get_net().unwrap()) + .collect(); + + for ri in required_inputs { + if !outputs_available.contains(&ri) { + // The previous segment did not provide the required output, + // so add a dependency. + gate_hierarchy[idx - 1].push(Circuit::new_forwarding_pin(Port::new_unplaced( + PortConnection::Net(ri), + ))); + outputs_available.push(ri); + } + } + } + } + + println!("[*] Performing channel routing."); + + // Place the first circuit block. + let mut block_x_start = 0; + { + let mut gate_y = 0; + let mut max_w = 0; + for c in gate_hierarchy[0].iter_mut() { + c.place(Position2D(block_x_start, gate_y)); + gate_y += c.height(); + if c.width() > max_w { + max_w = c.width(); + } + } + block_x_start += max_w; + } + + // Go at least a little bit straight before starting the channel router. + block_x_start += 2; + + // 1) Place + println!("[*] Placing gates"); + for gategroup_idx in 0..gate_hierarchy.len() - 1 { + // Get the current input layout (left side of the channel). + let channel_layout = + determine_channel_layout(gate_hierarchy[gategroup_idx].iter(), IOType::Output); + println!( + " [+] Step {}/{} - {} inputs to {} gates", + gategroup_idx + 1, + gate_hierarchy.len() - 1, + channel_layout.len(), + gate_hierarchy[gategroup_idx + 1].len() + ); + + // Determine required channel layout (input pins of the next group). + place_gates(&channel_layout, &mut gate_hierarchy[gategroup_idx + 1]); + } + + println!("[*] Routing"); + let ops_per_step = (0..gate_hierarchy.len() - 1) + .into_par_iter() + .map(|gategroup_idx| { + let channel_layout = + determine_channel_layout(gate_hierarchy[gategroup_idx].iter(), IOType::Output); + // Determine required channel layout (input pins of the next group). + let desired_channel_layout = + determine_channel_layout(gate_hierarchy[gategroup_idx + 1].iter(), IOType::Input); + route_channel(&channel_layout, &desired_channel_layout) + }) + .collect::>(); + + let mut canvas = Canvas::new(); + let mut place_constants_here = Vec::new(); + println!("[*] Drawing to canvas"); + for (gategroup_idx, ops) in ops_per_step.iter().enumerate() { + let channel_layout = + determine_channel_layout(gate_hierarchy[gategroup_idx].iter(), IOType::Output); + // Determine required channel layout (input pins of the next group). + let desired_channel_layout = + determine_channel_layout(gate_hierarchy[gategroup_idx + 1].iter(), IOType::Input); + + // Let's draw our channels. + // 1 pixel initial wires + const WIRE_LENGTH_AFTER_GATE: usize = 1; + let mut x = block_x_start; + for xi in 0..WIRE_LENGTH_AFTER_GATE { + for (ly, cly) in channel_layout.iter().enumerate() { + if cly.contains_net() { + canvas.set(x as usize + xi, ly as usize, BlockType::WireH); + } + } + } + x += WIRE_LENGTH_AFTER_GATE as u32; + + canvas.set_channel_wires(&ops, &mut x); + + for c in gate_hierarchy[gategroup_idx + 1].iter_mut() { + c.reposition(x); + } + + // Place constant inputs + for pos in desired_channel_layout + .iter() + .enumerate() + .filter(|(_, p)| p.is_constant_on()) + .map(|(idx, _)| idx) + { + place_constants_here.push((x as usize, pos)); + } + + block_x_start = x + 3; + } + + for gate_group in gate_hierarchy.iter() { + for g in gate_group.iter() { + let gate_pos = g + .position + .unwrap_or_else(|| panic!("Circuit {:#?} was not placed!", g)); + canvas.set( + gate_pos.0 as usize, + gate_pos.1 as usize, + BlockType::Gate(g.circuit_type), + ); + for i in &g.inputs { + let input_pos = i.position.expect("Input was not placed"); + canvas.set(input_pos.0 as usize, input_pos.1 as usize, BlockType::Input); + } + for o in &g.outputs { + let output_pos = o.position.expect("Output was not placed"); + canvas.set( + output_pos.0 as usize, + output_pos.1 as usize, + BlockType::Output, + ); + } + g.draw(&mut canvas); + } + } + + for (x, y) in place_constants_here.iter() { + canvas.set(*x, *y, BlockType::Constant); + } + + println!("[*] Canvas dimensions: {:?}", canvas.dimensions()); + if parameters.occurrences_of("text") > 0 { + println!("*** text overview ***"); + canvas.draw(); + } + if let Some(f) = lua_filename { + println!("[*] Generating lua schematic file"); + canvas.generate_lua_schematic(f)?; + } + + if let Some(f) = mts_filename { + println!("[*] Generating MTS schematic file"); + canvas.serialize_to_mts(f)? + } + + Ok(()) +} diff --git a/src/placer.rs b/src/placer.rs new file mode 100644 index 0000000..0a66d9b --- /dev/null +++ b/src/placer.rs @@ -0,0 +1,115 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::channel_router::{ChannelLayout, ChannelState}; +use crate::circuit::{Circuit, Position2D}; + +fn get_net_index_in_layout(channel_layout: &ChannelLayout, net: usize) -> Option { + channel_layout + .iter() + .enumerate() + .filter(|(_, v)| v == &&ChannelState::Net(net)) + .map(|(p, _)| p) + .next() +} + +pub fn place_gates(channel_layout: &ChannelLayout, circuits: &mut Vec) { + let mut desired_channel_layout = vec![ChannelState::Free; channel_layout.len()]; + // Place output circuits (right side of the channel). + // Since we don't know the width of the channel yet, assign temporary + // coordinates there. + + // First iteration: Step gates where the inputs are aligned with + // the outputs of the previous step and swap inputs if necessary. + for circuit in circuits.iter_mut() { + // If we have two inputs, swap them if they would cause unnecessary wire crossings. + if circuit.inputs.len() == 2 { + if !circuit.can_swap_inputs() { + continue; + } + + // Check where the inputs are that we need. + let p1 = get_net_index_in_layout( + channel_layout, + circuit.inputs[0].connection.get_net().unwrap(), + ) + .unwrap(); + let p2 = get_net_index_in_layout( + channel_layout, + circuit.inputs[1].connection.get_net().unwrap(), + ) + .unwrap(); + + if p1 > p2 { + circuit.swap_inputs(); + } + } else if circuit.inputs.len() == 1 { + if let Some(req_input) = circuit.inputs[0].connection.get_net() { + let p = get_net_index_in_layout(channel_layout, req_input) + .unwrap_or_else(|| panic!("Could not find net {}", req_input)); + + // If the target slot is still available, place ourselves here. + // Note: This will also only work with 1x1 gates, circuits might + // overlap. + if desired_channel_layout[p].is_free() { + circuit.place(Position2D(100_000, p as u32)); + desired_channel_layout[p] = ChannelState::Net(req_input); + } + } + } + } + + // Second iteration: Place everything else. + for circuit in circuits.iter_mut() { + // Skip circuits that were already placed. + if circuit.position.is_some() { + continue; + } + + // Find free space to place this circuit. + let space_pattern = vec![ChannelState::Free; circuit.height() as usize]; + let free_pos = desired_channel_layout + .windows(circuit.height() as usize) + .position(|window| window == &*space_pattern) + // TODO: This adds it at the end, there should be more + // efficient things to do. + .unwrap_or_else(|| desired_channel_layout.len()); + + circuit.place(Position2D(100_000, free_pos as u32)); + + for i in circuit.inputs.iter() { + let off = i.position.unwrap().1 as usize; + while off >= desired_channel_layout.len() { + desired_channel_layout.push(ChannelState::Free); + } + assert!(desired_channel_layout[off].is_free()); + desired_channel_layout[off] = i.connection.into(); + } + + // Hack: Make sure to mark the space between the inputs as occupied. + assert!(circuit.inputs.len() == 1 || circuit.inputs.len() == 2); + if circuit.inputs.len() == 2 { + let off = circuit.inputs[0].position.unwrap().1 as usize; + assert!(desired_channel_layout[off + 1].is_free()); + desired_channel_layout[off + 1] = ChannelState::Occupied; + } + } + + #[cfg(debug_assertions)] + for c in circuits.iter() { + if c.position.is_none() { + panic!("Some circuits were not placed"); + } + } +}