# ============================================================================
# retroDE_ps2 — minimal SDC for the DE25-Nano top_psmct32_raster_demo
# board wrapper (current as of Ch163 — real PLL IP retuned to 50 MHz
# outclk_0; first .sof that genuinely runs at 50 MHz on hardware).
# Scope: just enough constraints to close timing on the single
# `design_clk` domain the wrapper uses, and to mark the IO false paths
# Quartus would otherwise flag.
#
# Clock topology:
#   CLOCK2_50 (board pad, physical 50 MHz)
#     └─→ pll u_pll (Quartus IOPLL when USE_PLL_IP is defined;
#         de25_nano_pll_stub when not — sim-only pass-through)
#           └─→ design_clk = u_pll.outclk_0  (50 MHz under USE_PLL_IP)
#                 └─→ EE core / PCRTC / GIF / etc.
#
# Ch161-Ch163 timing-closure journey:
#   - Ch159 BRAM-backed VRAM rescued the Ch152 fit failure but STA
#     missed the 50 MHz target by −6.95 ns of setup slack.
#   - Ch160 retargeted the SDC to a 30 MHz profile so STA could
#     close, but the PLL was still a sim stub so the .sof was
#     profile-only on hardware.
#   - **Ch161** committed a real Quartus Agilex 5 IOPLL
#     (`synth/.../ip/pll.ip` → `pll.qip`, configured for 50 MHz
#     refclk → 30 MHz outclk_0) and defined `USE_PLL_IP=1` so the
#     wrapper's `\`ifdef USE_PLL_IP` instantiates the IP-generated
#     `pll` module. The .sof now genuinely ran at 30 MHz on hardware.
#   - **Ch162** retired the EE-core auto-inferred 32-bit DIVU
#     divider via `STRIP_HW_DIVIDER` (Fmax 30.74 → 33.6 MHz).
#   - **Ch163** retired the PCRTC magnification divider via
#     `STRIP_PCRTC_MAG_DIV` (Fmax 33.6 → 81.83 MHz at the 30 MHz
#     constraint), then **retuned `pll.ip` to 50 MHz outclk_0**.
#     STA closes at the 50 MHz target with +7.500 ns setup slack
#     and Fmax 80.0 MHz; the .sof produced now genuinely runs at
#     50 MHz on the DE25-Nano.
#
# Clock constraint plan:
#   - CLOCK2_50 period stays at 20.000 ns (the physical board
#     oscillator). This is the IP's refclk input.
#   - The IOPLL's auto-generated SDC (inside the .qip's pll.sdc)
#     declares the post-PLL clock at **50 MHz** via
#     `create_generated_clock` on the IP's output. Quartus picks
#     it up automatically when the .qip is included; nothing else
#     is needed here for the design_clk domain.
#
# Default (`USE_PLL_IP` undefined): the de25_nano_pll_stub is a sim
# pass-through — `design_clk` follows `CLOCK2_50` cycle-for-cycle and
# the 20 ns period below covers everything downstream.
#
# Reset bridge (Ch151 + Ch161 audit High #1): the rst_sync register
# async-asserts on `(ninit_done | ~pll_locked)` and synchronously
# deasserts on design_clk. Both async sources AND both stages of
# the synchronizer are covered by the `set_false_path -to *rst_sync[*]`
# line below — Ch161's audit High fix widened the wildcard from
# `*rst_sync[0]` to `*rst_sync[*]` after a real recovery violation
# fired on the second stage.
#
# Sourced from sibling retroDE_splash.sdc for false-path conventions
# (reset synchronizers, button/SW/LED IO false paths).
#
# Deferred to Ch165+:
#   - ADV7513 wake-up FSM + matching `set_output_delay` constraints
#     for HDMI_TX_*. Ch164 pinned the HDMI 24-bit RGB + sync data
#     path and constrained the outputs as `set_false_path -to`
#     (see the "HDMI ADV7513 output ports" block below). Ch165
#     replaces those false-paths with real output-delay constraints
#     once the ADV7513's setup/hold window is documented from its
#     bring-up datasheet pass.
#   - LPDDR4 / SDRAM / HPS clock groups.
# ============================================================================

set_time_format -unit ns -decimal_places 3
derive_clock_uncertainty

# ----------------------------------------------------------------------------
# Board clock — CLOCK2_50 is the physical 50 MHz oscillator that
# drives the IOPLL's refclk input. The PLL's output (`design_clk`,
# 50 MHz under USE_PLL_IP after the Ch163 retune) is constrained
# by the IP's auto-generated SDC inside
# `synth/.../ip/pll/pll.qip`.
# ----------------------------------------------------------------------------
create_clock -name CLOCK2_50 -period 20.000 [get_ports {CLOCK2_50}]

# CLOCK0_50 / CLOCK1_50 are pin-assigned but unused by this design;
# constrain them at their physical 50 MHz period so Quartus doesn't
# synthesize unconstrained-clock warnings in case a future chapter
# promotes them.
create_clock -name CLOCK0_50 -period 20.000 [get_ports {CLOCK0_50}]
create_clock -name CLOCK1_50 -period 20.000 [get_ports {CLOCK1_50}]

# CLOCK0_50 / CLOCK1_50 are asynchronous to CLOCK2_50 — separate
# board oscillator pads on the DE25-Nano. Mark them so Quartus
# doesn't try to optimize cross-paths that don't exist.
set_clock_groups -asynchronous \
    -group [get_clocks {CLOCK0_50}] \
    -group [get_clocks {CLOCK1_50}] \
    -group [get_clocks {CLOCK2_50}]

# ----------------------------------------------------------------------------
# Ch357 (Codex) — the HDMI I2C config FSM's internal work clock. I2C_HDMI_Config
# toggles mI2C_CTRL_CLK every 2501 CLOCK2_50 cycles (mI2C_CLK_DIV counts
# 0..CLK_Freq/I2C_Freq=2500, then resets+toggles), so its FULL period is a
# divide-by-5002 of CLOCK2_50. It clocks the I2C_Controller + the config FSM
# (.CLK(mI2C_CTRL_CLK) / posedge mI2C_CTRL_CLK), so it is a REAL generated clock —
# false-pathing the HDMI *pins* does NOT constrain it, which is why the last STA
# reported it unconstrained. Constrain it (~10 kHz class, trivially met) with a
# FATAL target-count check so a renamed register cannot silently orphan it. No
# extra cross-domain cut is needed: the module is a self-contained CLOCK2_50-rooted
# config sequencer whose only outputs are the already-false-pathed HDMI I2C pins.
# ----------------------------------------------------------------------------
set i2c_ctrl_clk_reg [get_registers -nowarn {u_hdmi_i2c|mI2C_CTRL_CLK}]
set i2c_ctrl_clk_n   [get_collection_size $i2c_ctrl_clk_reg]
if { $i2c_ctrl_clk_n != 1 } { error "Ch357 SDC FATAL: u_hdmi_i2c|mI2C_CTRL_CLK register count $i2c_ctrl_clk_n != 1 (renamed? generated clock would orphan and STA would flag it unconstrained)" }
create_generated_clock -name mI2C_CTRL_CLK -source [get_ports {CLOCK2_50}] -divide_by 5002 $i2c_ctrl_clk_reg
post_message -type info "Ch357 SDC: mI2C_CTRL_CLK generated clock (divide-by-5002 of CLOCK2_50) applied to $i2c_ctrl_clk_n register"

