From 58fab1ca90edd382595855ad03c19992a527ea2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Mon, 6 Jul 2026 09:58:32 +0200 Subject: [PATCH 01/17] synth: single writer for 1_synth.sdc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1_synth.sdc had two writers: yosys' synth.tcl copied the user's SDC_FILE into place (a leftover from before SDC canonicalization existed, "one day a more sophisticated synthesis..."), then synth_odb.tcl overwrote it in place with the OpenSTA-canonicalized version. cp preserves the source's mode bits, so when SDC_FILE is a read-only build input (bazel RBE), the copy is read-only and the canonicalization write fails. Locally sources are writable, so this only accidentally worked. Drop the copies from synth.tcl and synth_preamble.tcl: synth_odb.tcl/synth_syn.tcl are the sole writers of 1_synth.sdc. 1_2_yosys.sdc (copied by the do-yosys make recipe) remains the raw SDC the yosys stage hands to synth_odb.tcl. Also write 1_synth.odb/.sdc unconditionally in synth_odb.tcl and synth_syn.tcl instead of via the orfs_write_* wrappers: producing those files is the step's contract, and the WRITE_ODB_AND_SDC_EACH_STAGE=0 no-op silently left downstream stages consuming the stale, non-canonicalized copy. Signed-off-by: Øyvind Harboe --- flow/scripts/synth.tcl | 4 ---- flow/scripts/synth_odb.tcl | 9 ++++++--- flow/scripts/synth_preamble.tcl | 1 - flow/scripts/synth_syn.tcl | 9 ++++++--- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/flow/scripts/synth.tcl b/flow/scripts/synth.tcl index fa79829904..8f22a9c99c 100644 --- a/flow/scripts/synth.tcl +++ b/flow/scripts/synth.tcl @@ -299,7 +299,3 @@ if { # Write synthesized design write_verilog -nohex -nodec $::env(RESULTS_DIR)/1_2_yosys.v -# One day a more sophisticated synthesis will write out a modified -# .sdc file after synthesis. For now, just copy the input .sdc file, -# making synthesis more consistent with other stages. -log_cmd exec cp $::env(SDC_FILE) $::env(RESULTS_DIR)/1_synth.sdc diff --git a/flow/scripts/synth_odb.tcl b/flow/scripts/synth_odb.tcl index 63bdc98cda..2345128895 100644 --- a/flow/scripts/synth_odb.tcl +++ b/flow/scripts/synth_odb.tcl @@ -28,9 +28,12 @@ report_design_area report_design_area_metrics source_step_tcl POST SYNTH -orfs_write_db $::env(RESULTS_DIR)/1_synth.odb +# Producing 1_synth.odb/.sdc is this step's contract: +# unconditional, no WRITE_ODB_AND_SDC_EACH_STAGE. +log_cmd write_db $::env(RESULTS_DIR)/1_synth.odb # Canonicalize 1_synth.sdc. The original SDC_FILE provided by # the user could have dependencies, such as sourcing util.tcl, # which are read in here and a canonicalized version is written -# out by OpenSTA that has no dependencies. -orfs_write_sdc $::env(RESULTS_DIR)/1_synth.sdc +# out by OpenSTA that has no dependencies. Sole writer of +# 1_synth.sdc. +log_cmd write_sdc -no_timestamp $::env(RESULTS_DIR)/1_synth.sdc diff --git a/flow/scripts/synth_preamble.tcl b/flow/scripts/synth_preamble.tcl index af43512c0f..58c016a7a8 100644 --- a/flow/scripts/synth_preamble.tcl +++ b/flow/scripts/synth_preamble.tcl @@ -14,7 +14,6 @@ if { [env_var_exists_and_non_empty SYNTH_NETLIST_FILES] } { # keep things simple we just use the creation date log_cmd exec cat {*}$::env(SYNTH_NETLIST_FILES) > $::env(RESULTS_DIR)/1_2_yosys.v } - log_cmd exec cp -p $::env(SDC_FILE) $::env(RESULTS_DIR)/1_synth.sdc if { [env_var_exists_and_non_empty CACHED_REPORTS] } { log_cmd exec cp -p {*}$::env(CACHED_REPORTS) $::env(REPORTS_DIR)/. } diff --git a/flow/scripts/synth_syn.tcl b/flow/scripts/synth_syn.tcl index f0469dce0a..f013fd5eae 100644 --- a/flow/scripts/synth_syn.tcl +++ b/flow/scripts/synth_syn.tcl @@ -68,9 +68,12 @@ log_cmd repair_design -pre_placement report_metrics 1 "synth" false false -orfs_write_db $::env(RESULTS_DIR)/1_synth.odb +# Producing 1_synth.odb/.sdc is this step's contract: +# unconditional, no WRITE_ODB_AND_SDC_EACH_STAGE. +log_cmd write_db $::env(RESULTS_DIR)/1_synth.odb # Canonicalize 1_synth.sdc. The original SDC_FILE provided by # the user could have dependencies, such as sourcing util.tcl, # which are read in here and a canonicalized version is written -# out by OpenSTA that has no dependencies. -orfs_write_sdc $::env(RESULTS_DIR)/1_synth.sdc +# out by OpenSTA that has no dependencies. Sole writer of +# 1_synth.sdc. +log_cmd write_sdc -no_timestamp $::env(RESULTS_DIR)/1_synth.sdc From 6da7ca56f32494420ae5b221c7f11c0bc1df1839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 7 Jul 2026 05:52:15 +0200 Subject: [PATCH 02/17] bazel: mirror tools/OpenROAD's rules_pycross Python 3.8 workaround MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sv-lang (via tools/OpenROAD) pulls in rules_pycross, whose toolchain extension fails with "Unknown Python version: 3.8" (transitively registered via or-tools -> pybind11_abseil; 3.8 was dropped from rules_python 2.0.0's MINOR_MAPPING). tools/OpenROAD/MODULE.bazel already carries the pycross.configure_environments workaround, but the extension configuration is only honored in the root module, so any bazel analysis in this repository fails without mirroring it here. Co-Authored-By: Claude Fable 5 Signed-off-by: Øyvind Harboe --- MODULE.bazel | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/MODULE.bazel b/MODULE.bazel index 1d21ea2f79..90161b23f2 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -83,6 +83,20 @@ register_toolchains( dev_dependency = True, ) +# sv-lang (via tools/OpenROAD) pulls in rules_pycross, whose toolchain +# extension fails on Python 3.8 (transitively registered via or-tools -> +# pybind11_abseil, dropped from rules_python 2.0.0's MINOR_MAPPING). +# pycross.configure_environments is root-honored only, so the workaround +# must be mirrored here from tools/OpenROAD/MODULE.bazel. +bazel_dep(name = "rules_pycross", version = "0.8.1", dev_dependency = True) + +pycross = use_extension( + "@rules_pycross//pycross/extensions:pycross.bzl", + "pycross", + dev_dependency = True, +) +pycross.configure_environments(python_versions = ["3.13"]) + python = use_extension("@rules_python//python/extensions:python.bzl", "python") python.toolchain( ignore_root_user_error = True, From 1268384d8bfd13902c32f0e1822d97f23e3368ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 7 Jul 2026 13:01:07 +0200 Subject: [PATCH 03/17] flow: gate every stage .odb/.sdc file product on WRITE_ODB_AND_SDC_EACH_STAGE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A sweep of flow/scripts for stage outputs produced outside the orfs_write_db/orfs_write_sdc helpers found four spots that break the single-process flow contract (WRITE_ODB_AND_SDC_EACH_STAGE=0 means stage scripts produce no .odb/.sdc files): - io_placement.tcl, fillcell.tcl and density_fill.tcl forward the input .odb with a raw exec cp on their skip paths. The cp is deliberate — much faster than re-serializing an unchanged database with write_db — so keep it, behind a new orfs_copy_db helper that is gated exactly like orfs_write_db. In a single-process flow the source file may not even exist. - write_ref_sdc.tcl (sourced unconditionally from global_route.tcl) wrote updated_clks.sdc with a raw write_sdc; use orfs_write_sdc. Not addressed here: detail_route.tcl exits the process in the SKIP_DETAILED_ROUTE branch, which would end a single-process flow early. Left for a follow-up since it produces no stray files. Co-Authored-By: Claude Fable 5 Signed-off-by: Øyvind Harboe --- flow/scripts/density_fill.tcl | 2 +- flow/scripts/fillcell.tcl | 2 +- flow/scripts/io_placement.tcl | 2 +- flow/scripts/util.tcl | 16 ++++++++++++++++ flow/scripts/write_ref_sdc.tcl | 2 +- 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/flow/scripts/density_fill.tcl b/flow/scripts/density_fill.tcl index 8b1d019eab..1f93ed4c52 100644 --- a/flow/scripts/density_fill.tcl +++ b/flow/scripts/density_fill.tcl @@ -11,7 +11,7 @@ if { $::env(USE_FILL) } { write_verilog $::env(RESULTS_DIR)/6_1_fill.v orfs_write_db $::env(RESULTS_DIR)/6_1_fill.odb } else { - log_cmd exec cp $::env(RESULTS_DIR)/5_route.odb $::env(RESULTS_DIR)/6_1_fill.odb + orfs_copy_db $::env(RESULTS_DIR)/5_route.odb $::env(RESULTS_DIR)/6_1_fill.odb # There is no 5_route.v file to copy } diff --git a/flow/scripts/fillcell.tcl b/flow/scripts/fillcell.tcl index b778ccb717..032c1ab58b 100644 --- a/flow/scripts/fillcell.tcl +++ b/flow/scripts/fillcell.tcl @@ -12,7 +12,7 @@ if { [env_var_exists_and_non_empty FILL_CELLS] } { report_design_area orfs_write_db $::env(RESULTS_DIR)/5_3_fillcell.odb } else { - log_cmd exec cp $::env(RESULTS_DIR)/5_2_route.odb $::env(RESULTS_DIR)/5_3_fillcell.odb + orfs_copy_db $::env(RESULTS_DIR)/5_2_route.odb $::env(RESULTS_DIR)/5_3_fillcell.odb } source_step_tcl POST FILLCELL diff --git a/flow/scripts/io_placement.tcl b/flow/scripts/io_placement.tcl index 2241d997e1..5ad95bad51 100644 --- a/flow/scripts/io_placement.tcl +++ b/flow/scripts/io_placement.tcl @@ -17,7 +17,7 @@ if { orfs_write_db $::env(RESULTS_DIR)/3_2_place_iop.odb write_pin_placement $::env(RESULTS_DIR)/3_2_place_iop.tcl } else { - log_cmd exec cp $::env(RESULTS_DIR)/3_1_place_gp_skip_io.odb $::env(RESULTS_DIR)/3_2_place_iop.odb + orfs_copy_db $::env(RESULTS_DIR)/3_1_place_gp_skip_io.odb $::env(RESULTS_DIR)/3_2_place_iop.odb } source_step_tcl POST IO_PLACEMENT diff --git a/flow/scripts/util.tcl b/flow/scripts/util.tcl index e2ea5ae6ee..5b7d98c8d1 100644 --- a/flow/scripts/util.tcl +++ b/flow/scripts/util.tcl @@ -309,6 +309,22 @@ proc orfs_write_sdc { output_file } { log_cmd write_sdc -no_timestamp $output_file } +# For a stage that leaves the design unchanged, copying the input .odb +# forward is much faster than serializing the in-memory database again +# with write_db. Gated like orfs_write_db: in a single-process flow +# (WRITE_ODB_AND_SDC_EACH_STAGE=0) the input file may not exist and no +# stage file should be produced. +# +# exec cp rather than Tcl's file copy: file copy sets the destination +# mtime with second resolution, which breaks make's timestamp-based +# up-to-date checks for fast builds. +proc orfs_copy_db { input_file output_file } { + if { !$::env(WRITE_ODB_AND_SDC_EACH_STAGE) } { + return + } + log_cmd exec cp $input_file $output_file +} + proc source_step_tcl { hook_type step_name } { set env_var "${hook_type}_${step_name}_TCL" source_env_var_if_exists $env_var diff --git a/flow/scripts/write_ref_sdc.tcl b/flow/scripts/write_ref_sdc.tcl index 82565f375f..8dcec9be5c 100644 --- a/flow/scripts/write_ref_sdc.tcl +++ b/flow/scripts/write_ref_sdc.tcl @@ -25,7 +25,7 @@ if { [llength $clks] == 0 } { create_clock -name $clk_name -period $ref_period $sources # Undo the set_propagated_clock so SDC at beginning of flow uses ideal clocks. unset_propagated_clock [all_clocks] - write_sdc -no_timestamp [file join $::env(RESULTS_DIR) "updated_clks.sdc"] + orfs_write_sdc [file join $::env(RESULTS_DIR) "updated_clks.sdc"] # Reset create_clock -name $clk_name -period $period $sources set_propagated_clock [all_clocks] From 506260d308ead0254a7fb1dfd5d02c7380ea312d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 7 Jul 2026 13:01:41 +0200 Subject: [PATCH 04/17] synth: keep the 1_synth.odb/.sdc writes behind WRITE_ODB_AND_SDC_EACH_STAGE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Partially revert the previous commit: synth_odb.tcl and synth_syn.tcl go back to orfs_write_db/orfs_write_sdc. The single-writer property and the removal of the mode-bit-preserving SDC_FILE copies stand, but the writes must stay gated: WRITE_ODB_AND_SDC_EACH_STAGE=0 is the contract of the single-process flow (all stages in one OpenROAD invocation, sourcing the stage scripts back to back), where no stage script may produce .odb/.sdc files — the follow-up commit adds a flow.tcl plus a test that enforces exactly that, and the unconditional writes trip it. With the writes gated the bazel-orfs side needs no patch either: 1_synth.odb/.sdc are produced by the do-1_synth action, which runs with the default WRITE_ODB_AND_SDC_EACH_STAGE=1. Co-Authored-By: Claude Fable 5 Signed-off-by: Øyvind Harboe --- flow/scripts/synth_odb.tcl | 6 ++---- flow/scripts/synth_syn.tcl | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/flow/scripts/synth_odb.tcl b/flow/scripts/synth_odb.tcl index 2345128895..36280fa761 100644 --- a/flow/scripts/synth_odb.tcl +++ b/flow/scripts/synth_odb.tcl @@ -28,12 +28,10 @@ report_design_area report_design_area_metrics source_step_tcl POST SYNTH -# Producing 1_synth.odb/.sdc is this step's contract: -# unconditional, no WRITE_ODB_AND_SDC_EACH_STAGE. -log_cmd write_db $::env(RESULTS_DIR)/1_synth.odb +orfs_write_db $::env(RESULTS_DIR)/1_synth.odb # Canonicalize 1_synth.sdc. The original SDC_FILE provided by # the user could have dependencies, such as sourcing util.tcl, # which are read in here and a canonicalized version is written # out by OpenSTA that has no dependencies. Sole writer of # 1_synth.sdc. -log_cmd write_sdc -no_timestamp $::env(RESULTS_DIR)/1_synth.sdc +orfs_write_sdc $::env(RESULTS_DIR)/1_synth.sdc diff --git a/flow/scripts/synth_syn.tcl b/flow/scripts/synth_syn.tcl index f013fd5eae..efe1f7779b 100644 --- a/flow/scripts/synth_syn.tcl +++ b/flow/scripts/synth_syn.tcl @@ -68,12 +68,10 @@ log_cmd repair_design -pre_placement report_metrics 1 "synth" false false -# Producing 1_synth.odb/.sdc is this step's contract: -# unconditional, no WRITE_ODB_AND_SDC_EACH_STAGE. -log_cmd write_db $::env(RESULTS_DIR)/1_synth.odb +orfs_write_db $::env(RESULTS_DIR)/1_synth.odb # Canonicalize 1_synth.sdc. The original SDC_FILE provided by # the user could have dependencies, such as sourcing util.tcl, # which are read in here and a canonicalized version is written # out by OpenSTA that has no dependencies. Sole writer of # 1_synth.sdc. -log_cmd write_sdc -no_timestamp $::env(RESULTS_DIR)/1_synth.sdc +orfs_write_sdc $::env(RESULTS_DIR)/1_synth.sdc From f75083ebd96bac2c903d62231c2af0585ddad6af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 7 Jul 2026 13:03:30 +0200 Subject: [PATCH 05/17] flow: single-process flow.tcl + gcd equivalence test with no-write assertion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ORFS supports running all OpenROAD stages in a single OpenROAD process by sourcing the per-stage scripts into one Tcl script: load_design skips reloading when a design is already in memory, KEEP_VARS=1 keeps variables across stages, and WRITE_ODB_AND_SDC_EACH_STAGE=0 turns the per-stage .odb/.sdc writes into no-ops. floorplan_to_place.tcl uses this for floorplan through place; this PR's discussion noted the mechanism has no automated test coverage. Add scripts/flow.tcl, which runs the yosys netlist through the final report in one process with WRITE_ODB_AND_SDC_EACH_STAGE=0: - flow_source sources a stage script and errors if it wrote any .odb/.sdc file - the single-process contract that this PR's first version broke with unconditional writes in synth_odb.tcl (this test catches that). - flow_write_db/flow_write_sdc write out, from the top level, exactly the .odb/.sdc file set the stage-per-process make flow produces (substep writes plus the Makefile do-copy products). Cover it with bazel tests on asap7/gcd: an orfs_run executes the whole flow off the yosys synthesis outputs under FLOW_VARIANT=single, and per-stage sh_tests byte-compare the stage-boundary .odb/.sdc files (synth through final) against the regular per-stage flow targets, reusing tools/OpenROAD's check_same.sh. Equal bytes proves the single-process mode is equivalent to the per-stage flow, not merely that it runs to completion. bazelisk test //flow/designs/asap7/gcd:gcd_single_flow_test Known divergence, deliberately left failing so it gets root-caused rather than papered over: from grt on, the grt-stage repair_timing inserts one extra buffer when continuing from in-memory CTS state instead of reloading a byte-identical 4_cts.odb. SYNTH_USE_SYN is declared make-only (local_arguments): bazel-orfs's synth stage always runs the yosys flow and neither stages the Verilog sources nor sets VERILOG_FILES for the OpenROAD synthesis step, so the built-in synthesizer opt-in must not reach the bazel arguments (with it, bazelisk build :gcd_synth fails outright). Co-Authored-By: Claude Fable 5 Signed-off-by: Øyvind Harboe --- flow/designs/asap7/gcd/BUILD | 147 ++++++++++++++++++++++++- flow/designs/asap7/gcd/single_flow.tcl | 11 ++ flow/scripts/flow.tcl | 98 +++++++++++++++++ 3 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 flow/designs/asap7/gcd/single_flow.tcl create mode 100644 flow/scripts/flow.tcl diff --git a/flow/designs/asap7/gcd/BUILD b/flow/designs/asap7/gcd/BUILD index 527d6542e1..8be58cc48f 100644 --- a/flow/designs/asap7/gcd/BUILD +++ b/flow/designs/asap7/gcd/BUILD @@ -1,3 +1,148 @@ +load("@bazel-orfs//:openroad.bzl", "orfs_run") +load("@orfs_designs//:designs.bzl", "DESIGNS") +load("@rules_shell//shell:sh_test.bzl", "sh_test") load("//flow/designs:design.bzl", "design") -design(config = "config.mk") +# SYNTH_USE_SYN is make-only for now: bazel-orfs's synth stage always +# runs the yosys flow and neither stages the Verilog sources nor sets +# VERILOG_FILES for the OpenROAD synthesis step, so the built-in +# synthesizer opt-in must not reach the bazel arguments. +design( + config = "config.mk", + local_arguments = ["SYNTH_USE_SYN"], +) + +# Stage-boundary .odb/.sdc stems shared by the normal per-stage flow +# targets and the single-process flow below; the gcd_single_flow_test +# suite compares each of these files pairwise between the two flows. +SINGLE_FLOW_STAGES = { + "synth": "1_synth", + "floorplan": "2_floorplan", + "place": "3_place", + "cts": "4_cts", + "grt": "5_1_grt", + "route": "5_route", + "final": "6_final", +} + +# Substep files the single-process flow also writes, declared so the +# action keeps them and a human can inspect them on failure. +SINGLE_FLOW_EXTRA_OUTS = [ + "2_1_floorplan.odb", + "2_1_floorplan.sdc", + "2_2_floorplan_macro.odb", + "2_3_floorplan_tapcell.odb", + "2_4_floorplan_pdn.odb", + "3_1_place_gp_skip_io.odb", + "3_2_place_iop.odb", + "3_3_place_gp.odb", + "3_4_place_resized.odb", + "3_5_place_dp.odb", + "4_1_cts.odb", + "5_2_route.odb", + "5_3_fillcell.odb", + "6_1_fill.odb", + "6_1_fill.sdc", +] + +# The whole flow, yosys netlist through finish, as one Tcl script in a +# single OpenROAD process (scripts/flow.tcl) with +# WRITE_ODB_AND_SDC_EACH_STAGE=0: the stage scripts write no .odb/.sdc +# files (flow.tcl errors if one does), the top level writes every file +# the per-stage flow produces via explicit write_db/write_sdc. The +# synth stage config only carries synth-scoped variables, so the +# design's full argument set rides along; FLOW_VARIANT=single keeps the +# output paths clear of the regular stage targets' declared outputs in +# this package. +orfs_run( + name = "gcd_single_flow", + src = ":gcd_synth", + outs = [ + "results/asap7/gcd/single/{}.{}".format(stem, ext) + for stem in SINGLE_FLOW_STAGES.values() + for ext in [ + "odb", + "sdc", + ] + ] + [ + "results/asap7/gcd/single/" + name + for name in SINGLE_FLOW_EXTRA_OUTS + ], + arguments = { + k: v + for k, v in DESIGNS["asap7/gcd"]["arguments"].items() + if k != "SYNTH_USE_SYN" + } | { + "FLOW_VARIANT": "single", + }, + script = ":single_flow.tcl", + variant = "single", +) + +# Normal-flow stage outputs, one filegroup per compared file. +[ + filegroup( + name = "gcd_base_{}_{}".format(stage, ext), + srcs = [":gcd_" + stage], + output_group = "{}.{}".format(stem, ext), + ) + for stage, stem in SINGLE_FLOW_STAGES.items() + for ext in [ + "odb", + "sdc", + ] +] + +# Byte-compare each stage boundary's .odb/.sdc between the per-stage +# flow and the single-process flow: equal files prove the +# single-process mode is equivalent to the per-stage flow, not merely +# that it runs to completion. check_same.sh takes file pairs: +# file_a file_b ... (.sdc pair first: check_same.sh stops at a binary +# diff, and the .sdc verdict should be reported before that). +# +# Known divergence, deliberately left failing so it gets root-caused +# rather than papered over: from the grt stage on, the grt-stage +# repair_timing inserts one extra buffer (8 vs 7 on asap7/gcd) when the +# process continues from the in-memory CTS state instead of reloading +# 4_cts.odb, even though the 4_cts.odb/.sdc it starts from are +# byte-identical (all .sdc match through the whole flow). +[ + sh_test( + name = "gcd_single_flow_{}_test".format(stage), + srcs = ["@openroad//test/orfs:check_same.sh"], + args = [ + arg + for ext in [ + "sdc", + "odb", + ] + for arg in [ + "$(location :gcd_base_{}_{})".format(stage, ext), + "$(location :results/asap7/gcd/single/{}.{})".format(stem, ext), + ] + ], + data = [ + ":gcd_base_{}_{}".format(stage, ext) + for ext in [ + "sdc", + "odb", + ] + ] + [ + ":results/asap7/gcd/single/{}.{}".format(stem, ext) + for ext in [ + "sdc", + "odb", + ] + ], + tags = ["orfs"], + ) + for stage, stem in SINGLE_FLOW_STAGES.items() +] + +test_suite( + name = "gcd_single_flow_test", + tests = [ + ":gcd_single_flow_{}_test".format(stage) + for stage in SINGLE_FLOW_STAGES + ], +) diff --git a/flow/designs/asap7/gcd/single_flow.tcl b/flow/designs/asap7/gcd/single_flow.tcl new file mode 100644 index 0000000000..4e6ab51b0a --- /dev/null +++ b/flow/designs/asap7/gcd/single_flow.tcl @@ -0,0 +1,11 @@ +# Seed this variant's results dir with the yosys synthesis outputs, +# then run the whole flow in this single OpenROAD process. Under +# bazel-orfs the synthesis results are staged in the src stage's +# variant folder and its odb is exposed via ODB_FILE, while RESULTS_DIR +# points at this run's own variant folder. +file mkdir $::env(RESULTS_DIR) +set src_results [file dirname $::env(ODB_FILE)] +file copy -force $src_results/1_2_yosys.v $::env(RESULTS_DIR)/1_2_yosys.v +file copy -force $src_results/1_2_yosys.sdc $::env(RESULTS_DIR)/1_2_yosys.sdc + +source $::env(SCRIPTS_DIR)/flow.tcl diff --git a/flow/scripts/flow.tcl b/flow/scripts/flow.tcl new file mode 100644 index 0000000000..7ec1d7e69d --- /dev/null +++ b/flow/scripts/flow.tcl @@ -0,0 +1,98 @@ +# Runs all OpenROAD stages of the flow, from the yosys netlist through +# the final report, in a single OpenROAD process. +# +# With WRITE_ODB_AND_SDC_EACH_STAGE=0 the per-stage orfs_write_db / +# orfs_write_sdc / orfs_copy_db helpers are no-ops, so this top level is +# the sole writer of .odb/.sdc files. flow_write_db/flow_write_sdc +# produce the same file set as the stage-per-process make flow (the +# extra writes after a stage's own mirror the Makefile do-copy +# recipes), and flow_source enforces that stage scripts write no +# .odb/.sdc files themselves. + +# Enable keeping variables between stages +set ::env(KEEP_VARS) 1 +set ::env(WRITE_ODB_AND_SDC_EACH_STAGE) 0 + +set ::flow_expected [glob -nocomplain -directory $::env(RESULTS_DIR) *.odb *.sdc] + +proc flow_source { script } { + # Source in the global scope: stage scripts set top-level variables + # (e.g. util.tcl's global_route_congestion_report) that later stages + # read via $::, which a proc-scoped source would silently shadow. + uplevel #0 [list source $::env(SCRIPTS_DIR)/$script] + foreach f [glob -nocomplain -directory $::env(RESULTS_DIR) *.odb *.sdc] { + if { [lsearch -exact $::flow_expected $f] == -1 } { + error "$script wrote $f: with WRITE_ODB_AND_SDC_EACH_STAGE=0 stage\ + scripts must not write .odb/.sdc files" + } + } +} + +proc flow_write_db { name } { + set path $::env(RESULTS_DIR)/$name + log_cmd write_db $path + lappend ::flow_expected $path +} + +proc flow_write_sdc { name } { + set path $::env(RESULTS_DIR)/$name + log_cmd write_sdc -no_timestamp $path + lappend ::flow_expected $path +} + +# Synthesis odb (from the yosys netlist) +flow_source synth_odb.tcl +flow_write_db 1_synth.odb +flow_write_sdc 1_synth.sdc + +# Floorplan +flow_source floorplan.tcl +flow_write_db 2_1_floorplan.odb +flow_write_sdc 2_1_floorplan.sdc +flow_source macro_place.tcl +flow_write_db 2_2_floorplan_macro.odb +flow_source tapcell.tcl +flow_write_db 2_3_floorplan_tapcell.odb +flow_source pdn.tcl +flow_write_db 2_4_floorplan_pdn.odb +flow_write_db 2_floorplan.odb +flow_write_sdc 2_floorplan.sdc + +# Place +flow_source global_place_skip_io.tcl +flow_write_db 3_1_place_gp_skip_io.odb +flow_source io_placement.tcl +flow_write_db 3_2_place_iop.odb +flow_source global_place.tcl +flow_write_db 3_3_place_gp.odb +flow_source resize.tcl +flow_write_db 3_4_place_resized.odb +flow_source detail_place.tcl +flow_write_db 3_5_place_dp.odb +flow_write_db 3_place.odb +flow_write_sdc 3_place.sdc + +# CTS +flow_source cts.tcl +flow_write_db 4_1_cts.odb +flow_write_db 4_cts.odb +flow_write_sdc 4_cts.sdc + +# Route +flow_source global_route.tcl +flow_write_db 5_1_grt.odb +flow_write_sdc 5_1_grt.sdc +flow_source detail_route.tcl +flow_write_db 5_2_route.odb +flow_source fillcell.tcl +flow_write_db 5_3_fillcell.odb +flow_write_db 5_route.odb +flow_write_sdc 5_route.sdc + +# Finish +flow_source density_fill.tcl +flow_write_db 6_1_fill.odb +flow_write_sdc 6_1_fill.sdc +flow_write_sdc 6_final.sdc +flow_source final_report.tcl +flow_write_db 6_final.odb From 640e2057a1dcd031df767b73e7e4251cda8e8504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 7 Jul 2026 15:18:59 +0200 Subject: [PATCH 06/17] flow: fix the grt-stage single-process divergence via an OpenSTA patch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gcd_single_flow grt/route/final comparisons failed: the grt-stage repair_timing inserted one extra buffer when the flow continued from in-memory CTS state instead of reloading the byte-identical 4_cts.odb. Root cause, found by bisecting the single-process flow stage by stage and then diffing resistance-aware routing decisions: pins of a freshly created instance that are never connected — the dangling outputs of the dummy-load buffers (clkload*) CTS inserts to balance the clock tree — get no connectPinAfter call, so they never enter the OpenSTA search endpoint set and their required times are never seeded. Once the clock is propagated, such a pin reports slack = 0 - arrival (-195 ps on gcd) instead of +infinity. A process that reads the same design from disk seeds requireds for every endpoint in the full scan, so it sees +infinity. global_route -resistance_aware consumes net slacks to pick and order the resistance-aware net set, so the clock nets sorted differently and routing diverged before any repair ran; the repair_timing buffer delta was downstream fallout. Fix in Sta::makeInstanceAfter: register the new pin vertices as endpoint candidates (and initialize makePinVertices' return values, which are left unassigned for power/ground pins). Carried as patches/openroad/0001-sta-register-new-instance-pin-vertices-as- endpoints.patch, applied by fetching openroad via git_override pinned to the exact commit the tools/OpenROAD submodule records (with submodules, so the patch reaches src/sta); local_path_override does not accept patches. Revert to local_path_override when the fix lands upstream. With the patch, synth/floorplan/place/cts/grt are byte-identical between the single-process and per-stage flows. route and final still diverge from a second, independent cause (detail route behaves differently in-process than freshly loaded despite byte-identical 5_1_grt.odb inputs); their tests stay failing on purpose and the BUILD-file comment records the state. Ruled out along the way: global rand() state (cts/gpl do perturb it, but nothing in rsz/grt/est/dbSta consumes bare rand()), grt's shuffle seed (off by default), SDC write precision (gcd constraints are exact integers), rsz static move counters (report-only), odb unique-name counters (serialized). Co-Authored-By: Claude Fable 5 Signed-off-by: Øyvind Harboe --- MODULE.bazel | 28 +++++++++++++++++-- flow/designs/asap7/gcd/BUILD | 19 +++++++++---- ...w-instance-pin-vertices-as-endpoints.patch | 25 +++++++++++++++++ patches/openroad/BUILD.bazel | 9 ++++++ 4 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 patches/openroad/0001-sta-register-new-instance-pin-vertices-as-endpoints.patch create mode 100644 patches/openroad/BUILD.bazel diff --git a/MODULE.bazel b/MODULE.bazel index 90161b23f2..ca5f4d45ec 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -20,10 +20,34 @@ bazel_dep(name = "rules_shell", version = "0.6.1") # time. bazel_dep(name = "toolchains_llvm", version = "1.5.0", dev_dependency = True) + +# openroad is normally consumed straight from the tools/OpenROAD +# submodule via local_path_override. While OpenROAD fixes are carried +# as patches in //patches/openroad (local_path_override does not accept +# patches), it is fetched via git_override instead, pinned to the exact +# commit the submodule records so make (submodule) and bazel (fetched + +# patched) build the same base. Note that bazel builds the fetched +# source, not the live submodule tree, while this override is in place; +# revert to local_path_override when the patches land upstream. +# +# patches/openroad/0001-sta-register-new-instance-pin-vertices-as-endpoints.patch: +# pins of a freshly created instance that are never connected (eg the +# dangling outputs of CTS dummy-load buffers) never enter the STA +# endpoint set, so their requireds are never seeded and a propagated +# clock arrival yields a bogus negative net slack in the process that +# inserted them — resistance-aware global routing then routes +# differently in a single-process flow than in a stage-per-process +# flow. Applies to the src/sta submodule content (init_submodules). bazel_dep(name = "openroad", dev_dependency = True) -local_path_override( +git_override( module_name = "openroad", - path = "tools/OpenROAD", + commit = "f4e5e40f4755f135fea8e567c8c6822136b48729", + init_submodules = True, + patch_strip = 1, + patches = [ + "//patches/openroad:0001-sta-register-new-instance-pin-vertices-as-endpoints.patch", + ], + remote = "https://github.com/The-OpenROAD-Project/OpenROAD.git", ) bazel_dep(name = "qt-bazel", dev_dependency = True) diff --git a/flow/designs/asap7/gcd/BUILD b/flow/designs/asap7/gcd/BUILD index 8be58cc48f..6b81a9ece2 100644 --- a/flow/designs/asap7/gcd/BUILD +++ b/flow/designs/asap7/gcd/BUILD @@ -100,12 +100,19 @@ orfs_run( # file_a file_b ... (.sdc pair first: check_same.sh stops at a binary # diff, and the .sdc verdict should be reported before that). # -# Known divergence, deliberately left failing so it gets root-caused -# rather than papered over: from the grt stage on, the grt-stage -# repair_timing inserts one extra buffer (8 vs 7 on asap7/gcd) when the -# process continues from the in-memory CTS state instead of reloading -# 4_cts.odb, even though the 4_cts.odb/.sdc it starts from are -# byte-identical (all .sdc match through the whole flow). +# The synth through grt boundaries are byte-identical. The grt stage +# used to diverge (an extra repair_timing buffer): STA endpoint state +# for CTS dummy-load pins differed between a process that inserted them +# and a process that read the same design from disk, and +# resistance-aware global routing consumed the resulting bogus net +# slacks — fixed by patches/openroad/0001-sta-register-new-instance- +# pin-vertices-as-endpoints.patch. +# +# Known remaining divergence, deliberately left failing so it gets +# root-caused rather than papered over: route and final differ even +# though the 5_1_grt.odb they start from is byte-identical — detail +# route behaves differently in-process than freshly loaded (suspected +# drt/pin-access module state carried over from the grt stage). [ sh_test( name = "gcd_single_flow_{}_test".format(stage), diff --git a/patches/openroad/0001-sta-register-new-instance-pin-vertices-as-endpoints.patch b/patches/openroad/0001-sta-register-new-instance-pin-vertices-as-endpoints.patch new file mode 100644 index 0000000000..a09d59e1f3 --- /dev/null +++ b/patches/openroad/0001-sta-register-new-instance-pin-vertices-as-endpoints.patch @@ -0,0 +1,25 @@ +diff --git a/src/sta/search/Sta.cc b/src/sta/search/Sta.cc +index 9eaa669e..2b7aa5a5 100644 +--- a/src/sta/search/Sta.cc ++++ b/src/sta/search/Sta.cc +@@ -4516,8 +4516,19 @@ Sta::makeInstanceAfter(const Instance *inst) + LibertyPort *lib_port = port_iter.next(); + Pin *pin = network_->findPin(inst, lib_port); + if (pin) { +- Vertex *vertex, *bidir_drvr_vertex; ++ // makePinVertices leaves the return values unassigned for ++ // power/ground pins. ++ Vertex *vertex = nullptr; ++ Vertex *bidir_drvr_vertex = nullptr; + graph_->makePinVertices(pin, vertex, bidir_drvr_vertex); ++ // Pins that are never connected (eg dangling outputs of ++ // buffers inserted as pure loads) get no connectPinAfter ++ // call, so register the new vertices as endpoint candidates ++ // here or their requireds are never seeded. ++ if (vertex) ++ search_->endpointInvalid(vertex); ++ if (bidir_drvr_vertex) ++ search_->endpointInvalid(bidir_drvr_vertex); + } + } + graph_->makeInstanceEdges(inst); diff --git a/patches/openroad/BUILD.bazel b/patches/openroad/BUILD.bazel new file mode 100644 index 0000000000..698cafd0f9 --- /dev/null +++ b/patches/openroad/BUILD.bazel @@ -0,0 +1,9 @@ +"""OpenROAD fixes carried as git_override patches. + +The openroad module is fetched via git_override (with submodules) and +these patches are applied on top; they may therefore also patch +submodule content such as src/sta. Drop each file when the fix lands +upstream and the pinned commit is bumped past it. +""" + +exports_files(glob(["*.patch"])) From 87b6286a0d24b25f6e957b0d0bda8e08fb53ed86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 7 Jul 2026 16:13:19 +0200 Subject: [PATCH 07/17] flow: fix the route-stage single-process divergence via a drt patch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the grt stage byte-identical, route and final still diverged. Root causes, found with the same fresh-vs-in-process experiment pair on byte-identical 5_1_grt.odb inputs: - drt's incremental design update (reused when pin_access already ran in the same process) appends recreated nets and instances to its lists while a fresh read follows odb iteration order, and track assignment iterates those lists: the wire assignment diverged before detail routing started. - drt's final database update emitted each net's vias in a history-derived order (the track-assignment path already canonicalizes; the final path did not). Both fixed by patches/openroad/0002-drt-make-incremental-design- updates-match-a-fresh-read.patch: reorder insts/nets to odb order in updateDesign, and order vias canonically in the final write. The routed layout is now identical between the flows (equal DEF for 5_route). The route/final tests stay red on the .odb byte comparison for two fully root-caused reasons recorded in the BUILD file: odb id reuse history (byte equality after further edits needs canonical-id serialization) and the 6_final.odb write point (the per-stage flow writes it before the RCX extraction reorders every dbWire; the single-process flow writes at end of script). Co-Authored-By: Claude Fable 5 Signed-off-by: Øyvind Harboe --- MODULE.bazel | 7 + flow/designs/asap7/gcd/BUILD | 18 ++- ...al-design-updates-match-a-fresh-read.patch | 141 ++++++++++++++++++ 3 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 patches/openroad/0002-drt-make-incremental-design-updates-match-a-fresh-read.patch diff --git a/MODULE.bazel b/MODULE.bazel index ca5f4d45ec..f515c5557b 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -38,6 +38,12 @@ bazel_dep(name = "toolchains_llvm", version = "1.5.0", dev_dependency = True) # inserted them — resistance-aware global routing then routes # differently in a single-process flow than in a stage-per-process # flow. Applies to the src/sta submodule content (init_submodules). +# +# patches/openroad/0002-drt-make-incremental-design-updates-match-a-fresh-read.patch: +# detailed route iterated nets/instances in incremental-update order +# (appended) instead of odb order, and emitted vias in history-derived +# order — a single-process flow routed differently than a fresh +# process on a byte-identical database. bazel_dep(name = "openroad", dev_dependency = True) git_override( module_name = "openroad", @@ -46,6 +52,7 @@ git_override( patch_strip = 1, patches = [ "//patches/openroad:0001-sta-register-new-instance-pin-vertices-as-endpoints.patch", + "//patches/openroad:0002-drt-make-incremental-design-updates-match-a-fresh-read.patch", ], remote = "https://github.com/The-OpenROAD-Project/OpenROAD.git", ) diff --git a/flow/designs/asap7/gcd/BUILD b/flow/designs/asap7/gcd/BUILD index 6b81a9ece2..587b0c799b 100644 --- a/flow/designs/asap7/gcd/BUILD +++ b/flow/designs/asap7/gcd/BUILD @@ -108,11 +108,21 @@ orfs_run( # slacks — fixed by patches/openroad/0001-sta-register-new-instance- # pin-vertices-as-endpoints.patch. # +# The route stage used to route differently in-process (net/instance +# iteration order and via emission order in drt reflected the process +# history) — fixed by patches/openroad/0002-drt-make-incremental- +# design-updates-match-a-fresh-read.patch; the routed layout is now +# identical (equal DEF). +# # Known remaining divergence, deliberately left failing so it gets -# root-caused rather than papered over: route and final differ even -# though the 5_1_grt.odb they start from is byte-identical — detail -# route behaves differently in-process than freshly loaded (suspected -# drt/pin-access module state carried over from the grt stage). +# root-caused rather than papered over: the route/final .odb files +# still differ byte-wise from two causes. (1) odb serializes object +# ids, and a database that lived through create/delete cycles reuses +# freed ids where a freshly read one appends, so byte equality after +# further edits needs canonical-id serialization. (2) 6_final.odb is +# written mid-final_report in the per-stage flow but at end of script +# here, and the RCX extraction in between reorders every dbWire +# (odb orderWires), which also shows in the DEF. [ sh_test( name = "gcd_single_flow_{}_test".format(stage), diff --git a/patches/openroad/0002-drt-make-incremental-design-updates-match-a-fresh-read.patch b/patches/openroad/0002-drt-make-incremental-design-updates-match-a-fresh-read.patch new file mode 100644 index 0000000000..a5e9a43317 --- /dev/null +++ b/patches/openroad/0002-drt-make-incremental-design-updates-match-a-fresh-read.patch @@ -0,0 +1,141 @@ +diff --git a/src/drt/src/db/obj/frBlock.h b/src/drt/src/db/obj/frBlock.h +index ab21293087..3d642f631c 100644 +--- a/src/drt/src/db/obj/frBlock.h ++++ b/src/drt/src/db/obj/frBlock.h +@@ -9,6 +9,7 @@ + #include + #include + #include ++#include + #include + #include + +@@ -379,6 +380,62 @@ class frBlock : public frBlockObject + net->setId(id++); + } + } ++ // Reorder nets_/insts_ to the given permutation and renumber ids. ++ // A fresh odb read creates them in odb iteration order while ++ // incremental updates append, which changes downstream iteration ++ // order (eg track assignment) and with it the routing result. ++ void orderNets(const std::vector& order) ++ { ++ std::vector> prev; ++ prev.swap(nets_); ++ std::unordered_map index; ++ index.reserve(prev.size()); ++ for (size_t i = 0; i < prev.size(); i++) { ++ index[prev[i].get()] = i; ++ } ++ for (frNet* net : order) { ++ auto it = index.find(net); ++ if (it != index.end() && prev[it->second]) { ++ nets_.push_back(std::move(prev[it->second])); ++ } ++ } ++ // Anything not covered by order (there should be nothing) keeps ++ // its previous relative position at the end. ++ for (auto& net : prev) { ++ if (net) { ++ nets_.push_back(std::move(net)); ++ } ++ } ++ int id = 0; ++ for (const auto& net : nets_) { ++ net->setId(id++); ++ } ++ } ++ void orderInsts(const std::vector& order) ++ { ++ std::vector> prev; ++ prev.swap(insts_); ++ std::unordered_map index; ++ index.reserve(prev.size()); ++ for (size_t i = 0; i < prev.size(); i++) { ++ index[prev[i].get()] = i; ++ } ++ for (frInst* inst : order) { ++ auto it = index.find(inst); ++ if (it != index.end() && prev[it->second]) { ++ insts_.push_back(std::move(prev[it->second])); ++ } ++ } ++ for (auto& inst : prev) { ++ if (inst) { ++ insts_.push_back(std::move(inst)); ++ } ++ } ++ int id = 0; ++ for (const auto& inst : insts_) { ++ inst->setId(id++); ++ } ++ } + void addNet(std::unique_ptr in) + { + in->setId(nets_.size()); +diff --git a/src/drt/src/io/io.cpp b/src/drt/src/io/io.cpp +index 5929913cf7..126ff248f6 100644 +--- a/src/drt/src/io/io.cpp ++++ b/src/drt/src/io/io.cpp +@@ -3383,6 +3383,29 @@ void io::Parser::updateDesign() + netIn->clearOrigGuides(); + updateNetRouting(netIn, db_net); + } ++ // Match a fresh read: order insts and nets as odb iterates them. ++ // Incremental updates append recreated objects at the end while odb ++ // recycles ids, and downstream iteration order affects the routing ++ // result. ++ std::vector inst_order; ++ inst_order.reserve(getBlock()->getInsts().size()); ++ for (auto db_inst : block->getInsts()) { ++ auto inst = getBlock()->findInst(db_inst); ++ if (inst != nullptr) { ++ inst_order.push_back(inst); ++ } ++ } ++ getBlock()->orderInsts(inst_order); ++ std::vector net_order; ++ net_order.reserve(getBlock()->getNets().size()); ++ for (auto db_net : block->getNets()) { ++ // Special nets live in the snet list and keep their order. ++ auto net = getBlock()->findNet(db_net->getName()); ++ if (net != nullptr) { ++ net_order.push_back(net); ++ } ++ } ++ getBlock()->orderNets(net_order); + getDesign()->getRegionQuery()->init(); + getDesign()->getRegionQuery()->initDRObj(); + } +@@ -3742,6 +3765,31 @@ void io::Writer::fillConnFigs(bool isTA, int verbose) + for (auto& it : connFigs_) { + mergeSplitConnFigs(it.second); + } ++ } else { ++ // The via order within a net is semantically irrelevant to the ++ // written wire but reflects the routing history (a process that ++ // ran earlier stages in memory orders them differently than one ++ // that read the same design from disk). Order vias canonically so ++ // the written database does not depend on that history; path segs ++ // keep their order. ++ for (auto& it : connFigs_) { ++ it.second.sort([](const std::shared_ptr& lhs, ++ const std::shared_ptr& rhs) { ++ const bool lhs_via = lhs->typeId() == frcVia; ++ const bool rhs_via = rhs->typeId() == frcVia; ++ if (!lhs_via || !rhs_via) { ++ return false; ++ } ++ const auto lv = std::static_pointer_cast(lhs); ++ const auto rv = std::static_pointer_cast(rhs); ++ return std::make_tuple(lv->getViaDef()->getCutLayerNum(), ++ lv->getOrigin().x(), ++ lv->getOrigin().y()) ++ < std::make_tuple(rv->getViaDef()->getCutLayerNum(), ++ rv->getOrigin().x(), ++ rv->getOrigin().y()); ++ }); ++ } + } + } + From a9dededba42ecc7ccdf1ae72c3410dd0914a680e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 7 Jul 2026 23:22:48 +0200 Subject: [PATCH 08/17] flow: route stage byte-identical; the residue was stale bterm access points MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review feedback (maliberty) was right: odb free lists are serialized and reloaded, so the earlier explanation blaming id/freelist history for the remaining route-stage .odb difference was wrong. With byte-identical inputs both flows start from equivalent allocation state, which meant the databases had to contain genuinely different objects — and they did. Per-table stream accounting located the delta in the access point table: the per-stage flow's 5_route.odb contained 138 block-terminal access points where the single-process flow had 69. drt's updateDbAccessPoints appends bterm access points without destroying the previously written ones, so every process that reruns pin access on a design that already has them (each flow stage after pin_access first ran) duplicates them; the single-process flow's in-memory bookkeeping skips the unchanged terms and was the correct one. Fixed by patches/openroad/0003 (destroy previously written bterm access points before writing, in reverse creation order so recycled ids preserve the original assignment). The route stage is now byte-identical between the flows and its test passes; only the final stage remains, from the already-documented 6_final.odb write point vs RCX's orderWires re-encoding. Co-Authored-By: Claude Fable 5 Signed-off-by: Øyvind Harboe --- MODULE.bazel | 7 +++++++ flow/designs/asap7/gcd/BUILD | 16 +++++++------- ...do-not-duplicate-bterm-access-points.patch | 21 +++++++++++++++++++ 3 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 patches/openroad/0003-drt-do-not-duplicate-bterm-access-points.patch diff --git a/MODULE.bazel b/MODULE.bazel index f515c5557b..2757e27d10 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -44,6 +44,12 @@ bazel_dep(name = "toolchains_llvm", version = "1.5.0", dev_dependency = True) # (appended) instead of odb order, and emitted vias in history-derived # order — a single-process flow routed differently than a fresh # process on a byte-identical database. +# +# patches/openroad/0003-drt-do-not-duplicate-bterm-access-points.patch: +# every pin access update appended block-terminal access points +# without destroying the previously written ones, so staged flows +# accumulate stale access points at each stage that reruns pin access +# (69 -> 138 on asap7/gcd between grt and route). bazel_dep(name = "openroad", dev_dependency = True) git_override( module_name = "openroad", @@ -53,6 +59,7 @@ git_override( patches = [ "//patches/openroad:0001-sta-register-new-instance-pin-vertices-as-endpoints.patch", "//patches/openroad:0002-drt-make-incremental-design-updates-match-a-fresh-read.patch", + "//patches/openroad:0003-drt-do-not-duplicate-bterm-access-points.patch", ], remote = "https://github.com/The-OpenROAD-Project/OpenROAD.git", ) diff --git a/flow/designs/asap7/gcd/BUILD b/flow/designs/asap7/gcd/BUILD index 587b0c799b..75450c0c9c 100644 --- a/flow/designs/asap7/gcd/BUILD +++ b/flow/designs/asap7/gcd/BUILD @@ -114,14 +114,16 @@ orfs_run( # design-updates-match-a-fresh-read.patch; the routed layout is now # identical (equal DEF). # +# The route .odb byte difference was NOT an odb id/freelist issue +# (free lists serialize and reload): the per-stage flow accumulated +# stale block-terminal access points on every pin access rerun — fixed +# by patches/openroad/0003-drt-do-not-duplicate-bterm-access-points. +# patch; route is byte-identical now. +# # Known remaining divergence, deliberately left failing so it gets -# root-caused rather than papered over: the route/final .odb files -# still differ byte-wise from two causes. (1) odb serializes object -# ids, and a database that lived through create/delete cycles reuses -# freed ids where a freshly read one appends, so byte equality after -# further edits needs canonical-id serialization. (2) 6_final.odb is -# written mid-final_report in the per-stage flow but at end of script -# here, and the RCX extraction in between reorders every dbWire +# root-caused rather than papered over: 6_final.odb is written +# mid-final_report in the per-stage flow but at end of script here, +# and the RCX extraction in between reorders every dbWire # (odb orderWires), which also shows in the DEF. [ sh_test( diff --git a/patches/openroad/0003-drt-do-not-duplicate-bterm-access-points.patch b/patches/openroad/0003-drt-do-not-duplicate-bterm-access-points.patch new file mode 100644 index 0000000000..e2fd15d0f8 --- /dev/null +++ b/patches/openroad/0003-drt-do-not-duplicate-bterm-access-points.patch @@ -0,0 +1,21 @@ +diff --git a/src/drt/src/io/io.cpp b/src/drt/src/io/io.cpp +index 126ff248f6..7e591084bb 100644 +--- a/src/drt/src/io/io.cpp ++++ b/src/drt/src/io/io.cpp +@@ -4138,6 +4138,16 @@ void io::Writer::updateDbAccessPoints(odb::dbBlock* block, odb::dbTech* db_tech) + continue; + } + auto db_pin = (odb::dbBPin*) *db_pins.begin(); ++ // This branch appends: destroy previously written access points or ++ // a process that reads a design which already has bterm access ++ // points (any flow stage after pin_access ran) duplicates them on ++ // every pin access update. Destroy in reverse creation order so ++ // the recycled ids preserve the original assignment when the same ++ // access points are written back. ++ const auto old_aps = db_pin->getAccessPoints(); ++ for (auto it = old_aps.rbegin(); it != old_aps.rend(); ++it) { ++ odb::dbAccessPoint::destroy(*it); ++ } + auto& pin = pins[0]; + int j = 0; + int sz = pin->getNumPinAccess(); From 45677014744800d130df9983917fa9eabc7362d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 7 Jul 2026 23:37:04 +0200 Subject: [PATCH 09/17] flow: split final_report.tcl around the 6_final.odb write MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The final stage was the last remaining divergence in the single-process flow test: the per-stage flow writes 6_final.odb in the middle of final_report.tcl (after global_connect, before deleteRoutingObstructions and the RCX extraction whose odb orderWires re-encodes every dbWire), while the single-process flow could only write at end of script and captured the re-encoded state. Split final_report.tcl into final_connect.tcl (metrics stage, load, PRE hook, set_propagated_clock, global_connect) and final_outputs.tcl (everything after the write); final_report.tcl now sources the first, performs the gated 6_final.odb write, and sources the second, so the per-stage flow is unchanged. flow.tcl sources the two parts and performs the write itself at the identical point. With this, all seven stage boundaries (synth, floorplan, place, cts, grt, route, final) are byte-identical between the single-process and stage-per-process flows: //flow/designs/asap7/gcd:gcd_single_flow_test 7/7 PASSED Co-Authored-By: Claude Fable 5 Signed-off-by: Øyvind Harboe --- flow/designs/asap7/gcd/BUILD | 11 ++--- flow/scripts/final_connect.tcl | 10 +++++ flow/scripts/final_outputs.tcl | 59 ++++++++++++++++++++++++++ flow/scripts/final_report.tcl | 76 +++------------------------------- flow/scripts/flow.tcl | 6 ++- 5 files changed, 84 insertions(+), 78 deletions(-) create mode 100644 flow/scripts/final_connect.tcl create mode 100644 flow/scripts/final_outputs.tcl diff --git a/flow/designs/asap7/gcd/BUILD b/flow/designs/asap7/gcd/BUILD index 75450c0c9c..b8c3ddc518 100644 --- a/flow/designs/asap7/gcd/BUILD +++ b/flow/designs/asap7/gcd/BUILD @@ -120,11 +120,12 @@ orfs_run( # by patches/openroad/0003-drt-do-not-duplicate-bterm-access-points. # patch; route is byte-identical now. # -# Known remaining divergence, deliberately left failing so it gets -# root-caused rather than papered over: 6_final.odb is written -# mid-final_report in the per-stage flow but at end of script here, -# and the RCX extraction in between reorders every dbWire -# (odb orderWires), which also shows in the DEF. +# The final stage used to diverge because the per-stage flow writes +# 6_final.odb mid-final_report (before the RCX extraction reorders +# every dbWire via odb orderWires) while this flow wrote at end of +# script — resolved by splitting final_report.tcl around its write so +# flow.tcl writes at the same point. All seven stage boundaries are +# byte-identical between the two flows. [ sh_test( name = "gcd_single_flow_{}_test".format(stage), diff --git a/flow/scripts/final_connect.tcl b/flow/scripts/final_connect.tcl new file mode 100644 index 0000000000..22324613c6 --- /dev/null +++ b/flow/scripts/final_connect.tcl @@ -0,0 +1,10 @@ +utl::set_metrics_stage "finish__{}" +source $::env(SCRIPTS_DIR)/load.tcl +erase_non_stage_variables final +load_design 6_1_fill.odb 6_1_fill.sdc +source_step_tcl PRE FINAL_REPORT + +set_propagated_clock [all_clocks] + +# Ensure all OR created (rsz/cts) instances are connected +global_connect diff --git a/flow/scripts/final_outputs.tcl b/flow/scripts/final_outputs.tcl new file mode 100644 index 0000000000..b264b293b0 --- /dev/null +++ b/flow/scripts/final_outputs.tcl @@ -0,0 +1,59 @@ +# Delete routing obstructions for final DEF +source $::env(SCRIPTS_DIR)/deleteRoutingObstructions.tcl +deleteRoutingObstructions + +write_def $::env(RESULTS_DIR)/6_final.def +write_verilog $::env(RESULTS_DIR)/6_final.v \ + -remove_cells [find_physical_only_masters] + +# Run extraction and STA +if { + [env_var_exists_and_non_empty RCX_RULES] + && !$::env(SKIP_DETAILED_ROUTE) +} { + # RCX section + define_process_corner -ext_model_index 0 X + extract_parasitics -ext_model_file $::env(RCX_RULES) + + # Write Spef + write_spef $::env(RESULTS_DIR)/6_final.spef + file delete $::env(DESIGN_NAME).totCap + + # Read Spef for OpenSTA + read_spef $::env(RESULTS_DIR)/6_final.spef + + # Static IR drop analysis + if { [env_var_exists_and_non_empty PWR_NETS_VOLTAGES] } { + dict for {pwrNetName pwrNetVoltage} $::env(PWR_NETS_VOLTAGES) { + set_pdnsim_net_voltage -net ${pwrNetName} -voltage ${pwrNetVoltage} + analyze_power_grid -net ${pwrNetName} \ + -error_file $::env(REPORTS_DIR)/${pwrNetName}.rpt + } + } else { + puts "IR drop analysis for power nets is skipped because PWR_NETS_VOLTAGES is undefined" + } + if { [env_var_exists_and_non_empty GND_NETS_VOLTAGES] } { + dict for {gndNetName gndNetVoltage} $::env(GND_NETS_VOLTAGES) { + set_pdnsim_net_voltage -net ${gndNetName} -voltage ${gndNetVoltage} + analyze_power_grid -net ${gndNetName} \ + -error_file $::env(REPORTS_DIR)/${gndNetName}.rpt + } + } else { + puts "IR drop analysis for ground nets is skipped because GND_NETS_VOLTAGES is undefined" + } +} else { + puts "OpenRCX is not enabled for this platform." + puts "Falling back to global route-based estimates." + log_cmd estimate_parasitics -global_routing +} + +report_cell_usage + +report_metrics 6 "finish" + +source_step_tcl POST FINAL_REPORT + +# Save a final image if openroad is compiled with the gui +if { [ord::openroad_gui_compiled] } { + gui::show "source $::env(SCRIPTS_DIR)/save_images.tcl" false +} diff --git a/flow/scripts/final_report.tcl b/flow/scripts/final_report.tcl index d81acc986e..174e4b153a 100644 --- a/flow/scripts/final_report.tcl +++ b/flow/scripts/final_report.tcl @@ -1,72 +1,6 @@ -utl::set_metrics_stage "finish__{}" -source $::env(SCRIPTS_DIR)/load.tcl -erase_non_stage_variables final -load_design 6_1_fill.odb 6_1_fill.sdc -source_step_tcl PRE FINAL_REPORT - -set_propagated_clock [all_clocks] - -# Ensure all OR created (rsz/cts) instances are connected -global_connect - +# Split around the 6_final.odb write so a single-process flow +# (scripts/flow.tcl) can source the parts and perform the write itself +# at the same point. +source $::env(SCRIPTS_DIR)/final_connect.tcl orfs_write_db $::env(RESULTS_DIR)/6_final.odb - -# Delete routing obstructions for final DEF -source $::env(SCRIPTS_DIR)/deleteRoutingObstructions.tcl -deleteRoutingObstructions - -write_def $::env(RESULTS_DIR)/6_final.def -write_verilog $::env(RESULTS_DIR)/6_final.v \ - -remove_cells [find_physical_only_masters] - -# Run extraction and STA -if { - [env_var_exists_and_non_empty RCX_RULES] - && !$::env(SKIP_DETAILED_ROUTE) -} { - # RCX section - define_process_corner -ext_model_index 0 X - extract_parasitics -ext_model_file $::env(RCX_RULES) - - # Write Spef - write_spef $::env(RESULTS_DIR)/6_final.spef - file delete $::env(DESIGN_NAME).totCap - - # Read Spef for OpenSTA - read_spef $::env(RESULTS_DIR)/6_final.spef - - # Static IR drop analysis - if { [env_var_exists_and_non_empty PWR_NETS_VOLTAGES] } { - dict for {pwrNetName pwrNetVoltage} $::env(PWR_NETS_VOLTAGES) { - set_pdnsim_net_voltage -net ${pwrNetName} -voltage ${pwrNetVoltage} - analyze_power_grid -net ${pwrNetName} \ - -error_file $::env(REPORTS_DIR)/${pwrNetName}.rpt - } - } else { - puts "IR drop analysis for power nets is skipped because PWR_NETS_VOLTAGES is undefined" - } - if { [env_var_exists_and_non_empty GND_NETS_VOLTAGES] } { - dict for {gndNetName gndNetVoltage} $::env(GND_NETS_VOLTAGES) { - set_pdnsim_net_voltage -net ${gndNetName} -voltage ${gndNetVoltage} - analyze_power_grid -net ${gndNetName} \ - -error_file $::env(REPORTS_DIR)/${gndNetName}.rpt - } - } else { - puts "IR drop analysis for ground nets is skipped because GND_NETS_VOLTAGES is undefined" - } -} else { - puts "OpenRCX is not enabled for this platform." - puts "Falling back to global route-based estimates." - log_cmd estimate_parasitics -global_routing -} - -report_cell_usage - -report_metrics 6 "finish" - -source_step_tcl POST FINAL_REPORT - -# Save a final image if openroad is compiled with the gui -if { [ord::openroad_gui_compiled] } { - gui::show "source $::env(SCRIPTS_DIR)/save_images.tcl" false -} +source $::env(SCRIPTS_DIR)/final_outputs.tcl diff --git a/flow/scripts/flow.tcl b/flow/scripts/flow.tcl index 7ec1d7e69d..7b53057e9c 100644 --- a/flow/scripts/flow.tcl +++ b/flow/scripts/flow.tcl @@ -89,10 +89,12 @@ flow_write_db 5_3_fillcell.odb flow_write_db 5_route.odb flow_write_sdc 5_route.sdc -# Finish +# Finish. final_report.tcl is split around its 6_final.odb write so +# this top level can write at the same point. flow_source density_fill.tcl flow_write_db 6_1_fill.odb flow_write_sdc 6_1_fill.sdc flow_write_sdc 6_final.sdc -flow_source final_report.tcl +flow_source final_connect.tcl flow_write_db 6_final.odb +flow_source final_outputs.tcl From abb514cfca1f5f39aa03f9150019aa67b2088117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Wed, 8 Jul 2026 07:12:50 +0200 Subject: [PATCH 10/17] flow: file join for single-process flow output paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit flow_write_db/flow_write_sdc built paths with string concatenation; a trailing slash in RESULTS_DIR would produce dir//file, which never matches glob results in flow_source's lsearch -exact check. Co-Authored-By: Claude Fable 5 Signed-off-by: Øyvind Harboe --- flow/scripts/flow.tcl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flow/scripts/flow.tcl b/flow/scripts/flow.tcl index 7b53057e9c..c7eb1b56e2 100644 --- a/flow/scripts/flow.tcl +++ b/flow/scripts/flow.tcl @@ -29,13 +29,13 @@ proc flow_source { script } { } proc flow_write_db { name } { - set path $::env(RESULTS_DIR)/$name + set path [file join $::env(RESULTS_DIR) $name] log_cmd write_db $path lappend ::flow_expected $path } proc flow_write_sdc { name } { - set path $::env(RESULTS_DIR)/$name + set path [file join $::env(RESULTS_DIR) $name] log_cmd write_sdc -no_timestamp $path lappend ::flow_expected $path } From 7780f820d612f7dce067896596f3bf2123b73691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Wed, 8 Jul 2026 07:12:50 +0200 Subject: [PATCH 11/17] flow: don't exit the process on SKIP_DETAILED_ROUTE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit detail_route.tcl exited on the skip path, which is fine when each stage is its own process (OPENROAD_CMD passes -exit anyway) but kills the whole single-process flow before 5_2_route.odb and every later stage is written. Skip via if/else instead of exit. Co-Authored-By: Claude Fable 5 Signed-off-by: Øyvind Harboe --- flow/scripts/detail_route.tcl | 123 +++++++++++++++++----------------- 1 file changed, 61 insertions(+), 62 deletions(-) diff --git a/flow/scripts/detail_route.tcl b/flow/scripts/detail_route.tcl index c3503558f6..907f4b708b 100644 --- a/flow/scripts/detail_route.tcl +++ b/flow/scripts/detail_route.tcl @@ -7,80 +7,79 @@ if { ![grt::have_routes] } { in DRC viewer to view congestion" } -if { $::env(SKIP_DETAILED_ROUTE) } { - orfs_write_db $::env(RESULTS_DIR)/5_2_route.odb - exit -} - -erase_non_stage_variables route -set_propagated_clock [all_clocks] +if { !$::env(SKIP_DETAILED_ROUTE) } { + erase_non_stage_variables route + set_propagated_clock [all_clocks] -set additional_args "" -append_env_var additional_args dbProcessNode -db_process_node 1 -append_env_var additional_args OR_SEED -or_seed 1 -append_env_var additional_args OR_K -or_k 1 -append_env_var additional_args VIA_IN_PIN_MIN_LAYER -via_in_pin_bottom_layer 1 -append_env_var additional_args VIA_IN_PIN_MAX_LAYER -via_in_pin_top_layer 1 -append_env_var additional_args DISABLE_VIA_GEN -disable_via_gen 0 -append_env_var additional_args REPAIR_PDN_VIA_LAYER -repair_pdn_vias 1 -append_env_var additional_args DETAILED_ROUTE_END_ITERATION -droute_end_iter 1 + set additional_args "" + append_env_var additional_args dbProcessNode -db_process_node 1 + append_env_var additional_args OR_SEED -or_seed 1 + append_env_var additional_args OR_K -or_k 1 + append_env_var additional_args VIA_IN_PIN_MIN_LAYER -via_in_pin_bottom_layer 1 + append_env_var additional_args VIA_IN_PIN_MAX_LAYER -via_in_pin_top_layer 1 + append_env_var additional_args DISABLE_VIA_GEN -disable_via_gen 0 + append_env_var additional_args REPAIR_PDN_VIA_LAYER -repair_pdn_vias 1 + append_env_var additional_args DETAILED_ROUTE_END_ITERATION -droute_end_iter 1 -append additional_args " -verbose 1" + append additional_args " -verbose 1" -# DETAILED_ROUTE_ARGS is used when debugging detailed, route, e.g. append -# "-droute_end_iter 5" to look at routing violations after only 5 iterations, -# speeding up iterations on a problem where detailed routing doesn't converge -# or converges slower than expected. -# -# If DETAILED_ROUTE_ARGS is not specified, save out progress report a -# few iterations after the first two iterations. The first couple of -# iterations would produce very large .drc reports without interesting -# information for the user. -# -# The idea is to have a policy that gives progress information soon without -# having to go spelunking in Tcl or modify configuration scripts, while -# not having to wait too long or generating large useless reports. + # DETAILED_ROUTE_ARGS is used when debugging detailed, route, e.g. append + # "-droute_end_iter 5" to look at routing violations after only 5 iterations, + # speeding up iterations on a problem where detailed routing doesn't converge + # or converges slower than expected. + # + # If DETAILED_ROUTE_ARGS is not specified, save out progress report a + # few iterations after the first two iterations. The first couple of + # iterations would produce very large .drc reports without interesting + # information for the user. + # + # The idea is to have a policy that gives progress information soon without + # having to go spelunking in Tcl or modify configuration scripts, while + # not having to wait too long or generating large useless reports. -set arguments [expr { - [env_var_exists_and_non_empty DETAILED_ROUTE_ARGS] ? $::env(DETAILED_ROUTE_ARGS) : - [concat $additional_args {-drc_report_iter_step 5}] -}] + set arguments [expr { + [env_var_exists_and_non_empty DETAILED_ROUTE_ARGS] ? $::env(DETAILED_ROUTE_ARGS) : + [concat $additional_args {-drc_report_iter_step 5}] + }] -set all_args [concat [list \ - -output_drc $::env(REPORTS_DIR)/5_route_drc.rpt \ - -output_maze $::env(RESULTS_DIR)/maze.log] \ - $arguments] + set all_args [concat [list \ + -output_drc $::env(REPORTS_DIR)/5_route_drc.rpt \ + -output_maze $::env(RESULTS_DIR)/maze.log] \ + $arguments] -log_cmd detailed_route {*}$all_args + log_cmd detailed_route {*}$all_args -if { - !$::env(SKIP_ANTENNA_REPAIR_POST_DRT) && - [env_var_exists_and_non_empty MAX_REPAIR_ANTENNAS_ITER_DRT] -} { - set repair_antennas_iters 1 - if { [repair_antennas] } { - detailed_route {*}$all_args + if { + !$::env(SKIP_ANTENNA_REPAIR_POST_DRT) && + [env_var_exists_and_non_empty MAX_REPAIR_ANTENNAS_ITER_DRT] + } { + set repair_antennas_iters 1 + if { [repair_antennas] } { + detailed_route {*}$all_args + } + while { + [check_antennas] && $repair_antennas_iters < $::env(MAX_REPAIR_ANTENNAS_ITER_DRT) + } { + repair_antennas + detailed_route {*}$all_args + incr repair_antennas_iters + } + } else { + utl::metric_int "antenna_diodes_count" -1 } - while { [check_antennas] && $repair_antennas_iters < $::env(MAX_REPAIR_ANTENNAS_ITER_DRT) } { - repair_antennas - detailed_route {*}$all_args - incr repair_antennas_iters - } -} else { - utl::metric_int "antenna_diodes_count" -1 -} -source_step_tcl POST DETAIL_ROUTE + source_step_tcl POST DETAIL_ROUTE -check_antennas -report_file $env(REPORTS_DIR)/drt_antennas.log + check_antennas -report_file $env(REPORTS_DIR)/drt_antennas.log -if { ![design_is_routed] } { - error "Design has unrouted nets." -} + if { ![design_is_routed] } { + error "Design has unrouted nets." + } -report_design_area + report_design_area -# Don't report metrics as we have not extracted parasitics, which will happen -# in final so there is no need to repeat it here. + # Don't report metrics as we have not extracted parasitics, which will + # happen in final so there is no need to repeat it here. +} orfs_write_db $::env(RESULTS_DIR)/5_2_route.odb From 898c39f6204c8ccfbb41eb615cea495e7fb3c2ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Wed, 8 Jul 2026 07:15:17 +0200 Subject: [PATCH 12/17] drt: strict weak ordering for the canonical via sort MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fillConnFigs comparator returned false whenever either side was a non-via, making every non-via equivalent to every via while vias were still ordered among themselves — not a strict weak ordering, so std::list::sort was UB. Partition non-vias before vias instead (list::sort is stable, so non-vias keep their relative order); each connFig is encoded as its own path in the written wire, so the partitioning does not change the layout. Regenerates patches 0002 (the fix) and 0003 (hunk offsets shift). Co-Authored-By: Claude Fable 5 Signed-off-by: Øyvind Harboe --- ...al-design-updates-match-a-fresh-read.patch | 21 ++++++++++++------- ...do-not-duplicate-bterm-access-points.patch | 4 ++-- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/patches/openroad/0002-drt-make-incremental-design-updates-match-a-fresh-read.patch b/patches/openroad/0002-drt-make-incremental-design-updates-match-a-fresh-read.patch index a5e9a43317..d6e1f723eb 100644 --- a/patches/openroad/0002-drt-make-incremental-design-updates-match-a-fresh-read.patch +++ b/patches/openroad/0002-drt-make-incremental-design-updates-match-a-fresh-read.patch @@ -74,7 +74,7 @@ index ab21293087..3d642f631c 100644 { in->setId(nets_.size()); diff --git a/src/drt/src/io/io.cpp b/src/drt/src/io/io.cpp -index 5929913cf7..126ff248f6 100644 +index 5929913cf7..a84a977739 100644 --- a/src/drt/src/io/io.cpp +++ b/src/drt/src/io/io.cpp @@ -3383,6 +3383,29 @@ void io::Parser::updateDesign() @@ -107,23 +107,28 @@ index 5929913cf7..126ff248f6 100644 getDesign()->getRegionQuery()->init(); getDesign()->getRegionQuery()->initDRObj(); } -@@ -3742,6 +3765,31 @@ void io::Writer::fillConnFigs(bool isTA, int verbose) +@@ -3742,6 +3765,36 @@ void io::Writer::fillConnFigs(bool isTA, int verbose) for (auto& it : connFigs_) { mergeSplitConnFigs(it.second); } + } else { + // The via order within a net is semantically irrelevant to the -+ // written wire but reflects the routing history (a process that -+ // ran earlier stages in memory orders them differently than one -+ // that read the same design from disk). Order vias canonically so -+ // the written database does not depend on that history; path segs -+ // keep their order. ++ // written wire (each connFig is encoded as its own path) but ++ // reflects the routing history (a process that ran earlier stages ++ // in memory orders them differently than one that read the same ++ // design from disk). Order canonically so the written database ++ // does not depend on that history: non-vias first, keeping their ++ // relative order (list::sort is stable), then vias by cut layer ++ // and origin. + for (auto& it : connFigs_) { + it.second.sort([](const std::shared_ptr& lhs, + const std::shared_ptr& rhs) { + const bool lhs_via = lhs->typeId() == frcVia; + const bool rhs_via = rhs->typeId() == frcVia; -+ if (!lhs_via || !rhs_via) { ++ if (lhs_via != rhs_via) { ++ return !lhs_via; ++ } ++ if (!lhs_via) { + return false; + } + const auto lv = std::static_pointer_cast(lhs); diff --git a/patches/openroad/0003-drt-do-not-duplicate-bterm-access-points.patch b/patches/openroad/0003-drt-do-not-duplicate-bterm-access-points.patch index e2fd15d0f8..cb7de1d8c9 100644 --- a/patches/openroad/0003-drt-do-not-duplicate-bterm-access-points.patch +++ b/patches/openroad/0003-drt-do-not-duplicate-bterm-access-points.patch @@ -1,8 +1,8 @@ diff --git a/src/drt/src/io/io.cpp b/src/drt/src/io/io.cpp -index 126ff248f6..7e591084bb 100644 +index a84a977739..74514c58b8 100644 --- a/src/drt/src/io/io.cpp +++ b/src/drt/src/io/io.cpp -@@ -4138,6 +4138,16 @@ void io::Writer::updateDbAccessPoints(odb::dbBlock* block, odb::dbTech* db_tech) +@@ -4143,6 +4143,16 @@ void io::Writer::updateDbAccessPoints(odb::dbBlock* block, odb::dbTech* db_tech) continue; } auto db_pin = (odb::dbBPin*) *db_pins.begin(); From 0b58bdcda73cd3e133ac36affe26d6f96c36321c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Wed, 8 Jul 2026 07:44:57 +0200 Subject: [PATCH 13/17] bazel: patch sed's gnulib memchr for glibc >= 2.42 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit glibc 2.42 defines memchr as a function-like _Generic macro under C23 (implied by _GNU_SOURCE), and sed's vendored gnulib only #undefs it for _LIBC builds, so lib/memchr.c no longer parses against current host glibc headers and every build of @sed (bazel-orfs -> abc -> readline -> ncurses -> sed) fails. Carry a one-hunk #undef patch via single_version_override until a fixed sed lands in BCR; 4.9.bcr.5 was tried and fails the same way under toolchains_llvm + host sysroot. Co-Authored-By: Claude Fable 5 Signed-off-by: Øyvind Harboe --- MODULE.bazel | 14 ++++++++++++++ ...memchr-undef-glibc-const-generic-macro.patch | 17 +++++++++++++++++ patches/sed/BUILD.bazel | 7 +++++++ 3 files changed, 38 insertions(+) create mode 100644 patches/sed/0001-memchr-undef-glibc-const-generic-macro.patch create mode 100644 patches/sed/BUILD.bazel diff --git a/MODULE.bazel b/MODULE.bazel index 2757e27d10..d4cc478409 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -21,6 +21,20 @@ bazel_dep(name = "rules_shell", version = "0.6.1") bazel_dep(name = "toolchains_llvm", version = "1.5.0", dev_dependency = True) +# sed is transitive (bazel-orfs -> abc -> readline -> ncurses -> sed). +# Its vendored gnulib only #undefs memchr for _LIBC builds, and glibc +# >= 2.42 defines memchr as a function-like _Generic macro under C23 +# (_GNU_SOURCE implies it), so lib/memchr.c no longer parses against +# current host glibc headers. Patch the #undef in; root-honored only, +# drop when a fixed sed lands in BCR. +single_version_override( + module_name = "sed", + patch_strip = 1, + patches = [ + "//patches/sed:0001-memchr-undef-glibc-const-generic-macro.patch", + ], +) + # openroad is normally consumed straight from the tools/OpenROAD # submodule via local_path_override. While OpenROAD fixes are carried # as patches in //patches/openroad (local_path_override does not accept diff --git a/patches/sed/0001-memchr-undef-glibc-const-generic-macro.patch b/patches/sed/0001-memchr-undef-glibc-const-generic-macro.patch new file mode 100644 index 0000000000..8f209bef39 --- /dev/null +++ b/patches/sed/0001-memchr-undef-glibc-const-generic-macro.patch @@ -0,0 +1,17 @@ +diff --git a/lib/memchr.c b/lib/memchr.c +index 0ca7b30..4483f58 100644 +--- a/lib/memchr.c ++++ b/lib/memchr.c +@@ -46,9 +46,9 @@ + #endif + + #undef __memchr +-#ifdef _LIBC +-# undef memchr +-#endif ++/* glibc >= 2.42 defines memchr as a function-like _Generic macro ++ (C23), which breaks the function definition below. */ ++#undef memchr + + #ifndef weak_alias + # define __memchr memchr diff --git a/patches/sed/BUILD.bazel b/patches/sed/BUILD.bazel new file mode 100644 index 0000000000..0f2bc72919 --- /dev/null +++ b/patches/sed/BUILD.bazel @@ -0,0 +1,7 @@ +"""sed fixes carried as single_version_override patches. + +Drop each file when a fixed sed module version lands in BCR and the +override in MODULE.bazel is removed. +""" + +exports_files(glob(["*.patch"])) From 93b999277a2b9058f83c495b22463573e179316e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Wed, 8 Jul 2026 08:06:46 +0200 Subject: [PATCH 14/17] deps: bump OpenROAD to master; adopt the hermetic-llvm toolchain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenROAD master merged the hermetic-llvm-toolchain work (#10812): a statically linked, zero-sysroot LLVM toolchain with no host compiler or /usr/include involved. Bump the pin from f4e5e40f to e8230044 and switch this repo from toolchains_llvm (host sysroot) to the same hermetic toolchain, mirroring the root-honored parts of tools/OpenROAD's MODULE.bazel/.bazelrc: - llvm 0.8.11 + register_toolchains(@llvm//toolchain:all), plus BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1 and rules_python bootstrap_impl=script in .bazelrc. - gnulib tool version overrides (sed 4.9.bcr.5, gawk 5.3.2.bcr.7, m4 1.4.21.bcr.4, bison 3.8.2.bcr.7 + wrapper-header patch) and the tcl_lang crypt.h patch; single_version_override is root-honored only, so each is copied here with its patch files. - bazel/slang-compat placeholder: openroad's new_local_repository(name="slang") resolves its relative path against the root workspace, so ORFS-as-root must provide the directory. The zero-sysroot toolchain takes host glibc out of the build entirely, which supersedes the sed gnulib memchr patch for glibc >= 2.42 — remove it. The abc, boost.context and scip overrides openroad carries are not needed in this graph (full test suite builds without them). patches/openroad/0001..0003 are unchanged: no src/sta or src/drt changes between the old and new pin, and the src/sta submodule pin is identical. All seven gcd_single_flow byte-equality tests pass on the new base. Co-Authored-By: Claude Fable 5 Signed-off-by: Øyvind Harboe --- .bazelrc | 9 +++ MODULE.bazel | 74 +++++++++++++------ bazel/slang-compat/README | 6 ++ ...ib-wrapper-headers-use-I-not-isystem.patch | 61 +++++++++++++++ patches/bison/BUILD.bazel | 7 ++ ...mchr-undef-glibc-const-generic-macro.patch | 17 ----- patches/sed/BUILD.bazel | 7 -- ...ude-vendored-minizip-crypt.h-by-path.patch | 20 +++++ patches/tcl_lang/BUILD.bazel | 7 ++ tools/OpenROAD | 2 +- 10 files changed, 164 insertions(+), 46 deletions(-) create mode 100644 bazel/slang-compat/README create mode 100644 patches/bison/0001-gnulib-wrapper-headers-use-I-not-isystem.patch create mode 100644 patches/bison/BUILD.bazel delete mode 100644 patches/sed/0001-memchr-undef-glibc-const-generic-macro.patch delete mode 100644 patches/sed/BUILD.bazel create mode 100644 patches/tcl_lang/0001-tclZipfs-include-vendored-minizip-crypt.h-by-path.patch create mode 100644 patches/tcl_lang/BUILD.bazel diff --git a/.bazelrc b/.bazelrc index 1d550b988f..e9f83d934f 100644 --- a/.bazelrc +++ b/.bazelrc @@ -11,6 +11,15 @@ common --registry=https://raw.githubusercontent.com/oharboe/bazel-central-regist build --incompatible_strict_action_env build --cxxopt "-std=c++20" --host_cxxopt "-std=c++20" +# Refuse to autodetect a local C++ toolchain (/usr/bin/gcc); the hermetic +# toolchain must win resolution. Mirrors tools/OpenROAD/.bazelrc. +common --repo_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1 + +# Launch py_binary via a shell stub instead of rules_python's legacy python +# stub, whose '#!/usr/bin/env python3' shebang needs a host python3 before +# the hermetic interpreter takes over. Mirrors tools/OpenROAD/.bazelrc. +common --@rules_python//python/config_settings:bootstrap_impl=script + # Don't track MODULE.bazel.lock. Resolved versions ride along with the # pinned BCR + git_override(commit=…) coordinates in MODULE.bazel and # downstream cache hits are what we actually care about. Tracking the diff --git a/MODULE.bazel b/MODULE.bazel index d4cc478409..961c934f50 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -19,19 +19,61 @@ bazel_dep(name = "rules_shell", version = "0.6.1") # our MODULE.bazel doesn't need to be patched at non-root consumption # time. -bazel_dep(name = "toolchains_llvm", version = "1.5.0", dev_dependency = True) - -# sed is transitive (bazel-orfs -> abc -> readline -> ncurses -> sed). -# Its vendored gnulib only #undefs memchr for _LIBC builds, and glibc -# >= 2.42 defines memchr as a function-like _Generic macro under C23 -# (_GNU_SOURCE implies it), so lib/memchr.c no longer parses against -# current host glibc headers. Patch the #undef in; root-honored only, -# drop when a fixed sed lands in BCR. +# hermetic-llvm (BCR module "llvm") provides statically linked LLVM +# binaries and a zero-sysroot cc_toolchain: no host compiler, linker or +# /usr/include involved, so host glibc changes cannot break the build. +# Mirrors tools/OpenROAD/MODULE.bazel (its llvm dep is dev-only, so it +# does not propagate here). +bazel_dep(name = "llvm", version = "0.8.11", dev_dependency = True) + +# The gnulib-based GNU tools (sed, gawk, m4, bison) vendor wrapper +# headers that must shadow libc headers, which breaks against +# hermetic-llvm's explicit libc -isystem entries (BCR #7642). Mirror +# tools/OpenROAD's fixed-version overrides (single_version_override is +# root-honored only); drop each when tools/OpenROAD drops its copy. +# +# sed is transitive (bazel-orfs -> abc -> readline -> ncurses -> sed); +# 4.9.bcr.5 carries the upstream fix (BCR #7915). single_version_override( module_name = "sed", + version = "4.9.bcr.5", +) + +# gawk is transitive (abc -> ncurses -> gawk; yosys -> gawk); +# 5.3.2.bcr.7 carries the upstream fix (BCR #7989). +single_version_override( + module_name = "gawk", + version = "5.3.2.bcr.7", +) + +# m4 is transitive (rules_bison/rules_flex -> m4). +single_version_override( + module_name = "m4", + version = "1.4.21.bcr.4", +) + +# bison is transitive (rules_bison -> bison). 3.8.2.bcr.7 fixes lib/ +# but src/ still picks up raw libc headers; the patch restores gnulib +# wrapper-header precedence via -I (to be proposed upstream as +# 3.8.2.bcr.8, then drop). +single_version_override( + module_name = "bison", patch_strip = 1, patches = [ - "//patches/sed:0001-memchr-undef-glibc-const-generic-macro.patch", + "//patches/bison:0001-gnulib-wrapper-headers-use-I-not-isystem.patch", + ], + version = "3.8.2.bcr.7", +) + +# tclZipfs.c's '#include "crypt.h"' picks up glibc's crypt.h instead of +# the vendored minizip one when glibc headers are explicit -isystem +# directories (hermetic-llvm zero-sysroot toolchain). Drop when fixed +# in a tcl_lang BCR release. +single_version_override( + module_name = "tcl_lang", + patch_strip = 1, + patches = [ + "//patches/tcl_lang:0001-tclZipfs-include-vendored-minizip-crypt.h-by-path.patch", ], ) @@ -67,7 +109,7 @@ single_version_override( bazel_dep(name = "openroad", dev_dependency = True) git_override( module_name = "openroad", - commit = "f4e5e40f4755f135fea8e567c8c6822136b48729", + commit = "e82300449bc71da46a0959fdb67ab6686f7b1240", init_submodules = True, patch_strip = 1, patches = [ @@ -120,18 +162,8 @@ git_override( # --- Extensions --- -llvm = use_extension( - "@toolchains_llvm//toolchain/extensions:llvm.bzl", - "llvm", - dev_dependency = True, -) -llvm.toolchain( - llvm_version = "20.1.8", -) -use_repo(llvm, "llvm_toolchain") - register_toolchains( - "@llvm_toolchain//:all", + "@llvm//toolchain:all", dev_dependency = True, ) diff --git a/bazel/slang-compat/README b/bazel/slang-compat/README new file mode 100644 index 0000000000..03051c7e29 --- /dev/null +++ b/bazel/slang-compat/README @@ -0,0 +1,6 @@ +Placeholder directory for the @slang alias build file to internally +manifest. tools/OpenROAD's MODULE.bazel declares +new_local_repository(name="slang", path="bazel/slang-compat"); the +relative path resolves against the root workspace, so when ORFS is the +root module consuming openroad via git_override the directory must +exist here too. Mirrors tools/OpenROAD/bazel/slang-compat. diff --git a/patches/bison/0001-gnulib-wrapper-headers-use-I-not-isystem.patch b/patches/bison/0001-gnulib-wrapper-headers-use-I-not-isystem.patch new file mode 100644 index 0000000000..feb3911152 --- /dev/null +++ b/patches/bison/0001-gnulib-wrapper-headers-use-I-not-isystem.patch @@ -0,0 +1,61 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Oyvind Harboe +Date: Mon, 6 Jul 2026 00:00:00 +0000 +Subject: [PATCH] BUILD: let gnulib wrapper headers shadow libc via -I + +Gnulib generates wrapper headers (lib/stdio.h, lib/fcntl.h, ...) that +must be found before the libc ones; they forward with #include_next and +add gnulib decorations (_GL_ATTRIBUTE_DEALLOC etc.) that lib/*-safer.h +and src/*.c rely on. The overlay's 'includes = ["lib"]' emits -isystem, +which loses to toolchains that inject libc headers as earlier -isystem +entries (e.g. the zero-sysroot hermetic-llvm BCR toolchain), and a dir +listed as both -I and -isystem is kept as -isystem, so the -I must +replace it, not merely accompany it. + +Plain -I outranks every -isystem, restoring autotools' AM_CPPFLAGS = +-I./lib ordering. $(BINDIR) covers the generated wrappers, the source +path the checked-in ones. + +Same class of fix as sed@4.9.bcr.5 (BCR #7915) and gawk@5.3.2.bcr.7 +(BCR #7989); to be proposed upstream as bison@3.8.2.bcr.8. +--- +diff --git a/BUILD.bazel b/BUILD.bazel +--- a/BUILD.bazel ++++ b/BUILD.bazel +@@ -711,7 +711,19 @@ + tags = ["manual"], + ) + +-BISON_COPTS = select({ ++# Gnulib's wrapper headers (generated lib/stdio.h, lib/fcntl.h, ... plus ++# the checked-in lib/*-safer.h helpers) must shadow the libc headers: they ++# forward via #include_next and add gnulib decorations ++# (_GL_ATTRIBUTE_DEALLOC etc.) that lib/ and src/ sources rely on. ++# `includes = ["lib"]` emits -isystem, which loses against toolchains that ++# inject libc headers as earlier -isystem entries (e.g. the zero-sysroot ++# hermetic-llvm BCR toolchain). Plain -I outranks every -isystem (and a dir ++# listed as both -I and -isystem stays -isystem, so the -isystem form must ++# go entirely). This restores autotools' AM_CPPFLAGS = -I./lib ordering. ++BISON_COPTS = [ ++ "-Iexternal/" + repo_name() + "/lib", ++ "-I$(BINDIR)/external/" + repo_name() + "/lib", ++] + select({ + "@rules_cc//cc/compiler:clang": [ + "-w", + "-std=c11", +@@ -813,7 +825,6 @@ + "src/scan-skel.c", + ":configmake_h_src", + ], +- includes = ["lib"], + textual_hdrs = [ + "lib/timevar.def", + "lib/printf-frexp.c", +@@ -997,6 +1008,7 @@ + "lib/main.c", + "lib/yyerror.c", + ], ++ copts = BISON_COPTS, + implementation_deps = [":headers"], + local_defines = LOCAL_DEFINES, + ) diff --git a/patches/bison/BUILD.bazel b/patches/bison/BUILD.bazel new file mode 100644 index 0000000000..9a8f39982d --- /dev/null +++ b/patches/bison/BUILD.bazel @@ -0,0 +1,7 @@ +"""bison fixes carried as single_version_override patches. + +Mirrored from tools/OpenROAD/bazel/bison-patches (overrides are +root-honored only). Drop when tools/OpenROAD drops its copy. +""" + +exports_files(glob(["*.patch"])) diff --git a/patches/sed/0001-memchr-undef-glibc-const-generic-macro.patch b/patches/sed/0001-memchr-undef-glibc-const-generic-macro.patch deleted file mode 100644 index 8f209bef39..0000000000 --- a/patches/sed/0001-memchr-undef-glibc-const-generic-macro.patch +++ /dev/null @@ -1,17 +0,0 @@ -diff --git a/lib/memchr.c b/lib/memchr.c -index 0ca7b30..4483f58 100644 ---- a/lib/memchr.c -+++ b/lib/memchr.c -@@ -46,9 +46,9 @@ - #endif - - #undef __memchr --#ifdef _LIBC --# undef memchr --#endif -+/* glibc >= 2.42 defines memchr as a function-like _Generic macro -+ (C23), which breaks the function definition below. */ -+#undef memchr - - #ifndef weak_alias - # define __memchr memchr diff --git a/patches/sed/BUILD.bazel b/patches/sed/BUILD.bazel deleted file mode 100644 index 0f2bc72919..0000000000 --- a/patches/sed/BUILD.bazel +++ /dev/null @@ -1,7 +0,0 @@ -"""sed fixes carried as single_version_override patches. - -Drop each file when a fixed sed module version lands in BCR and the -override in MODULE.bazel is removed. -""" - -exports_files(glob(["*.patch"])) diff --git a/patches/tcl_lang/0001-tclZipfs-include-vendored-minizip-crypt.h-by-path.patch b/patches/tcl_lang/0001-tclZipfs-include-vendored-minizip-crypt.h-by-path.patch new file mode 100644 index 0000000000..11bf1388c8 --- /dev/null +++ b/patches/tcl_lang/0001-tclZipfs-include-vendored-minizip-crypt.h-by-path.patch @@ -0,0 +1,20 @@ +tclZipfs.c includes the vendored minizip crypt.h as #include "crypt.h", +relying on compat/zlib/contrib/minizip preceding any system directory +that also has a crypt.h. Hermetic toolchains that provide glibc headers +as explicit -isystem directories (e.g. the hermetic-llvm cc_toolchain) +search glibc's crypt.h (Unix password hashing) first, and the build +fails with implicit declarations of init_keys/decrypt_byte/zencode. +Include the intended header by its unambiguous path relative to the +compat/zlib include root instead. + +--- a/generic/tclZipfs.c ++++ b/generic/tclZipfs.c +@@ -75,7 +75,7 @@ + } while (0) + + #include "zlib.h" +-#include "crypt.h" ++#include "contrib/minizip/crypt.h" + #include "zutil.h" + #include "crc32.h" + diff --git a/patches/tcl_lang/BUILD.bazel b/patches/tcl_lang/BUILD.bazel new file mode 100644 index 0000000000..73513a9ed2 --- /dev/null +++ b/patches/tcl_lang/BUILD.bazel @@ -0,0 +1,7 @@ +"""tcl_lang fixes carried as single_version_override patches. + +Mirrored from tools/OpenROAD/bazel/tcl-patches (overrides are +root-honored only). Drop when tools/OpenROAD drops its copy. +""" + +exports_files(glob(["*.patch"])) diff --git a/tools/OpenROAD b/tools/OpenROAD index f4e5e40f47..e82300449b 160000 --- a/tools/OpenROAD +++ b/tools/OpenROAD @@ -1 +1 @@ -Subproject commit f4e5e40f4755f135fea8e567c8c6822136b48729 +Subproject commit e82300449bc71da46a0959fdb67ab6686f7b1240 From f3fcd903ae465305d13c36e62a77f29d3b18f590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Wed, 8 Jul 2026 10:53:12 +0200 Subject: [PATCH 15/17] flow: update rules-base.json for QoR shift from the OpenROAD bump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OpenROAD bump to master e8230044 plus patches/openroad/0001..0003 (the sta endpoint-seeding fix that changes -resistance_aware global routing, and the two drt ordering/access-point fixes) shift QoR deterministically across designs, not just the gcd single-flow test target. Three designs regressed past their committed rules-base.json limits in the pr-head CI run (build #7, 93b999277): - nangate45/mempool_group: cts setup tns - nangate45/tinyRocket: cts/globalroute/finish setup tns - sky130hd/microwatt: globalroute/finish setup tns, detailed route antenna diode count Regenerate the three rules files with genRuleFile.py --failing --tighten (equivalent to `make update_rules`) against the metadata the pr-head job produced, so the failing limits are relaxed with the standard margin and incidentally-improved metrics are tightened. checkMetadata.py passes for all three against that metadata. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Øyvind Harboe --- .../nangate45/mempool_group/rules-base.json | 4 ++-- .../designs/nangate45/tinyRocket/rules-base.json | 8 ++++---- flow/designs/sky130hd/microwatt/rules-base.json | 16 ++++++++-------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/flow/designs/nangate45/mempool_group/rules-base.json b/flow/designs/nangate45/mempool_group/rules-base.json index ae9133af02..1539cdc5c9 100644 --- a/flow/designs/nangate45/mempool_group/rules-base.json +++ b/flow/designs/nangate45/mempool_group/rules-base.json @@ -42,7 +42,7 @@ "compare": ">=" }, "cts__timing__setup__tns": { - "value": -12400.0, + "value": -14900.0, "compare": ">=" }, "cts__timing__hold__ws": { @@ -62,7 +62,7 @@ "compare": ">=" }, "globalroute__timing__setup__tns": { - "value": -14800.0, + "value": -12400.0, "compare": ">=" }, "globalroute__timing__hold__ws": { diff --git a/flow/designs/nangate45/tinyRocket/rules-base.json b/flow/designs/nangate45/tinyRocket/rules-base.json index 2675dbcf47..e503a1e6a1 100644 --- a/flow/designs/nangate45/tinyRocket/rules-base.json +++ b/flow/designs/nangate45/tinyRocket/rules-base.json @@ -10,7 +10,7 @@ "level": "warning" }, "synth__design__instance__area__stdcell": { - "value": 59681.09, + "value": 58300.0, "compare": "<=" }, "constraints__clocks__count": { @@ -42,7 +42,7 @@ "compare": ">=" }, "cts__timing__setup__tns": { - "value": -32.4, + "value": -37.6, "compare": ">=" }, "cts__timing__hold__ws": { @@ -62,7 +62,7 @@ "compare": ">=" }, "globalroute__timing__setup__tns": { - "value": -53.9, + "value": -56.6, "compare": ">=" }, "globalroute__timing__hold__ws": { @@ -94,7 +94,7 @@ "compare": ">=" }, "finish__timing__setup__tns": { - "value": -46.6, + "value": -48.2, "compare": ">=" }, "finish__timing__hold__ws": { diff --git a/flow/designs/sky130hd/microwatt/rules-base.json b/flow/designs/sky130hd/microwatt/rules-base.json index f511144e80..e4fd15894a 100644 --- a/flow/designs/sky130hd/microwatt/rules-base.json +++ b/flow/designs/sky130hd/microwatt/rules-base.json @@ -38,11 +38,11 @@ "compare": "<=" }, "cts__timing__setup__ws": { - "value": -2.66, + "value": -2.6, "compare": ">=" }, "cts__timing__setup__tns": { - "value": -337.0, + "value": -331.0, "compare": ">=" }, "cts__timing__hold__ws": { @@ -54,7 +54,7 @@ "compare": ">=" }, "globalroute__antenna_diodes_count": { - "value": 1931, + "value": 1747, "compare": "<=" }, "globalroute__timing__setup__ws": { @@ -62,7 +62,7 @@ "compare": ">=" }, "globalroute__timing__setup__tns": { - "value": -295.0, + "value": -315.0, "compare": ">=" }, "globalroute__timing__hold__ws": { @@ -82,11 +82,11 @@ "compare": "<=" }, "detailedroute__antenna__violating__nets": { - "value": 1, + "value": 0, "compare": "<=" }, "detailedroute__antenna_diodes_count": { - "value": 1274, + "value": 1297, "compare": "<=" }, "finish__timing__setup__ws": { @@ -94,7 +94,7 @@ "compare": ">=" }, "finish__timing__setup__tns": { - "value": -258.0, + "value": -312.0, "compare": ">=" }, "finish__timing__hold__ws": { @@ -102,7 +102,7 @@ "compare": ">=" }, "finish__timing__hold__tns": { - "value": -5.76, + "value": -5.02, "compare": ">=" }, "finish__design__instance__area": { From 63e365c7d6372d14c773a46dc14248b6d71753dc Mon Sep 17 00:00:00 2001 From: Matt Liberty Date: Wed, 8 Jul 2026 20:49:42 +0000 Subject: [PATCH 16/17] update OR Signed-off-by: Matt Liberty --- tools/OpenROAD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/OpenROAD b/tools/OpenROAD index e82300449b..b678131181 160000 --- a/tools/OpenROAD +++ b/tools/OpenROAD @@ -1 +1 @@ -Subproject commit e82300449bc71da46a0959fdb67ab6686f7b1240 +Subproject commit b678131181c09a5a4c6d377a35abc6f157b96054 From adaa6a91ff53d433e27546ef12b414d10a2e9d93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 10 Jul 2026 08:14:16 +0200 Subject: [PATCH 17/17] bazel: drop OpenROAD pinning, keep local_path_override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The single-process flow test needs three OpenROAD fixes (two drt, one sta) to make the single-process and per-stage flows byte-identical. This PR carried them as patches under patches/openroad and, since local_path_override cannot apply patches, pinned OpenROAD via git_override(commit=..., patches=[...]) plus a tools/OpenROAD gitlink bump. Those fixes belong upstream, not in ORFS: the two drt fixes are tracked in their own OpenROAD PRs and the sta fix has already landed in parallaxsw/OpenSTA master. Pinning a specific OpenROAD hash also breaks the ORFS master policy of local_path_override, where bazel builds the live tools/OpenROAD checkout. Drop the pinning only: revert to local_path_override, remove patches/openroad/, and un-bump the tools/OpenROAD gitlink. bazel is a local testing workflow (not CI); local_path_override builds whatever OpenROAD the developer has checked out, so gcd_single_flow_test proves the flow scripts are correct once the tracked OpenROAD/OpenSTA fixes land. Everything else (hermetic-llvm toolchain, overrides, flow scripts, the single_flow test) is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Øyvind Harboe --- MODULE.bazel | 41 +---- ...w-instance-pin-vertices-as-endpoints.patch | 25 --- ...al-design-updates-match-a-fresh-read.patch | 146 ------------------ ...do-not-duplicate-bterm-access-points.patch | 21 --- patches/openroad/BUILD.bazel | 9 -- tools/OpenROAD | 2 +- 6 files changed, 3 insertions(+), 241 deletions(-) delete mode 100644 patches/openroad/0001-sta-register-new-instance-pin-vertices-as-endpoints.patch delete mode 100644 patches/openroad/0002-drt-make-incremental-design-updates-match-a-fresh-read.patch delete mode 100644 patches/openroad/0003-drt-do-not-duplicate-bterm-access-points.patch delete mode 100644 patches/openroad/BUILD.bazel diff --git a/MODULE.bazel b/MODULE.bazel index 961c934f50..894fdb6318 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -77,47 +77,10 @@ single_version_override( ], ) -# openroad is normally consumed straight from the tools/OpenROAD -# submodule via local_path_override. While OpenROAD fixes are carried -# as patches in //patches/openroad (local_path_override does not accept -# patches), it is fetched via git_override instead, pinned to the exact -# commit the submodule records so make (submodule) and bazel (fetched + -# patched) build the same base. Note that bazel builds the fetched -# source, not the live submodule tree, while this override is in place; -# revert to local_path_override when the patches land upstream. -# -# patches/openroad/0001-sta-register-new-instance-pin-vertices-as-endpoints.patch: -# pins of a freshly created instance that are never connected (eg the -# dangling outputs of CTS dummy-load buffers) never enter the STA -# endpoint set, so their requireds are never seeded and a propagated -# clock arrival yields a bogus negative net slack in the process that -# inserted them — resistance-aware global routing then routes -# differently in a single-process flow than in a stage-per-process -# flow. Applies to the src/sta submodule content (init_submodules). -# -# patches/openroad/0002-drt-make-incremental-design-updates-match-a-fresh-read.patch: -# detailed route iterated nets/instances in incremental-update order -# (appended) instead of odb order, and emitted vias in history-derived -# order — a single-process flow routed differently than a fresh -# process on a byte-identical database. -# -# patches/openroad/0003-drt-do-not-duplicate-bterm-access-points.patch: -# every pin access update appended block-terminal access points -# without destroying the previously written ones, so staged flows -# accumulate stale access points at each stage that reruns pin access -# (69 -> 138 on asap7/gcd between grt and route). bazel_dep(name = "openroad", dev_dependency = True) -git_override( +local_path_override( module_name = "openroad", - commit = "e82300449bc71da46a0959fdb67ab6686f7b1240", - init_submodules = True, - patch_strip = 1, - patches = [ - "//patches/openroad:0001-sta-register-new-instance-pin-vertices-as-endpoints.patch", - "//patches/openroad:0002-drt-make-incremental-design-updates-match-a-fresh-read.patch", - "//patches/openroad:0003-drt-do-not-duplicate-bterm-access-points.patch", - ], - remote = "https://github.com/The-OpenROAD-Project/OpenROAD.git", + path = "tools/OpenROAD", ) bazel_dep(name = "qt-bazel", dev_dependency = True) diff --git a/patches/openroad/0001-sta-register-new-instance-pin-vertices-as-endpoints.patch b/patches/openroad/0001-sta-register-new-instance-pin-vertices-as-endpoints.patch deleted file mode 100644 index a09d59e1f3..0000000000 --- a/patches/openroad/0001-sta-register-new-instance-pin-vertices-as-endpoints.patch +++ /dev/null @@ -1,25 +0,0 @@ -diff --git a/src/sta/search/Sta.cc b/src/sta/search/Sta.cc -index 9eaa669e..2b7aa5a5 100644 ---- a/src/sta/search/Sta.cc -+++ b/src/sta/search/Sta.cc -@@ -4516,8 +4516,19 @@ Sta::makeInstanceAfter(const Instance *inst) - LibertyPort *lib_port = port_iter.next(); - Pin *pin = network_->findPin(inst, lib_port); - if (pin) { -- Vertex *vertex, *bidir_drvr_vertex; -+ // makePinVertices leaves the return values unassigned for -+ // power/ground pins. -+ Vertex *vertex = nullptr; -+ Vertex *bidir_drvr_vertex = nullptr; - graph_->makePinVertices(pin, vertex, bidir_drvr_vertex); -+ // Pins that are never connected (eg dangling outputs of -+ // buffers inserted as pure loads) get no connectPinAfter -+ // call, so register the new vertices as endpoint candidates -+ // here or their requireds are never seeded. -+ if (vertex) -+ search_->endpointInvalid(vertex); -+ if (bidir_drvr_vertex) -+ search_->endpointInvalid(bidir_drvr_vertex); - } - } - graph_->makeInstanceEdges(inst); diff --git a/patches/openroad/0002-drt-make-incremental-design-updates-match-a-fresh-read.patch b/patches/openroad/0002-drt-make-incremental-design-updates-match-a-fresh-read.patch deleted file mode 100644 index d6e1f723eb..0000000000 --- a/patches/openroad/0002-drt-make-incremental-design-updates-match-a-fresh-read.patch +++ /dev/null @@ -1,146 +0,0 @@ -diff --git a/src/drt/src/db/obj/frBlock.h b/src/drt/src/db/obj/frBlock.h -index ab21293087..3d642f631c 100644 ---- a/src/drt/src/db/obj/frBlock.h -+++ b/src/drt/src/db/obj/frBlock.h -@@ -9,6 +9,7 @@ - #include - #include - #include -+#include - #include - #include - -@@ -379,6 +380,62 @@ class frBlock : public frBlockObject - net->setId(id++); - } - } -+ // Reorder nets_/insts_ to the given permutation and renumber ids. -+ // A fresh odb read creates them in odb iteration order while -+ // incremental updates append, which changes downstream iteration -+ // order (eg track assignment) and with it the routing result. -+ void orderNets(const std::vector& order) -+ { -+ std::vector> prev; -+ prev.swap(nets_); -+ std::unordered_map index; -+ index.reserve(prev.size()); -+ for (size_t i = 0; i < prev.size(); i++) { -+ index[prev[i].get()] = i; -+ } -+ for (frNet* net : order) { -+ auto it = index.find(net); -+ if (it != index.end() && prev[it->second]) { -+ nets_.push_back(std::move(prev[it->second])); -+ } -+ } -+ // Anything not covered by order (there should be nothing) keeps -+ // its previous relative position at the end. -+ for (auto& net : prev) { -+ if (net) { -+ nets_.push_back(std::move(net)); -+ } -+ } -+ int id = 0; -+ for (const auto& net : nets_) { -+ net->setId(id++); -+ } -+ } -+ void orderInsts(const std::vector& order) -+ { -+ std::vector> prev; -+ prev.swap(insts_); -+ std::unordered_map index; -+ index.reserve(prev.size()); -+ for (size_t i = 0; i < prev.size(); i++) { -+ index[prev[i].get()] = i; -+ } -+ for (frInst* inst : order) { -+ auto it = index.find(inst); -+ if (it != index.end() && prev[it->second]) { -+ insts_.push_back(std::move(prev[it->second])); -+ } -+ } -+ for (auto& inst : prev) { -+ if (inst) { -+ insts_.push_back(std::move(inst)); -+ } -+ } -+ int id = 0; -+ for (const auto& inst : insts_) { -+ inst->setId(id++); -+ } -+ } - void addNet(std::unique_ptr in) - { - in->setId(nets_.size()); -diff --git a/src/drt/src/io/io.cpp b/src/drt/src/io/io.cpp -index 5929913cf7..a84a977739 100644 ---- a/src/drt/src/io/io.cpp -+++ b/src/drt/src/io/io.cpp -@@ -3383,6 +3383,29 @@ void io::Parser::updateDesign() - netIn->clearOrigGuides(); - updateNetRouting(netIn, db_net); - } -+ // Match a fresh read: order insts and nets as odb iterates them. -+ // Incremental updates append recreated objects at the end while odb -+ // recycles ids, and downstream iteration order affects the routing -+ // result. -+ std::vector inst_order; -+ inst_order.reserve(getBlock()->getInsts().size()); -+ for (auto db_inst : block->getInsts()) { -+ auto inst = getBlock()->findInst(db_inst); -+ if (inst != nullptr) { -+ inst_order.push_back(inst); -+ } -+ } -+ getBlock()->orderInsts(inst_order); -+ std::vector net_order; -+ net_order.reserve(getBlock()->getNets().size()); -+ for (auto db_net : block->getNets()) { -+ // Special nets live in the snet list and keep their order. -+ auto net = getBlock()->findNet(db_net->getName()); -+ if (net != nullptr) { -+ net_order.push_back(net); -+ } -+ } -+ getBlock()->orderNets(net_order); - getDesign()->getRegionQuery()->init(); - getDesign()->getRegionQuery()->initDRObj(); - } -@@ -3742,6 +3765,36 @@ void io::Writer::fillConnFigs(bool isTA, int verbose) - for (auto& it : connFigs_) { - mergeSplitConnFigs(it.second); - } -+ } else { -+ // The via order within a net is semantically irrelevant to the -+ // written wire (each connFig is encoded as its own path) but -+ // reflects the routing history (a process that ran earlier stages -+ // in memory orders them differently than one that read the same -+ // design from disk). Order canonically so the written database -+ // does not depend on that history: non-vias first, keeping their -+ // relative order (list::sort is stable), then vias by cut layer -+ // and origin. -+ for (auto& it : connFigs_) { -+ it.second.sort([](const std::shared_ptr& lhs, -+ const std::shared_ptr& rhs) { -+ const bool lhs_via = lhs->typeId() == frcVia; -+ const bool rhs_via = rhs->typeId() == frcVia; -+ if (lhs_via != rhs_via) { -+ return !lhs_via; -+ } -+ if (!lhs_via) { -+ return false; -+ } -+ const auto lv = std::static_pointer_cast(lhs); -+ const auto rv = std::static_pointer_cast(rhs); -+ return std::make_tuple(lv->getViaDef()->getCutLayerNum(), -+ lv->getOrigin().x(), -+ lv->getOrigin().y()) -+ < std::make_tuple(rv->getViaDef()->getCutLayerNum(), -+ rv->getOrigin().x(), -+ rv->getOrigin().y()); -+ }); -+ } - } - } - diff --git a/patches/openroad/0003-drt-do-not-duplicate-bterm-access-points.patch b/patches/openroad/0003-drt-do-not-duplicate-bterm-access-points.patch deleted file mode 100644 index cb7de1d8c9..0000000000 --- a/patches/openroad/0003-drt-do-not-duplicate-bterm-access-points.patch +++ /dev/null @@ -1,21 +0,0 @@ -diff --git a/src/drt/src/io/io.cpp b/src/drt/src/io/io.cpp -index a84a977739..74514c58b8 100644 ---- a/src/drt/src/io/io.cpp -+++ b/src/drt/src/io/io.cpp -@@ -4143,6 +4143,16 @@ void io::Writer::updateDbAccessPoints(odb::dbBlock* block, odb::dbTech* db_tech) - continue; - } - auto db_pin = (odb::dbBPin*) *db_pins.begin(); -+ // This branch appends: destroy previously written access points or -+ // a process that reads a design which already has bterm access -+ // points (any flow stage after pin_access ran) duplicates them on -+ // every pin access update. Destroy in reverse creation order so -+ // the recycled ids preserve the original assignment when the same -+ // access points are written back. -+ const auto old_aps = db_pin->getAccessPoints(); -+ for (auto it = old_aps.rbegin(); it != old_aps.rend(); ++it) { -+ odb::dbAccessPoint::destroy(*it); -+ } - auto& pin = pins[0]; - int j = 0; - int sz = pin->getNumPinAccess(); diff --git a/patches/openroad/BUILD.bazel b/patches/openroad/BUILD.bazel deleted file mode 100644 index 698cafd0f9..0000000000 --- a/patches/openroad/BUILD.bazel +++ /dev/null @@ -1,9 +0,0 @@ -"""OpenROAD fixes carried as git_override patches. - -The openroad module is fetched via git_override (with submodules) and -these patches are applied on top; they may therefore also patch -submodule content such as src/sta. Drop each file when the fix lands -upstream and the pinned commit is bumped past it. -""" - -exports_files(glob(["*.patch"])) diff --git a/tools/OpenROAD b/tools/OpenROAD index b678131181..f5df0085c2 160000 --- a/tools/OpenROAD +++ b/tools/OpenROAD @@ -1 +1 @@ -Subproject commit b678131181c09a5a4c6d377a35abc6f157b96054 +Subproject commit f5df0085c27b1efaf0af4ab2950bfafa5a73bbb6