// Copyright © 2025 Valve Corporation
// SPDX-License-Identifier: MIT

use crate::api::{GetDebugFlags, DEBUG};
use crate::ir::*;
use crate::liveness::{BlockLiveness, LiveSet, Liveness, SimpleLiveness};
use crate::opt_instr_sched_common::{
    calc_statistics, estimate_variable_latency, side_effect_type, DepGraph,
    EdgeLabel, FutureReadyInstr, ReadyInstr, SideEffect,
};
use rustc_hash::FxHashMap;
use std::cmp::Reverse;
use std::cmp::{max, min};
use std::collections::BTreeSet;

// This is the maximum number of reserved gprs - (TODO: Model more cases where
// we actually need 2)
const SW_RESERVED_GPRS: i32 = 1;
const SW_RESERVED_GPRS_SPILL: i32 = 2;

/// Target number of free GPRs. This is used for the threshold to switch to
/// scheduling for register pressure
const TARGET_FREE: i32 = 4;

/// Typically using an extra register is free... until you hit a threshold where
/// one more register causes occupancy to plummet. This function figures out how
/// many GPRs you can use without costing occupancy, assuming you always need at
/// least `x` GPRs.
fn next_occupancy_cliff(sm: &ShaderModelInfo, x: u32) -> u32 {
    let total_regs: u32 = 65536;
    let threads = max_warps_per_sm(sm, x) * 32;

    // This function doesn't actually model the maximum number of registers
    // correctly - callers need to worry about that separately. We do,
    // however, want to avoid a divide by zero.
    let threads = max(threads, 1);

    prev_multiple_of(total_regs / threads, 8)
}

#[cfg(test)]
#[test]
fn test_next_occupancy_cliff() {
    for max_hw_warps in [32, 48, 64] {
        let sm = ShaderModelInfo::new(75, max_hw_warps);
        for x in 0..255 {
            let y = next_occupancy_cliff(&sm, x);
            assert!(y >= x);
            assert_eq!(max_warps_per_sm(&sm, x), max_warps_per_sm(&sm, y));
            assert!(max_warps_per_sm(&sm, y) > max_warps_per_sm(&sm, y + 1));
        }
    }
}

fn next_occupancy_cliff_with_reserved(
    sm: &ShaderModelInfo,
    gprs: i32,
    reserved: i32,
) -> i32 {
    i32::try_from(next_occupancy_cliff(
        sm,
        (gprs + reserved).try_into().unwrap(),
    ))
    .unwrap()
        - reserved
}

fn generate_dep_graph(sm: &ShaderModelInfo, instrs: &[Instr]) -> DepGraph {
    let mut g = DepGraph::new((0..instrs.len()).map(|_| Default::default()));

    let mut defs = FxHashMap::<SSAValue, (usize, usize)>::default();

    let mut last_memory_op = None;
    let mut last_barrier_op = None;

    for ip in 0..instrs.len() {
        let instr = &instrs[ip];

        if let Some(bar_ip) = last_barrier_op {
            g.add_edge(bar_ip, ip, EdgeLabel { latency: 0 });
        }

        match side_effect_type(&instr.op) {
            SideEffect::None => (),
            SideEffect::Barrier => {
                let first_ip = last_barrier_op.unwrap_or(0);
                for other_ip in first_ip..ip {
                    g.add_edge(other_ip, ip, EdgeLabel { latency: 0 });
                }
                last_barrier_op = Some(ip);
            }
            SideEffect::Memory => {
                if let Some(mem_ip) = last_memory_op {
                    g.add_edge(mem_ip, ip, EdgeLabel { latency: 0 });
                }
                last_memory_op = Some(ip);
            }
        }

        for (i, src) in instr.srcs().iter().enumerate() {
            for ssa in src.src_ref.iter_ssa() {
                if let Some(&(def_ip, def_idx)) = defs.get(ssa) {
                    let def_instr = &instrs[def_ip];
                    let latency =
                        if def_instr.op.is_virtual() || instr.op.is_virtual() {
                            0
                        } else {
                            max(
                                sm.raw_latency(
                                    &def_instr.op,
                                    def_idx,
                                    &instr.op,
                                    i,
                                ),
                                estimate_variable_latency(sm, &def_instr.op),
                            )
                        };

                    g.add_edge(def_ip, ip, EdgeLabel { latency });
                }
            }
        }

        if let PredRef::SSA(ssa) = &instr.pred.pred_ref {
            if let Some(&(def_ip, def_idx)) = defs.get(ssa) {
                let def_instr = &instrs[def_ip];

                let latency = if def_instr.op.is_virtual() {
                    0
                } else {
                    max(
                        sm.paw_latency(&def_instr.op, def_idx),
                        estimate_variable_latency(sm, &def_instr.op),
                    )
                };

                g.add_edge(def_ip, ip, EdgeLabel { latency });
            }
        }

        for (i, dst) in instr.dsts().iter().enumerate() {
            for &ssa in dst.iter_ssa() {
                defs.insert(ssa, (ip, i));
            }
        }
    }

    g
}