# ----------------------------------------------------------------------------
# Reset synchronizer — async assert, sync deassert. BOTH stages of
# `rst_sync` are async-cleared by `(ninit_done | ~pll_locked)`, not
# by clk; mark them false-path so the recovery analysis doesn't
# flag them under either source.
#
# Ch161 audit High #1 fix: the original constraint only covered
# `*rst_sync[0]`, which left rst_sync[1] under recovery analysis.
# Quartus reported a real recovery violation on
#   init_cnt[2] (refclk domain) → rst_sync[1].clrn (async-clear)
# of −0.079 ns at the 30 MHz outclk0 latch clock. The endpoint is
# an asynchronous-clear pin shared across both stages, so the
# correct constraint is the wildcard `*rst_sync[*]`. Recovery /
# removal analysis on the async-clear pin is masked the same way
# `set_false_path -to <regs>` masks the data-pin setup analysis —
# Quartus treats the reg's async-set/clear pin as a single endpoint
# under this constraint.
# ----------------------------------------------------------------------------
set_false_path -to [get_registers -nowarn {*rst_sync[*]}]

# HPS CORE_CTRL reset request is a CLOCK2_50-domain level sampled by the
# explicit two-flop hps_reset_sync chain in design_clk.  Cut only the first
# synchronizer stage; the second stage remains timed normally.  Without this
# CDC exception TimeQuest compares unrelated refclk/outclk edges and reports a
# placement-dependent multi-nanosecond hold violation on an otherwise correct
# synchronizer.
set hps_reset_sync0 [get_registers -nowarn {*hps_reset_sync[0]}]
set hps_reset_sync0_n [get_collection_size $hps_reset_sync0]
if { $hps_reset_sync0_n != 1 } { error "SDC FATAL: hps_reset_sync[0] count $hps_reset_sync0_n != 1" }
set_false_path -to $hps_reset_sync0
post_message -type info "SDC: HPS core-reset CDC cut to first synchronizer stage ($hps_reset_sync0_n register)"

# The runtime CLUT commit and OSD tile-write event are bridge/refclk-domain
# toggles received by explicit three-stage design-clock synchronizers.  Time
# only stages 1->2 internally; the asynchronous source->stage-0 paths have no
# meaningful setup/hold relationship.  Target counts are fatal so hierarchy
# drift cannot silently broaden or orphan these cuts.
set clut_commit_sync0 [get_registers -nowarn {u_runtime_clut|commit_sync[0]}]
set tile_toggle_sync0 [get_registers -nowarn {u_tile_cdc|toggle_sync[0]}]
set clut_commit_sync0_n [get_collection_size $clut_commit_sync0]
set tile_toggle_sync0_n [get_collection_size $tile_toggle_sync0]
if { $clut_commit_sync0_n != 1 } { error "SDC FATAL: CLUT commit synchronizer stage-0 count $clut_commit_sync0_n != 1" }
if { $tile_toggle_sync0_n != 1 } { error "SDC FATAL: tile toggle synchronizer stage-0 count $tile_toggle_sync0_n != 1" }
set_false_path -to $clut_commit_sync0
set_false_path -to $tile_toggle_sync0
post_message -type info "SDC: explicit toggle CDC cuts applied to CLUT=$clut_commit_sync0_n tile=$tile_toggle_sync0_n first stages"

# ----------------------------------------------------------------------------
# gs_async_fifo gray-pointer CDC (Ch324). The dual-clock FIFOs (gs_z_flush_writer
# color/Z spill, gs_lpddr_axi_master FB writer, scanout) cross design_clk (u_pll,
# 40 ns / 25 MHz) <-> emif_clk (u_emif, 3.225 ns / 310 MHz) — a 12x ratio. With NO
# constraint the tool timed the gray-pointer crossing against the FAST 3.225 ns
# clock (an impossible single-cycle setup), so place&route corrupted the Z FIFO's
# wgray_s sync (placement-unlucky) -> rempty read wrong-low -> PHANTOM pops
# (board: z_pop/beats 1454 > z_push 987, ovf, ~2 artifact pixels). Color got lucky.
# A gray pointer changes ONE bit per source-clock; correctness needs the gray-bus
# arrival SKEW bounded below the period. Quartus Prime PRO has no `set_max_delay
# -datapath_only` (that is a Standard-only option) — the Pro constraint for CDC bus
# skew is `set_max_skew`, which is a SEPARATE analysis from setup (so it is NOT voided
# by clock groups / false paths the way max_delay would be). Bounding the source->first-
# sync skew well under the fast emif period (3.225 ns) forces the tool to place the gray
# bits compactly for EVERY gs_async_fifo instance, so at most one bit is ever in flight ->
# the 2-FF synchronizers handle metastability normally. (1.0 ns << both clock periods.)
# Ch357 (Codex) — PER-INSTANCE gray-skew. Keep the global 1 ns for every gs_async_fifo EXCEPT the two Z-path FIFOs
# (u_zc_emit request FIFO + its color-writer FIFO): those route into the fast 310 MHz EMIF domain and route+placement
# put them just over 1 ns (worst -0.346). Give THEM a justified 2.5 ns bound — still < the 3.225 ns fast-source period, so
# at most one gray bit is ever in flight and the 2-FF synchronizers handle metastability normally. Overlapping
# set_max_skew take the TIGHTEST, so the Z FIFOs are CARVED OUT of the global 1 ns set (remove_from_collection) — the
# broad rule must not keep overriding the 2.5 ns exception.
# Ch415 (Codex-reviewed): bumped 2.0 -> 2.5. Post-fog placement pushed the worst zc_emit gray skew to 2.010 ns, failing
# the round-number 2.0 bound by ~10 ps. The bound is conservative — the real correctness ceiling is the 3.225 ns fast-EMIF
# period (skew < period => at most one gray bit in flight, gray-code single-bit-change guarantee holds). 2.5 ns clears the
# 2.010 ns actual with margin AND stays well under 3.225 ns, so the CDC guarantee is intact. This is a documented relax of
# a conservative round number, not a waiver of a real requirement.
set z_wgray     [get_keepers -nowarn {*u_zc_emit|*wgray[*]}]
set z_rgray     [get_keepers -nowarn {*u_zc_emit|*rgray[*]}]
set other_wgray [remove_from_collection [get_keepers -nowarn {*wgray[*]}] $z_wgray]
set other_rgray [remove_from_collection [get_keepers -nowarn {*rgray[*]}] $z_rgray]
set_max_skew -from $other_wgray -to {*wgray_s1[*]} 1.0
set_max_skew -from $other_rgray -to {*rgray_s1[*]} 1.0
set_max_skew -from {*u_zc_emit|*wgray[*]} -to {*u_zc_emit|*wgray_s1[*]} 2.5
set_max_skew -from {*u_zc_emit|*rgray[*]} -to {*u_zc_emit|*rgray_s1[*]} 2.5
set z_wgray_n [get_collection_size $z_wgray]
set z_rgray_n [get_collection_size $z_rgray]
post_message -type info "Ch357 SDC: gray-skew 1ns global (Z FIFOs carved out), 2ns for $z_wgray_n u_zc_emit wgray + $z_rgray_n rgray bits"

# ----------------------------------------------------------------------------
# Ch352 — design fabric <-> EMIF user clock are ASYNCHRONOUS (Codex). Every
# fabric<->emif transfer now goes through a real synchronizer: gray-pointer FIFOs
# (max_skew above), the HPS-bridge status snapshots / toggle syncs, the
# gs_lpddr_axi_master {arm,canary,fb_base} commit snapshot + admission-stable AXI,
# and the video_src/scanout_lb commit-capture. So the tool must STOP timing these
# crossings against the fast 310 MHz emif clock as if single-cycle (the source of
# the −5.7/−7.5 ns violations). set_max_skew is a SEPARATE analysis and SURVIVES
# clock groups, so the gray pointers stay bounded.
#
# SCOPE (Codex): ONLY the bridge/design fabric clocks vs the EMIF USER clock. Do
# NOT wildcard the EMIF PHY/internal clocks — the EMIF IP constrains those itself;
# grouping them would hide real intra-EMIF paths.
#
# CLOCK NAME (Codex, post-fit STA): the fabric base clock is u_pll|iopll_0_refclk,
# NOT the CLOCK2_50 PORT. The PLL IP owns that port, so the TimeQuest clock at the
# bridge/refclk node is named u_pll|iopll_0_refclk; get_clocks {CLOCK2_50} returned
# an empty/1-clock collection, the group never activated, and placement burned ~76
# min on the impossible fabric<->emif paths. The collection-size checks are now a
# FATAL Tcl `error` (not a warning): a renamed/mistyped clock HALTS SDC immediately.
# Ch357 — RESTORED (signoff-clean for the persistent-Z fit). The Ch352 diagnostic/bisect that commented these
# out is over: STA is being closed properly, so the fabric<->EMIF crossings are cut with a TARGETED bidirectional
# set_false_path (NOT the asynchronous clock-group, which declared a generated clock async to its own generator and
# triggered the Place grind). Every fabric<->emif transfer is RTL-synchronized (gray-pointer FIFOs [max_skew below],
# bridge status snapshots/toggle syncs, the gs_lpddr_axi_master commit snapshot + admission-stable AXI, the zc_emit
# request FIFO), so cutting the raw setup/hold analysis on these two clock relationships is correct. set_max_skew is
# a SEPARATE analysis that survives false_path, so the gray pointers stay bounded.
#
# FATAL collection-size checks (Codex): a renamed/mistyped clock would silently orphan the exception and let Place
# churn the impossible fabric<->emif paths, so HALT immediately if the collection sizes are wrong. Clock names pinned
# from the post-fit STA summary: fabric = {u_pll|iopll_0_refclk, u_pll|iopll_0_outclk0} (2), EMIF user =
# {u_emif_lpddr4b|iopll|iopll_0_outclk0} (1). Do NOT wildcard the EMIF PHY/internal clocks — the EMIF IP owns those.
set ch352_fabric_clks [get_clocks -nowarn {u_pll|iopll_0_refclk u_pll|iopll_0_outclk0}]
set ch352_emif_user   [get_clocks -nowarn {u_emif_lpddr4b|iopll|iopll_0_outclk0}]
set ch352_fabric_n [get_collection_size $ch352_fabric_clks]
set ch352_emif_n   [get_collection_size $ch352_emif_user]
if { $ch352_fabric_n != 2 } { error "Ch352 SDC FATAL: fabric clock collection size $ch352_fabric_n != 2 (clock renamed? fabric<->emif cut would orphan)" }
if { $ch352_emif_n   != 1 } { error "Ch352 SDC FATAL: emif user clock collection size $ch352_emif_n != 1 (clock renamed? fabric<->emif cut would orphan)" }
set_false_path -from $ch352_fabric_clks -to $ch352_emif_user
set_false_path -from $ch352_emif_user   -to $ch352_fabric_clks
post_message -type info "Ch357 SDC: fabric<->EMIF bidirectional false_path applied (fabric=$ch352_fabric_n clks, emif_user=$ch352_emif_n clk)"
# DEFERRED (Codex, acknowledged NOT hidden): the Ch323/324 tile-spill/reload + writer DEBUG counters at
# 0x080-0x0FC (spill_*_errs, dbg_z/c_*_count, feeder_addr) are read by the bridge directly from the emif
# domain and are now inside the async group above. They are explicitly DEFERRED "stable-after-scene
# diagnostics": INERT in the SH3 profile (GS_TILE_SPILL undefined -> the spill writers are tied off, counters
# static 0), and in the GS_TILE_SPILL profile they are read only AFTER a render completes (writers quiescent,
# values stable). They are NOT bundled-snapshot CDC. If a future flow polls them WHILE the spill writers are
# active, they must be snapshotted (latch-on-quiesce, like the fill counters) before being trusted.

# ----------------------------------------------------------------------------
# IO false paths — buttons, switches, LEDs are async to the design
# clock (debounce/sample is handled in RTL via the rst_sync register).
# ----------------------------------------------------------------------------
set_false_path -from [get_ports {KEY[*]}]
set_false_path -from [get_ports {SW[*]}]
set_false_path -to   [get_ports {LED[*]}]

# ----------------------------------------------------------------------------
# Abstract VIDEO_* ports — kept VIRTUAL_PIN ON in the QSF for
# sim/TB compat (the Ch149 board TB references them via hierarchical
# probe). They aren't real package pins, so a false_path is the
# right constraint.
# ----------------------------------------------------------------------------
set_false_path -to [get_ports {VIDEO_R[*]}]
set_false_path -to [get_ports {VIDEO_G[*]}]
set_false_path -to [get_ports {VIDEO_B[*]}]
set_false_path -to [get_ports {VIDEO_HSYNC}]
set_false_path -to [get_ports {VIDEO_VSYNC}]
set_false_path -to [get_ports {VIDEO_DE}]