mod net_live {
    use crate::ir::*;
    use crate::liveness::LiveSet;
    use rustc_hash::FxHashMap;
    use std::ops::Index;

    /// The net change in live values, from the end of an instruction to a
    /// specific point during the instruction's execution
    pub(super) struct InstrCount {
        /// The net change in live values across the whole instruction
        pub net: PerRegFile<i8>,

        /// peak1 is at the end of the instruction, where any immediately-killed
        /// defs are live
        pub peak1: PerRegFile<i8>,

        /// peak2 is just before sources are read, and after vector defs are live
        pub peak2: PerRegFile<i8>,
    }

    /// For each instruction, keep track of a "net live" value, which is how
    /// much the size of the live values set will change if we chedule a given
    /// instruction next. This is tracked per-register-file.
    ///
    /// Assumes that we are iterating over instructions in reverse order
    pub(super) struct NetLive {
        counts: Vec<InstrCount>,
        ssa_to_instr: FxHashMap<SSAValue, Vec<usize>>,
    }

    impl NetLive {
        pub(super) fn new(instrs: &[Instr], live_out: &LiveSet) -> Self {
            let mut use_set = LiveSet::new();
            let mut ssa_to_instr = FxHashMap::default();

            let mut counts: Vec<InstrCount> = instrs
                .iter()
                .enumerate()
                .map(|(instr_idx, instr)| {
                    use_set.clear();
                    for ssa in instr.ssa_uses() {
                        if !live_out.contains(ssa) {
                            if use_set.insert(*ssa) {
                                ssa_to_instr
                                    .entry(*ssa)
                                    .or_insert_with(Vec::new)
                                    .push(instr_idx);
                            }
                        }
                    }

                    let net = PerRegFile::new_with(|f| {
                        use_set.count(f).try_into().unwrap()
                    });
                    InstrCount {
                        net: net,
                        peak1: Default::default(),
                        peak2: net,
                    }
                })
                .collect();

            for (instr_idx, instr) in instrs.iter().enumerate() {
                for dst in instr.dsts() {
                    let is_vector = dst.iter_ssa().len() > 1;
                    let count = &mut counts[instr_idx];

                    for &ssa in dst.iter_ssa() {
                        if ssa_to_instr.contains_key(&ssa)
                            || live_out.contains(&ssa)
                        {
                            count.net[ssa.file()] -= 1;
                        } else {
                            count.peak1[ssa.file()] += 1;
                            count.peak2[ssa.file()] += 1;
                        }

                        if !is_vector {
                            count.peak2[ssa.file()] -= 1;
                        }
                    }
                }
            }

            NetLive {
                counts,
                ssa_to_instr,
            }
        }

        pub(super) fn remove(&mut self, ssa: SSAValue) -> bool {
            match self.ssa_to_instr.remove(&ssa) {
                Some(instr_idxs) => {
                    assert!(!instr_idxs.is_empty());
                    let file = ssa.file();
                    for i in instr_idxs {
                        self.counts[i].net[file] -= 1;
                        self.counts[i].peak2[file] -= 1;
                    }
                    true
                }
                None => false,
            }
        }
    }