# ----------------------------------------------------------------------------
# HDMI ADV7513 output ports (Ch164) — real package pins. Ch164 is
# the "get pixels off-chip" first move; proper `set_output_delay`
# constraints with respect to a generated HDMI_TX_CLK domain land
# alongside the Ch165+ ADV7513 I²C wake-up FSM (the chip's actual
# setup/hold window comes out of the wake-up pass). Until then,
# false_path keeps STA quiet on the HDMI pins.
# ----------------------------------------------------------------------------
set_false_path -to [get_ports {HDMI_TX_CLK}]
set_false_path -to [get_ports {HDMI_TX_D[*]}]
set_false_path -to [get_ports {HDMI_TX_HS}]
set_false_path -to [get_ports {HDMI_TX_VS}]
set_false_path -to [get_ports {HDMI_TX_DE}]

# ----------------------------------------------------------------------------
# ADV7513 control / wake-up pins (Ch165) — clocked on the slow I²C
# domain (~20 kHz, derived inside `I2C_HDMI_Config` from CLOCK2_50)
# and inherently async to design_clk. The interrupt input
# `HDMI_TX_INT` is sampled by the wake-up FSM directly. Mark them
# false-path so STA doesn't try to time the I²C bus or the audio
# MCLK output against the design clock; their timing is dominated
# by the I²C SCL period (50 µs) and well below any meaningful
# constraint on the fabric.
# ----------------------------------------------------------------------------
set_false_path -to   [get_ports {HDMI_I2C_SCL}]
set_false_path -to   [get_ports {HDMI_I2C_SDA}]
set_false_path -from [get_ports {HDMI_I2C_SDA}]
set_false_path -from [get_ports {HDMI_TX_INT}]
set_false_path -to   [get_ports {HDMI_MCLK}]

# ----------------------------------------------------------------------------
# Ch357 — DS2 (PSX-style SPI) controller pins on the GPIO_0 header, driven by
# ds2_controller at the slow SPI bit-clock (async to design_clk; timing dominated
# by the SPI period, not the fabric clock). Same treatment as the HDMI I2C control
# pins — false-path so STA reports NO unconstrained I2C/DS2 ports (Codex signoff).
# ----------------------------------------------------------------------------
set_false_path -to   [get_ports {GPIO_0_DS2_CLK}]
set_false_path -to   [get_ports {GPIO_0_DS2_CMD}]
set_false_path -to   [get_ports {GPIO_0_DS2_ATTN}]
set_false_path -from [get_ports {GPIO_0_DS2_DATA}]

# ----------------------------------------------------------------------------
# Ch357 (Codex) — tile_ram_cdc bundled-bus CDC. The bridge writes {tile_wr_index, tile_wr_data} as a STABLE bus and the
# receiver samples it into shadow_mem ONLY after a 2-FF-synchronized toggle (tile_ram_cdc.sv:18 guarantees >=2 dclk of
# stability before the sample). So the raw HOLD check bridge->shadow_mem is FALSE (the bus never changes near the capture
# edge). Cut HOLD only (targeted, NOT a blanket clock waiver), and bound the datapath skew so the bus bits still arrive
# compactly inside the stability window. FATAL count checks so a rename can't silently orphan the exception.
# Cut the (spurious) HOLD only, then bound the ACTUAL requirement (Codex): the bus must arrive within the >=2 dclk
# stability window before the synchronized toggle samples it. set_max_skew alone bounds only bit-to-bit spread, NOT
# arrival. NOTE: `set_max_delay -datapath_only` is a Quartus Prime STANDARD option and DOES NOT EXIST in Pro 25.3.1
# (Error 332000 "Unknown option -datapath_only" aborts the fitter). The Pro-correct tool to bound CDC data-net arrival
# is `set_net_delay -max`, which bounds interconnect (routing) delay from source to destination. So the recipe is:
# HOLD false-path + set_max_skew (bit spread) + set_net_delay -max (arrival). Source = {tile_wr_index[9:0],
# tile_wr_data[31:0]} = 42 simple bridge regs (stable -> exact-count check); dest shadow_mem is M20K (nonzero check).
set tile_cdc_src [get_keepers -nowarn {*u_hps_bridge|tile_wr_data[*] *u_hps_bridge|tile_wr_index[*]}]
set tile_cdc_dst [get_keepers -nowarn {*u_tile_cdc|shadow_mem*}]
set tile_src_n [get_collection_size $tile_cdc_src]
set tile_dst_n [get_collection_size $tile_cdc_dst]
# This optional OSD tile path is correctly absent in profiles where the
# platform overlay is optimized away. Only a one-sided disappearance is an
# orphaned active CDC and therefore fatal; both sides absent needs no cut.
if { $tile_src_n == 0 && $tile_dst_n == 0 } {
    post_message -type info "Ch357 SDC: tile_ram_cdc inactive in this profile (0 src, 0 dst)"
} else {
    if { $tile_src_n != 42 } { error "Ch357 SDC FATAL: tile_ram_cdc source tile_wr_index+tile_wr_data count $tile_src_n != 42 (10+32; renamed/optimized? hold cut + net_delay would orphan)" }
    if { $tile_dst_n == 0 }  { error "Ch357 SDC FATAL: tile_ram_cdc dest u_tile_cdc|shadow_mem EMPTY (renamed? hold cut would orphan)" }
    set_false_path -hold      -from $tile_cdc_src -to $tile_cdc_dst
    # Ch443 (Codex): max_skew bound 2.0 -> 2.5. The Ch442 fit placed this quasi-static OSD
    # tile bundle at 2.050 ns actual skew, failing the round-number 2.0 by 50 ps. The CDC
    # contract guarantees >= 2 design-clock periods of bus stability before the synchronized
    # toggle samples (same recipe as the tile hold-false + net_delay below), so 2.5 ns stays
    # FAR inside the real functional window and avoids engineering to a 50 ps margin. The
    # set_net_delay ARRIVAL bound is UNCHANGED at 2.0 ns (it bounds the actual requirement:
    # each bit must arrive within the stability window; skew only bounds bit-to-bit spread).
    set_max_skew              -from $tile_cdc_src -to $tile_cdc_dst 2.5
    set_net_delay        -max -from $tile_cdc_src -to $tile_cdc_dst 2.0
    post_message -type info "Ch357/443 SDC: tile_ram_cdc hold-false-path + 2.5ns max_skew + 2ns net_delay ($tile_src_n src -> $tile_dst_n shadow_mem dst)"
}