    impl Index<usize> for NetLive {
        type Output = InstrCount;

        fn index(&self, index: usize) -> &Self::Output {
            &self.counts[index]
        }
    }
}

use net_live::NetLive;

/// The third element of each tuple is a weight meant to approximate the cost of
/// spilling a value from the first register file to the second. Right now, the
/// values are meant to approximate the cost of a spill + fill, in cycles
const SPILL_FILES: [(RegFile, RegFile, i32); 5] = [
    (RegFile::Bar, RegFile::GPR, 6 + 6),
    (RegFile::Pred, RegFile::GPR, 12 + 6),
    (RegFile::UPred, RegFile::UGPR, 12 + 6),
    (RegFile::UGPR, RegFile::GPR, 15 + 6),
    (RegFile::GPR, RegFile::Mem, 32 + 32),
];

/// Models how many gprs will be used after spilling other register files
fn calc_used_gprs(mut p: PerRegFile<i32>, max_regs: PerRegFile<i32>) -> i32 {
    for (src, dest, _) in SPILL_FILES {
        if p[src] > max_regs[src] {
            p[dest] += p[src] - max_regs[src];
        }
    }

    p[RegFile::GPR]
}

fn calc_score_part(
    mut p: PerRegFile<i32>,
    max_regs: PerRegFile<i32>,
) -> (i32, i32) {
    // We separate "badness" and "goodness" because we don't want eg. two extra
    // free predicates to offset the weight of spilling a UGPR - the spill is
    // always more important than keeping extra registers free
    let mut badness: i32 = 0;
    let mut goodness: i32 = 0;

    for (src, dest, weight) in SPILL_FILES {
        if p[src] > max_regs[src] {
            let spill_count = p[src] - max_regs[src];
            p[dest] += spill_count;
            badness += spill_count * weight;
        } else {
            let free_count = max_regs[src] - p[src];
            goodness += free_count * weight;
        }
    }
    (badness, goodness)
}

type Score = (bool, Reverse<i32>, i32);
fn calc_score(
    net: PerRegFile<i32>,
    peak1: PerRegFile<i32>,
    peak2: PerRegFile<i32>,
    max_regs: PerRegFile<i32>,
    delay_cycles: u32,
    thresholds: ScheduleThresholds,
) -> Score {
    let peak_gprs = max(
        calc_used_gprs(peak1, max_regs),
        calc_used_gprs(peak2, max_regs),
    );
    let instruction_usable = peak_gprs <= thresholds.quit_threshold;
    if !instruction_usable {
        return (false, Reverse(0), 0);
    }

    let (mut badness, goodness) = calc_score_part(net, max_regs);
    badness += i32::try_from(delay_cycles).unwrap();

    (true, Reverse(badness), goodness)
}

#[derive(Copy, Clone)]
struct ScheduleThresholds {
    /// Start scheduling for pressure if we use this many gprs
    heuristic_threshold: i32,

    /// Give up if we use this many gprs
    quit_threshold: i32,
}

struct GenerateOrder<'a> {
    max_regs: PerRegFile<i32>,
    net_live: NetLive,
    live: LiveSet,
    instrs: &'a [Instr],
}

impl<'a> GenerateOrder<'a> {
    fn new(
        max_regs: PerRegFile<i32>,
        instrs: &'a [Instr],
        live_out: &LiveSet,
    ) -> Self {
        let net_live = NetLive::new(instrs, live_out);
        let live: LiveSet = live_out.clone();

        GenerateOrder {
            max_regs,
            net_live,
            live,
            instrs,
        }
    }

    fn new_used_regs(&self, net: PerRegFile<i8>) -> PerRegFile<i32> {
        PerRegFile::new_with(|file| {
            i32::try_from(self.live.count(file)).unwrap() + (net[file] as i32)
        })
    }