# Ch357 (Codex) — osd_cfg QUASI-STATIC config CDC. osd_cfg{0,1}_q (bridge, refclk/fabric domain) cross into
# osd_cfg{0,1}_sync[0] (design_clk, first synchronizer stage; 2-FF, (* dont_merge, preserve *)). The config is quasi-
# static and resolved by the 2-FF sync -> CUT the async crossing INTO stage 0 (setup+hold). Do NOT touch sync[0]->sync[1]
# (the timed MTBF leg). Counts are NONZERO-checked, NOT exact: unused OSD_CFG bits are pruned, so the surviving sync FF
# count is < 64 (dst observed = 35). A nonzero check confirms the pattern binds without ever halting the fit on a count.
set osd_cfg_src [get_keepers -nowarn {*u_hps_bridge|osd_cfg0_q[*] *u_hps_bridge|osd_cfg1_q[*]}]
set osd_cfg_dst [get_keepers -nowarn {*osd_cfg0_sync[0][*] *osd_cfg1_sync[0][*]}]
set osd_src_n [get_collection_size $osd_cfg_src]
set osd_dst_n [get_collection_size $osd_cfg_dst]
if { $osd_src_n == 0 } { post_message -type warning "Ch357 SDC WARN: osd_cfg src pattern matched 0 keepers (renamed? osd_cfg false-path NOT applied)" }
if { $osd_dst_n == 0 } { post_message -type warning "Ch357 SDC WARN: osd_cfg dst pattern matched 0 keepers (renamed? osd_cfg false-path NOT applied)" }
set_false_path -from $osd_cfg_src -to $osd_cfg_dst

# ============================================================================
# Ch442 (Codex) — scanout A+B diagnostic CDC. gs_lpddr_scanout_lb (u_lpddr_scan_lb)
# drives the diagnostic into ps2_hps_bridge (u_hps_bridge) across async domains:
#   (a) THREE single-bit synchronizer chains — underflow (video_clk), read-error
#       (emif_clk), valid (video_clk) — 2/3-FF (* FORCED, dont_merge, preserve *).
#       Cut the async INTO stage 0 only (setup+hold); the sync[0]->sync[1..] MTBF
#       leg stays timed. Nonzero-check WARN (the whole path is absent in non-LPDDR
#       profiles), not fatal.
#   (b) the 31-src STABLE first-failure bundle {scan_y[10], nf_v[10], pmax[4],
#       vphase[4], base, lookahead, line_valid} (Ch443f: nf_s0[10] -> pmax[4];
#       diag_first[29:20] now carries the qualified-miss persistence max, upper 6
#       bits const 0 so the bridge dst reg stays 10-bit / 37-keeper). It is written
#       once per enabled session and held quiescent until the bridge captures it on
#       the synced-valid edge -> the raw HOLD check is false; cut HOLD only + bound
#       bit spread (set_max_skew 2ns) + bound net arrival (set_net_delay -max 2ns),
#       same recipe as the tile_ram_cdc bundle above. FATAL count checks so a rename
#       can't silently orphan the exception (src==31 exact; dst nonzero).
foreach s {scan_diag_uf_sync scan_diag_rderr_sync scan_diag_valid_sync} {
    set s0 [get_keepers -nowarn "*u_hps_bridge|$s\[0\]"]
    if { [get_collection_size $s0] == 0 } {
        post_message -type warning "Ch442 SDC WARN: $s\[0\] matched 0 keepers (renamed / no LPDDR scanout in profile) — stage-0 cut NOT applied"
    } else {
        set_false_path -to $s0
    }
}
# Ch443f: live per-frame pmax magnitude (4-bit bus) is a plain advisory 2-FF sync
# (video_clk uf_pmax_q -> bridge scan_diag_pmax_s0[*]); cut the async INTO stage 0
# only, leaving s0->s1 timed. Bus bits may be momentarily incoherent — harmless,
# it feeds 0x120[15:12] read across many frames, not the coherent capture bundle.
set pmax_s0 [get_keepers -nowarn {*u_hps_bridge|scan_diag_pmax_s0[*]}]
if { [get_collection_size $pmax_s0] == 0 } {
    post_message -type warning "Ch443f SDC WARN: scan_diag_pmax_s0 matched 0 keepers (renamed / no LPDDR scanout) — stage-0 cut NOT applied"
} else {
    set_false_path -to $pmax_s0
}
# Ch443f: coherent readiness GRAY code (axi next_fetch -> next_fetch_gray comb ->
# video nf_gray_s0[*]). Async into stage 0 -> cut setup+hold into nf_gray_s0; the
# s0->s1 MTBF leg stays timed. Gray changes ONE bit per increment, but bound the
# source->stage0 bit spread (max_skew + net_delay 2ns) so the lone multi-bit event
# (frame-restart reset to V_SOURCE_START, which fs_edge_v overrides on the video
# side anyway) cannot spread across more than one video sample. FATAL count guard.
set nfg_dst [get_keepers -nowarn {*u_lpddr_scan_lb|nf_gray_s0[*]}]
set nfg_src [get_keepers -nowarn {*u_lpddr_scan_lb|next_fetch[*]}]
if { [get_collection_size $nfg_dst] == 0 } {
    post_message -type info "Ch443f SDC: next_fetch Gray readiness CDC inactive in this profile (0 stage-0 dst)"
} else {
    if { [get_collection_size $nfg_src] == 0 } { error "Ch443f SDC FATAL: nf_gray_s0 present but u_lpddr_scan_lb|next_fetch[*] matched 0 keepers (renamed? Gray CDC source orphaned)" }
    set_false_path            -to   $nfg_dst
    set_max_skew   -from $nfg_src -to $nfg_dst 2.0
    set_net_delay  -max -from $nfg_src -to $nfg_dst 2.0
    post_message -type info "Ch443f SDC: next_fetch Gray readiness CDC async-in cut + 2ns max_skew/net_delay ([get_collection_size $nfg_src] src -> [get_collection_size $nfg_dst] stage-0 dst)"
}
set scan_diag_src [get_keepers -nowarn {*u_lpddr_scan_lb|diag_scan_y_q[*] *u_lpddr_scan_lb|diag_nf_v_q[*] *u_lpddr_scan_lb|diag_pmax_q[*] *u_lpddr_scan_lb|diag_vphase_q[*] *u_lpddr_scan_lb|diag_base_q *u_lpddr_scan_lb|diag_look_q *u_lpddr_scan_lb|diag_lv_q}]
set scan_diag_dst [get_keepers -nowarn {*u_hps_bridge|scan_diag_scan_y_q[*] *u_hps_bridge|scan_diag_nf_v_q[*] *u_hps_bridge|scan_diag_nf_s0_q[*] *u_hps_bridge|scan_diag_vphase_q[*] *u_hps_bridge|scan_diag_base_q *u_hps_bridge|scan_diag_look_q *u_hps_bridge|scan_diag_lv_q}]
set scan_diag_src_n [get_collection_size $scan_diag_src]
set scan_diag_dst_n [get_collection_size $scan_diag_dst]
if { $scan_diag_src_n == 0 } {
    post_message -type info "Ch442 SDC: scanout diagnostic CDC inactive in this profile (0 src -> no LPDDR scanout)"
} else {
    if { $scan_diag_src_n != 31 } { error "Ch442/443f SDC FATAL: scanout diag bundle source count $scan_diag_src_n != 31 (10+10+4+4+1+1+1 = scan_y+nf_v+pmax+vphase+base+look+lv; renamed/optimized? hold cut + net_delay would orphan)" }
    if { $scan_diag_dst_n == 0 }  { error "Ch442 SDC FATAL: scanout diag bundle dest u_hps_bridge|scan_diag_*_q EMPTY (renamed? hold cut would orphan)" }
    set_false_path -hold      -from $scan_diag_src -to $scan_diag_dst
    set_max_skew              -from $scan_diag_src -to $scan_diag_dst 2.0
    set_net_delay        -max -from $scan_diag_src -to $scan_diag_dst 2.0
    post_message -type info "Ch442 SDC: scanout diag bundle hold-false-path + 2ns max_skew + 2ns net_delay ($scan_diag_src_n src -> $scan_diag_dst_n dst)"
}
post_message -type info "Ch357 SDC: osd_cfg refclk->design_clk synchronizer false-path ($osd_src_n src -> $osd_dst_n stage0 dst)"

# Ch357 (Codex) — OSD MENU-STATE synchronizers (same quasi-static class as osd_cfg): cursor_row + osd_active cross
# refclk/menu -> design_clk via 2-FF (* dont_merge, preserve *) syncs. They change only on user menu input, never at the
# raster/EMIF rate. Cut the async input INTO the FIRST stage (sync[0]) only; sync[0]->sync[1] stays timed. Using -to (no
# -from) is the canonical synchronizer cut and needs no launch-reg name. Covers cursor_row (the -0.223 hold) + osd_active
# together so a future placement roll can't surface another OSD sync as the next hold. Nonzero-warn (never a fatal).
set osd_msync_dst [get_keepers -nowarn {*cursor_row_sync[0][*] *osd_active_sync[0]}]
set osd_msync_n [get_collection_size $osd_msync_dst]
if { $osd_msync_n == 0 } { post_message -type warning "Ch357 SDC WARN: OSD menu-state sync[0] matched 0 keepers (renamed? cursor_row/osd_active false-path NOT applied)" }
set_false_path -to $osd_msync_dst
post_message -type info "Ch357 SDC: OSD menu-state (cursor_row/osd_active) synchronizer stage-0 false-path ($osd_msync_n sync0 dst)"

# Ch397 — LPDDR control-commit toggle enters design_clk through a three-flop
# synchronizer.  Cut only the asynchronous/quasi-static source -> stage-0 leg;
# commit_d_sync[0] -> [1] -> [2] remains timed for metastability settling.
set lpddr_commit_sync0 [get_keepers -nowarn {*commit_d_sync[0]}]
set lpddr_commit_sync0_n [get_collection_size $lpddr_commit_sync0]
if { $lpddr_commit_sync0_n == 0 } { post_message -type warning "Ch397 SDC WARN: commit_d_sync[0] matched 0 keepers (LPDDR commit sync cut NOT applied)" }
set_false_path -to $lpddr_commit_sync0
post_message -type info "Ch397 SDC: LPDDR commit synchronizer stage-0 false-path ($lpddr_commit_sync0_n sync0 dst)"

# Ch439 — LPDDR video-source is a one-bit bundled control crossing from the
# bridge/refclk domain into design_clk. The source is written before
# lpddr_ctrl_commit toggles; vsrc_cap_d samples it only after that toggle has
# traversed commit_d_sync. Consequently the raw zero-cycle HOLD relationship
# between the unrelated clocks is not functional. Cut HOLD only and retain a
# 2 ns interconnect bound so the data remains well inside the commit-toggle
# stability window. Setup remains analyzed.
set lpddr_vsrc_src [get_keepers -nowarn {*u_hps_bridge|lpddr_video_src_q}]
set lpddr_vsrc_dst [get_keepers -nowarn {*vsrc_cap_d}]
set lpddr_vsrc_src_n [get_collection_size $lpddr_vsrc_src]
set lpddr_vsrc_dst_n [get_collection_size $lpddr_vsrc_dst]
if { $lpddr_vsrc_src_n == 0 } { post_message -type warning "Ch439 SDC WARN: LPDDR video-src source matched 0 keepers (bundled-CDC hold cut NOT applied)" }
if { $lpddr_vsrc_dst_n == 0 } { post_message -type warning "Ch439 SDC WARN: vsrc_cap_d matched 0 keepers (bundled-CDC hold cut NOT applied)" }
set_false_path -hold      -from $lpddr_vsrc_src -to $lpddr_vsrc_dst
set_net_delay        -max -from $lpddr_vsrc_src -to $lpddr_vsrc_dst 2.0
post_message -type info "Ch439 SDC: LPDDR video-src bundled-CDC hold-false + 2ns net_delay ($lpddr_vsrc_src_n src -> $lpddr_vsrc_dst_n dst)"

# Ch357 (Codex) — feeder staging BUNDLED-BUS CDC (same shape as tile_ram_cdc). {feeder_waddr_q[11:0], feeder_wdata_q[63:0]}
# = 76 stable bits cross from the bridge (refclk) into the feeder staging RAM (design_clk), sampled ONLY after the
# feeder_stg_we toggle 2-FF-syncs (fdr_we_sync). Cut the spurious HOLD, bound bit spread (max_skew) + arrival (net_delay).
# All counts NONZERO-checked, never exact (no count can halt the fit). Expected src ~76 (waddr 12 + wdata 64); the actual
# is recorded in the post_message. dest = the staging RAM (altsyncram) write registers, nonzero. Also false-path the toggle.
set fdr_cdc_src [get_keepers -nowarn {*u_hps_bridge|feeder_wdata_q[*] *u_hps_bridge|feeder_waddr_q[*]}]
set fdr_cdc_dst [get_keepers -nowarn {*g_feeder*feeder_stg*}]
set fdr_src_n [get_collection_size $fdr_cdc_src]
set fdr_dst_n [get_collection_size $fdr_cdc_dst]
if { $fdr_src_n == 0 } { post_message -type warning "Ch357 SDC WARN: feeder src pattern matched 0 keepers (renamed? feeder-CDC NOT applied)" }
if { $fdr_dst_n == 0 } { post_message -type warning "Ch357 SDC WARN: feeder staging RAM dest EMPTY (pattern *g_feeder*feeder_stg* matched 0 keepers; feeder-CDC hold NOT constrained this run)" }
set_false_path -hold      -from $fdr_cdc_src -to $fdr_cdc_dst
set_max_skew              -from $fdr_cdc_src -to $fdr_cdc_dst 2.0
set_net_delay        -max -from $fdr_cdc_src -to $fdr_cdc_dst 2.0
set fdr_tgl_dst [get_keepers -nowarn {*fdr_we_sync[0] *fdr_go_sync[0]}]
if { [get_collection_size $fdr_tgl_dst] > 0 } { set_false_path -to $fdr_tgl_dst }
post_message -type info "Ch357 SDC: feeder staging bundled-CDC hold-false + 2ns max_skew + 2ns net_delay ($fdr_src_n src -> $fdr_dst_n RAM dst)"