    fn current_used_gprs(&self) -> i32 {
        calc_used_gprs(
            PerRegFile::new_with(|f| self.live.count(f).try_into().unwrap()),
            self.max_regs,
        )
    }

    fn new_used_gprs_net(&self, instr_index: usize) -> i32 {
        calc_used_gprs(
            self.new_used_regs(self.net_live[instr_index].net),
            self.max_regs,
        )
    }

    fn new_used_gprs_peak1(&self, instr_index: usize) -> i32 {
        calc_used_gprs(
            self.new_used_regs(self.net_live[instr_index].peak1),
            self.max_regs,
        )
    }

    fn new_used_gprs_peak2(&self, instr_index: usize) -> i32 {
        calc_used_gprs(
            self.new_used_regs(self.net_live[instr_index].peak2),
            self.max_regs,
        )
    }

    fn new_score(
        &self,
        instr_index: usize,
        delay_cycles: u32,
        thresholds: ScheduleThresholds,
    ) -> Score {
        calc_score(
            self.new_used_regs(self.net_live[instr_index].net),
            self.new_used_regs(self.net_live[instr_index].peak1),
            self.new_used_regs(self.net_live[instr_index].peak2),
            self.max_regs,
            delay_cycles,
            thresholds,
        )
    }

    fn generate_order(
        mut self,
        g: &DepGraph,
        init_ready_list: &[usize],
        thresholds: ScheduleThresholds,
    ) -> Option<(Vec<usize>, PerRegFile<i32>)> {
        let mut ready_instrs: BTreeSet<ReadyInstr> = init_ready_list
            .iter()
            .map(|&i| ReadyInstr::new(g, i))
            .collect();
        let mut future_ready_instrs = BTreeSet::new();

        struct InstrInfo {
            num_uses: u32,
            ready_cycle: u32,
        }
        let mut instr_info: Vec<InstrInfo> = g
            .nodes
            .iter()
            .map(|node| InstrInfo {
                num_uses: node.label.num_uses,
                ready_cycle: node.label.ready_cycle,
            })
            .collect();

        let mut current_cycle = 0;
        let mut instr_order = Vec::with_capacity(g.nodes.len());
        loop {
            let used_gprs = self.current_used_gprs();

            // Move ready instructions to the ready list
            loop {
                match future_ready_instrs.last() {
                    None => break,
                    Some(FutureReadyInstr {
                        ready_cycle: std::cmp::Reverse(ready_cycle),
                        index,
                    }) => {
                        if current_cycle >= *ready_cycle {
                            ready_instrs.insert(ReadyInstr::new(g, *index));
                            future_ready_instrs.pop_last();
                        } else {
                            break;
                        }
                    }
                }
            }

            if ready_instrs.is_empty() {
                match future_ready_instrs.last() {
                    None => break, // Both lists are empty. We're done!
                    Some(&FutureReadyInstr {
                        ready_cycle: Reverse(ready_cycle),
                        ..
                    }) => {
                        // Fast-forward time to when the next instr is ready
                        assert!(ready_cycle > current_cycle);
                        current_cycle = ready_cycle;
                        continue;
                    }
                }
            }

            // Pick an instruction to schedule
            let next_idx = if used_gprs <= thresholds.heuristic_threshold {
                let ReadyInstr { index, .. } = ready_instrs.pop_last().unwrap();
                index
            } else {
                let (new_score, ready_instr) = ready_instrs
                    .iter()
                    .map(|ready_instr| {
                        (
                            self.new_score(ready_instr.index, 0, thresholds),
                            ready_instr.clone(),
                        )
                    })
                    .max()
                    .unwrap();

                let better_candidate = future_ready_instrs
                    .iter()
                    .filter_map(|future_ready_instr| {
                        let ready_cycle = future_ready_instr.ready_cycle.0;
                        let s = self.new_score(
                            future_ready_instr.index,
                            ready_cycle - current_cycle,
                            thresholds,
                        );
                        if s > new_score {
                            Some((s, future_ready_instr.clone()))
                        } else {
                            None
                        }
                    })
                    .max();

                if let Some((_, future_ready_instr)) = better_candidate {
                    future_ready_instrs.remove(&future_ready_instr);
                    let ready_cycle = future_ready_instr.ready_cycle.0;
                    // Fast-forward time to when this instr is ready
                    assert!(ready_cycle > current_cycle);
                    current_cycle = ready_cycle;
                    future_ready_instr.index
                } else {
                    ready_instrs.remove(&ready_instr);
                    ready_instr.index
                }
            };

            // Schedule the instuction
            let predicted_new_used_gprs_peak = max(
                self.new_used_gprs_peak1(next_idx),
                self.new_used_gprs_peak2(next_idx),
            );
            let predicted_new_used_gprs_net = self.new_used_gprs_net(next_idx);

            if predicted_new_used_gprs_peak > thresholds.quit_threshold {
                return None;
            }

            let outgoing_edges = &g.nodes[next_idx].outgoing_edges;
            for edge in outgoing_edges {
                let dep_instr = &mut instr_info[edge.head_idx];
                dep_instr.ready_cycle = max(
                    dep_instr.ready_cycle,
                    current_cycle + edge.label.latency,
                );
                dep_instr.num_uses -= 1;
                if dep_instr.num_uses <= 0 {
                    future_ready_instrs
                        .insert(FutureReadyInstr::new(g, edge.head_idx));
                }
            }

            // We're walking backwards, so the instr's defs are killed
            let instr = &self.instrs[next_idx];
            for dst in instr.dsts() {
                for ssa in dst.iter_ssa() {
                    self.live.remove(ssa);
                }
            }

            // We're walking backwards, so uses are now live
            for &ssa in instr.ssa_uses() {
                if self.net_live.remove(ssa) {
                    self.live.insert(ssa);
                } else {
                    // This branch should only happen if one instruction
                    // uses the same SSAValue multiple times
                    debug_assert!(!self.live.insert(ssa));
                }
            }

            instr_order.push(next_idx);
            current_cycle += 1;

            debug_assert_eq!(
                self.current_used_gprs(),
                predicted_new_used_gprs_net
            );
        }

        return Some((
            instr_order,
            PerRegFile::new_with(|f| self.live.count(f).try_into().unwrap()),
        ));
    }
}