# Ch357 (Codex) — EMIF calibration-ready (lock) -> texture-cache RAM. emif_reset_n is the EMIF's calibrated/ready
# indicator (RTL comment: "low during cal, high when done"): QUASI-STATIC — it asserts once when calibration completes,
# BEFORE any texture access, and never toggles at runtime. Its lock synchronizer (lock_sync_inst|dreg) feeds the tex_mem
# init/reset, leaving a residual ~-0.019ns setup that is functionally irrelevant (the texcache is inactive until cal is
# stable). Cut the NARROWEST possible crossing: from ONLY the EMIF lock-sync node to ONLY the tex_mem endpoints. This is
# NOT a clock-group cut and NOT a multicycle. Nonzero-warn (never fatal) so a rename can't halt the fit.
set emif_lock_src [get_keepers -nowarn {*lock_sync_inst|dreg[*]}]
set tex_mem_dst   [get_keepers -nowarn {*u_texcache|tex_mem*}]
set eln [get_collection_size $emif_lock_src]
set tdn [get_collection_size $tex_mem_dst]
if { $eln == 0 } { post_message -type warning "Ch357 SDC WARN: EMIF lock-sync src matched 0 keepers (renamed? lock->tex_mem false-path NOT applied)" }
if { $tdn == 0 } { post_message -type warning "Ch357 SDC WARN: tex_mem dst matched 0 keepers (renamed? lock->tex_mem false-path NOT applied)" }
set_false_path -from $emif_lock_src -to $tex_mem_dst
post_message -type info "Ch357 SDC: EMIF lock-sync -> tex_mem quasi-static false-path ($eln src -> $tdn dst)"

# Ch439g — the bilinear tap bank is separated from every externally visible
# texture result by tex_color_hold (captured only in BS_DONE, exposed only in
# the following BS_OUT state).  RTL has no combinational tap -> tex_rd_addr
# path.  Nevertheless Agilex DSP packing creates a conservative STA arc from
# tap[*], through mutually exclusive bilinear/modulation/interpolation DSP
# modes, to tex_mem's registered address port.  That address port is enabled
# only by tex_rd_en in BS_ISSUE/nearest-issue cycles; the tap blend is consumed
# only in BS_DONE, so the reported arc can never be captured functionally.
#
# Cut only this impossible launch family into the texture cache.  Real address
# timing from walker/UV/perspective registers to tex_mem remains fully timed.
# Fail closed on the exact architectural 4x32 tap register population, and on
# a nonempty cache destination, so hierarchy drift cannot silently broaden or
# orphan the exception.
set ch439_bili_taps [get_registers -nowarn {*u_demo|u_gs|u_tex|g_bilinear.tap[*]}]
set ch439_bili_taps_n [get_collection_size $ch439_bili_taps]
if { $ch439_bili_taps_n != 128 } { error "Ch439g SDC FATAL: bilinear tap register count $ch439_bili_taps_n != 128 (4x32; false-path would orphan or broaden)" }
if { $tdn == 0 } { error "Ch439g SDC FATAL: tex_mem destination collection empty" }
set_false_path -from $ch439_bili_taps -to $tex_mem_dst
post_message -type info "Ch439g SDC: impossible bilinear-tap -> tex_mem packed-DSP arc cut ($ch439_bili_taps_n src -> $tdn dst); all non-tap texture-address launches remain timed"

# Ch443 (Codex) — texture-cache EMIF DRAIN write-address multicycle. The fill FSM now holds
# drain_idx_q/drain_word_q stable across F_DRAIN -> F_SETTLE -> F_WRITE and asserts the RAM
# write-enable ONLY at the later F_WRITE edge, so the drain address is functionally required
# 2 EMIF cycles after launch. Give ONLY drain_idx_q -> tex_mem a 2-cycle setup / 1-cycle hold
# multicycle (a 6.45 ns window for the -0.370 ns drain write-address fanout that closing the
# four-bank split had opened). SCOPED -from the drain registers, so it does NOT touch the
# sampler/read-address path (ras_v0_x / walker / UV / perspective -> tex_mem portbaddr) — that
# launches from DIFFERENT registers and stays fully timed (Ch439g above). Reuses the tex_mem
# destination collection $tex_mem_dst / $tdn defined for the Ch357/Ch439g exceptions.
# Fail-closed: if tex_mem exists but drain_idx_q[*] is gone (renamed), HALT — never silently
# omit the exception (which would let the drain path refail setup unconstrained).
set tex_drain_src   [get_keepers -nowarn {*u_texcache|drain_idx_q[*]}]
set tex_drain_src_n [get_collection_size $tex_drain_src]
if { $tdn == 0 } {
    post_message -type info "Ch443 SDC: texture drain multicycle inactive (no tex_mem in this profile)"
} else {
    if { $tex_drain_src_n == 0 } { error "Ch443 SDC FATAL: tex_mem present but u_texcache|drain_idx_q[*] matched 0 keepers (renamed? drain multicycle orphaned -> write-address would silently refail setup)" }
    set_multicycle_path -setup -end 2 -from $tex_drain_src -to $tex_mem_dst
    set_multicycle_path -hold  -end 1 -from $tex_drain_src -to $tex_mem_dst
    post_message -type info "Ch443 SDC: texture drain_idx_q -> tex_mem 2-cycle setup / 1-cycle hold multicycle ($tex_drain_src_n src -> $tdn dst)"
}

# Ch358 (Codex) — the SAME quasi-static EMIF calibration-ready signal also reaches the scanout LINE-BUFFER RAMs
# (26.1 fit at 640x480: lock_sync_inst|dreg[1] -> u_lpddr_scan_lb|lb0/lb1, 10 endpoints at -0.010ns). These are
# startup/reset-derived line-buffer control paths, NOT runtime scanout data (the line buffers are inactive until
# calibration is stable, exactly like tex_mem). Extend the SAME narrow waiver to the lb RAM endpoints ONLY —
# same source collection, endpoints-only dst, nonzero-warn guard; NOT a clock cut, NOT a whole-module waiver.
set scan_lb_dst [get_keepers -nowarn {*u_lpddr_scan_lb|lb*}]
set sln [get_collection_size $scan_lb_dst]
if { $sln == 0 } { post_message -type warning "Ch358 SDC WARN: scanout lb dst matched 0 keepers (renamed? lock->scan_lb false-path NOT applied)" }
set_false_path -from $emif_lock_src -to $scan_lb_dst
post_message -type info "Ch358 SDC: EMIF lock-sync -> u_lpddr_scan_lb|lb* quasi-static false-path ($eln src -> $sln dst)"