struct InstructionOrder {
    order: Vec<usize>,
}

impl InstructionOrder {
    fn apply<'a>(
        &'a self,
        instrs: Vec<Instr>,
    ) -> impl 'a + Iterator<Item = Instr> {
        assert_eq!(self.order.len(), instrs.len());

        let mut instrs: Vec<Option<Instr>> =
            instrs.into_iter().map(|instr| Some(instr)).collect();

        self.order.iter().map(move |&i| {
            std::mem::take(&mut instrs[i]).expect("Instruction scheduled twice")
        })
    }
}

fn sched_buffer(
    max_regs: PerRegFile<i32>,
    instrs: &[Instr],
    graph: &ScheduleUnitGraph,
    live_in_count: PerRegFile<u32>,
    live_out: &LiveSet,
    thresholds: ScheduleThresholds,
) -> Option<InstructionOrder> {
    let (mut new_order, live_in_count2) = GenerateOrder::new(
        max_regs, instrs, live_out,
    )
    .generate_order(&graph.g, &graph.init_ready_list, thresholds)?;

    // If our accounting is correct, it should match live_in
    assert_eq!(
        live_in_count2,
        PerRegFile::new_with(|f| { live_in_count[f].try_into().unwrap() })
    );

    new_order.reverse();

    Some(InstructionOrder { order: new_order })
}

struct ScheduleUnitGraph {
    g: DepGraph,
    init_ready_list: Vec<usize>,
}

impl ScheduleUnitGraph {
    fn new(sm: &ShaderModelInfo, instrs: &[Instr]) -> Self {
        let mut g = generate_dep_graph(sm, instrs);

        let init_ready_list = calc_statistics(&mut g);

        // use crate::opt_instr_sched_common::save_graphviz;
        // save_graphviz(instrs, &g).unwrap();
        g.reverse();

        ScheduleUnitGraph { g, init_ready_list }
    }
}

#[derive(Default)]
struct ScheduleUnit {
    /// live counts from after the phi_srcs
    live_in_count: Option<PerRegFile<u32>>,
    /// live variables from before phi_dsts/branch
    live_out: Option<LiveSet>,

    instrs: Vec<Instr>,
    new_order: Option<InstructionOrder>,
    last_tried_schedule_type: Option<ScheduleType>,
    peak_gpr_count: i32,

    // Phis and branches aren't scheduled. Phis and par copies are the only
    // instructions that can take an arbitrary number of srs/dests and therefore
    // can overflow in net_live tracking. We simplify that accounting by not
    // handling these instructions there.
    phi_dsts: Option<Instr>,
    phi_srcs: Option<Instr>,
    branch: Option<Instr>,

    graph: Option<ScheduleUnitGraph>,
}

impl ScheduleUnit {
    fn schedule(
        &mut self,
        sm: &ShaderModelInfo,
        max_regs: PerRegFile<i32>,
        schedule_type: ScheduleType,
        thresholds: ScheduleThresholds,
    ) {
        let graph = self
            .graph
            .get_or_insert_with(|| ScheduleUnitGraph::new(sm, &self.instrs));

        self.last_tried_schedule_type = Some(schedule_type);
        let new_order = sched_buffer(
            max_regs,
            &self.instrs,
            graph,
            self.live_in_count.unwrap(),
            self.live_out.as_ref().unwrap(),
            thresholds,
        );

        if let Some(x) = new_order {
            self.new_order = Some(x);
        }
    }

    fn finish_block(&mut self, live_out: &LiveSet) {
        if self.live_out.is_none() {
            self.live_out = Some(live_out.clone());
        }
    }