# ----------------------------------------------------------------------------
# Ch357 (Codex) — triangle-setup gradient divide MULTICYCLE, now with a REGISTERED numerator so both endpoints are
# tiny. The RTL (gs_stub) captures grad_num_q[grad_step] into grad_num_sel_q, holds grad_num_sel_q + grad_det_q stable
# for the GRAD_DIV_CYCLES=5 settle window, captures the quotient into grad_result_q, THEN advances grad_step. So the
# combinational divide is grad_num_sel_q / grad_det_q -> grad_result_q: a clean register->register multicycle.
#
# WHY THIS REPLACES the Ch352 {grad_num_q,grad_det_q,grad_step} -from set: that set matched ~1157 regs (grad_num_q is
# 20x56 bits) x 32 dests = ~37k relaxed paths. Even after Ch352 shrank the DEST side (fifo_d* 5120 -> grad_result_q 32),
# the ~1157-source cone (the 20-way dynamic numerator mux) still made the timing-driven placer churn ~6 h in Place.
# Registering the mux output removes it from the divide's LAUNCH cone: -from is now {grad_num_sel_q, grad_det_q} ~88
# regs -> ~2.8k pairs (~13x smaller), and the divide is fed by 2 registers, not a 20-way mux. This is a structural fix
# (the mux is gone before the divider), not an SDC negotiation.
#
# FATAL checks (Codex): grad_num_sel_q + grad_det_q are (* preserve *)'d / real regs — HALT if the -from set is EMPTY
# (source orphaned) or the -to grad_result_q count != 32 (dest orphaned), before A&S->Place. Error strings interpolate
# only $vars (no square brackets -> no Tcl command-substitution bug).
# ----------------------------------------------------------------------------
# CONDITIONAL (Codex): apply the gradient multicycle ONLY when the COMBINATIONAL divider exists. On the board profile
# (GRAD_SEQ_DIVIDER=1) gs_stub's generate strips the `/` entirely and instantiates gs_grad_divider (registered
# iterations) instead — there is no divide cone, so a gradient exception would be meaningless and must NOT be applied.
# Detect the build by the sequential divider's own registers (qbuild/iter live ONLY inside gs_grad_divider).
set seqdiv_regs [get_keepers -nowarn {*u_grad_div|qbuild* *u_grad_div|iter*}]
set seqdiv_n [get_collection_size $seqdiv_regs]
if { $seqdiv_n > 0 } {
    # SEQUENTIAL build (board): combinational `/` generate-stripped -> NO gradient exception.
    post_message -type info "Ch357 SDC: sequential gradient divider present ($seqdiv_n regs) -> combinational divide absent, NO gradient multicycle applied"
} else {
    # COMBINATIONAL build (board): registered-numerator divide grad_num_sel_q/grad_det_q -> grad_result_q. Routed data
    # delay was 108.132 ns at 56/32 bits. Ch417 widens the operands to 64/35 bits, so GRAD_DIV_CYCLES=5 gives a
    # 200 ns functional window at design_clk = 40 ns (25 MHz). The RTL holds the sources for all five cycles before
    # capturing grad_result_q; final STA therefore fail-closes the widened cone against the real 200 ns contract.
    #
    # STA-ONLY multicycle (Codex, 2026-07-05; widened Ch417 contract): setup=5/hold=4 is applied ONLY during final quartus_sta,
    # NOT during quartus_fit. During the fit, a gradient multicycle makes the timing-driven placer treat the ~108 ns
    # combinational divide as a "closeable" cone and GRIND (proven twice: 6 h and 13 min). Without it the fitter keeps
    # its known-CONVERGENT "hopeless single-cycle" behavior (drops the path, places in seconds). Final quartus_sta then
    # applies the multicycle and fail-closes against the real 200 ns window. Fatal source/dest
    # collection checks live in this STA-only branch too. grad_result_q replicates for fanout during P&R (32 -> 55), so
    # the dest check is ">= 32" (preserve blocks REMOVAL, not duplication), not "== 32".
    if { [string match "quartus_sta*" $::quartus(nameofexecutable)] } {
        set grad_mc_from [get_keepers -nowarn {*u_gs|grad_num_sel_q* *u_gs|grad_det_q*}]
        set grad_mc_to   [get_keepers -nowarn {*u_gs|grad_result_q*}]
        set grad_from_n [get_collection_size $grad_mc_from]
        set grad_to_n   [get_collection_size $grad_mc_to]
        # -to grad_result_q ONLY (no -from): the Agilex Hyper-Retimer pushes a register INTO the LPM divider
        # (div_*_rtl_0|...|my_abs_den|dff), so the worst combinational launch is that INTERNAL register, NOT grad_num_sel_q.
        # The named source set is diagnostics only; it may be optimized away while the retimed destination and its real
        # divide cone survive. Keep the actual destination constraint in that case. If BOTH source tracking and the
        # destination are absent, this profile has no combinational divider and no multicycle is needed.
        if { $grad_to_n == 0 } {
            post_message -type info "Ch357 SDC: gradient combinational divider inactive in this profile (0 grad_result_q destinations)"
        } else {
            if { $grad_to_n < 32 } { error "Ch357 SDC FATAL: grad_result_q destination count $grad_to_n < 32 (preserve lost? multicycle would orphan)" }
            set_multicycle_path -setup -end 5 -to $grad_mc_to
            set_multicycle_path -hold  -end 4 -to $grad_mc_to
            if { $grad_from_n == 0 } {
                post_message -type warning "Ch357 SDC: grad_num_sel_q/grad_det_q tracker optimized away; retained destination-only gradient multicycle on $grad_to_n grad_result_q registers"
            } else {
                post_message -type info "Ch417 SDC (STA-only): widened gradient multicycle setup=5/hold=4 -to grad_result_q ($grad_to_n regs, all launches incl. retimed divider internals) — 200ns functional window; source set grad_num_sel_q+grad_det_q = $grad_from_n regs"
            }
        }
    } else {
        post_message -type info "Ch417 SDC (fit stage): gradient multicycle NOT applied (STA-only) -> fitter keeps convergent single-cycle behavior; final quartus_sta fail-closes the widened divide at the 200ns 5-cycle window"
    }
}