    fn to_instrs(self) -> Vec<Instr> {
        let mut instrs = Vec::new();
        instrs.extend(self.phi_dsts);
        match self.new_order {
            Some(order) => instrs.extend(order.apply(self.instrs)),
            None => instrs.extend(self.instrs.into_iter()),
        }
        instrs.extend(self.phi_srcs);
        instrs.extend(self.branch);
        instrs
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ScheduleType {
    RegLimit(u8),
    Spill,
}

impl ScheduleType {
    fn thresholds(
        &self,
        max_regs: PerRegFile<i32>,
        schedule_unit: &ScheduleUnit,
    ) -> ScheduleThresholds {
        match self {
            ScheduleType::RegLimit(gpr_target) => ScheduleThresholds {
                heuristic_threshold: i32::from(*gpr_target) - TARGET_FREE,
                quit_threshold: i32::from(*gpr_target),
            },
            ScheduleType::Spill => ScheduleThresholds {
                heuristic_threshold: max_regs[RegFile::GPR]
                    - SW_RESERVED_GPRS_SPILL
                    - TARGET_FREE,
                quit_threshold: schedule_unit.peak_gpr_count,
            },
        }
    }
}

fn get_schedule_types(
    sm: &ShaderModelInfo,
    max_regs: PerRegFile<i32>,
    min_gpr_target: i32,
    max_gpr_target: i32,
    reserved_gprs: i32,
) -> Vec<ScheduleType> {
    let mut out = Vec::new();

    let mut gpr_target =
        next_occupancy_cliff_with_reserved(sm, min_gpr_target, reserved_gprs);
    while gpr_target < max_regs[RegFile::GPR] {
        out.push(ScheduleType::RegLimit(gpr_target.try_into().unwrap()));

        // We want only 1 entry that's greater than or equal to the original
        // schedule (it can be greater in cases where increasing the number of
        // registers doesn't change occupancy)
        if gpr_target >= max_gpr_target {
            return out;
        }

        gpr_target = next_occupancy_cliff_with_reserved(
            sm,
            gpr_target + 1,
            reserved_gprs,
        );
    }

    assert!(gpr_target >= max_regs[RegFile::GPR]);
    out.push(ScheduleType::RegLimit(
        (max_regs[RegFile::GPR] - SW_RESERVED_GPRS)
            .try_into()
            .unwrap(),
    ));

    // Only allow spilling if the original schedule spilled
    if max_gpr_target > max_regs[RegFile::GPR] {
        out.push(ScheduleType::Spill);
    }
    return out;
}

impl Function {
    pub fn opt_instr_sched_prepass(
        &mut self,
        sm: &ShaderModelInfo,
        max_regs: PerRegFile<i32>,
    ) {
        let liveness = SimpleLiveness::for_function(self);
        let mut live_out_sets: Vec<LiveSet> = Vec::new();

        #[cfg(debug_assertions)]
        let orig_instr_counts: Vec<usize> =
            self.blocks.iter().map(|b| b.instrs.len()).collect();

        let reserved_gprs = SW_RESERVED_GPRS + (sm.hw_reserved_gprs() as i32);

        // First pass: Set up data structures and gather some statistics about
        // register pressure

        // lower and upper bounds for how many gprs we will use
        let mut min_gpr_target = 1;
        let mut max_gpr_target = 1;

        let mut schedule_units = Vec::new();

        for block_idx in 0..self.blocks.len() {
            let block_live = liveness.block_live(block_idx);
            let mut live_set = match self.blocks.pred_indices(block_idx) {
                [] => LiveSet::new(),
                [pred, ..] => LiveSet::from_iter(
                    live_out_sets[*pred]
                        .iter()
                        .filter(|ssa| block_live.is_live_in(ssa))
                        .cloned(),
                ),
            };

            let block = &mut self.blocks[block_idx];
            let mut unit: ScheduleUnit = Default::default();

            for (ip, instr) in
                std::mem::take(&mut block.instrs).into_iter().enumerate()
            {
                let starts_block = match instr.op {
                    Op::PhiDsts(_) => true,
                    _ => false,
                };
                let ends_block = match instr.op {
                    Op::PhiSrcs(_) => true,
                    _ => instr.op.is_branch(),
                };

                // First use the live set before the instr
                if !starts_block && unit.live_in_count == None {
                    unit.live_in_count =
                        Some(PerRegFile::new_with(|f| live_set.count(f)));
                }
                if ends_block {
                    unit.finish_block(&live_set);
                }

                // Update the live set
                let live_count =
                    live_set.insert_instr_top_down(ip, &instr, block_live);

                // Now use the live set after the instruction
                {
                    let live_count = PerRegFile::new_with(|f| {
                        live_count[f].try_into().unwrap()
                    });
                    let mut used_gprs = calc_used_gprs(live_count, max_regs);

                    if let Op::RegOut(reg_out) = &instr.op {
                        // This should be the last instruction.  Everything should
                        // be dead once we've processed it.
                        assert_eq!(live_set.count(RegFile::GPR), 0);
                        let num_gprs_out =
                            reg_out.srcs.len().try_into().unwrap();
                        used_gprs = max(used_gprs, num_gprs_out);
                    }

                    // We never want our target to be worse than the original schedule
                    max_gpr_target = max(max_gpr_target, used_gprs);

                    if side_effect_type(&instr.op) == SideEffect::Barrier {
                        // If we can't reorder an instruction, then it forms a lower
                        // bound on how well we can do after rescheduling
                        min_gpr_target = max(min_gpr_target, used_gprs);
                    }

                    if !starts_block && !ends_block {
                        unit.peak_gpr_count =
                            max(unit.peak_gpr_count, used_gprs);
                    }
                }

                match instr.op {
                    Op::PhiDsts(_) => {
                        unit.phi_dsts = Some(instr);
                    }
                    Op::PhiSrcs(_) => {
                        unit.phi_srcs = Some(instr);
                    }
                    _ => {
                        if instr.op.is_branch() {
                            unit.branch = Some(instr);
                        } else {
                            assert!(unit.live_out.is_none());
                            unit.instrs.push(instr);
                        }
                    }
                };
            }
            unit.finish_block(&live_set);
            schedule_units.push(unit);

            live_out_sets.push(live_set);
        }

        // Second pass: Generate a schedule for each schedule_unit
        let mut schedule_types = get_schedule_types(
            sm,
            max_regs,
            min_gpr_target,
            max_gpr_target,
            reserved_gprs,
        );
        schedule_types.reverse();

        for u in schedule_units.iter_mut() {
            if u.instrs.is_empty() {
                continue;
            }
            loop {
                let schedule_type = *schedule_types.last().unwrap();
                let thresholds = schedule_type.thresholds(max_regs, u);

                u.schedule(sm, max_regs, schedule_type, thresholds);

                if u.new_order.is_some() {
                    // Success!
                    break;
                }

                if schedule_types.len() > 1 {
                    // We've failed to schedule using the existing settings, so
                    // switch to the next schedule type, which will have more
                    // gprs
                    schedule_types.pop();
                } else {
                    // No other schedule types to try - this implies that the
                    // original program has a better instruction order than what
                    // our heuristics can generate. Just keep the original
                    // instruction order
                    break;
                }
            }
        }

        // Third pass: Apply the generated schedules
        let schedule_type = schedule_types.into_iter().last().unwrap();

        for (mut u, block) in
            schedule_units.into_iter().zip(self.blocks.iter_mut())
        {
            // If the global register limit has increased, then we can schedule
            // again with the new parameters
            if !u.instrs.is_empty()
                && u.last_tried_schedule_type != Some(schedule_type)
            {
                let thresholds = schedule_type.thresholds(max_regs, &u);
                u.schedule(sm, max_regs, schedule_type, thresholds);
            }

            block.instrs = u.to_instrs();
        }

        #[cfg(debug_assertions)]
        debug_assert_eq!(
            orig_instr_counts,
            self.blocks
                .iter()
                .map(|b| b.instrs.len())
                .collect::<Vec<usize>>()
        );

        if let ScheduleType::RegLimit(limit) = schedule_type {
            // Our liveness calculations should ideally agree with SimpleLiveness
            debug_assert!(
                {
                    let live = SimpleLiveness::for_function(self);
                    let max_live = live.calc_max_live(self);
                    max_live[RegFile::GPR]
                } <= limit.into()
            );
        }
    }
}

impl Shader<'_> {
    /// Pre-RA instruction scheduling
    ///
    /// We prioritize:
    /// 1. Occupancy
    /// 2. Preventing spills to memory
    /// 3. Instruction level parallelism
    ///
    /// We accomplish this by having an outer loop that tries different register
    /// limits in order of most to least occupancy. The inner loop computes
    /// actual schedules using a heuristic inspired by Goodman & Hsu 1988
    /// section 3, although the heuristic from that paper cannot be used
    /// directly here because they assume a single register file and we have
    /// multiple. Care is also taken to model quirks of register pressure on
    /// NVIDIA GPUs corretly.
    ///
    /// J. R. Goodman and W.-C. Hsu. 1988. Code scheduling and register
    ///     allocation in large basic blocks. In Proceedings of the 2nd
    ///     international conference on Supercomputing (ICS '88). Association
    ///     for Computing Machinery, New York, NY, USA, 442–452.
    ///     https://doi.org/10.1145/55364.55407
    pub fn opt_instr_sched_prepass(&mut self) {
        if DEBUG.annotate() {
            self.remove_annotations();
        }

        let mut max_regs = PerRegFile::<i32>::new_with(|f| {
            self.sm.num_regs(f).try_into().unwrap()
        });
        if let ShaderStageInfo::Compute(cs_info) = &self.info.stage {
            max_regs[RegFile::GPR] = min(
                max_regs[RegFile::GPR],
                (gpr_limit_from_local_size(&cs_info.local_size)
                    - self.sm.hw_reserved_gprs())
                .try_into()
                .unwrap(),
            );
        }
        max_regs[RegFile::GPR] -= SW_RESERVED_GPRS;

        for f in &mut self.functions {
            f.opt_instr_sched_prepass(self.sm, max_regs);
        }
    }
}
