1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
//! MD3 `AutoHardCutMode` inside the engine: the per-frame driver, the cut counter the hosts consume and the
//! composite-shader effect injection of mode 7 (`docs/md3.md`, "Auto hard cuts").
use super::*;
use crate::auto_cut::{Effect, toggle_effect};
impl Engine {
/// The configured `AutoHardCutMode` (0 = off), whatever the compat mode; see
/// [`Engine::auto_hard_cut_active`].
pub fn auto_hard_cut_mode(&self) -> u8 {
self.auto_cut.mode()
}
/// Set MD3's `AutoHardCutMode`: 0 = off, 1..=7 see [`crate::auto_cut`]; larger values are clamped to 7.
/// Takes effect on the next frame. Without [`lactea_render::Compat::MilkDrop3`] it is stored but never acts.
pub fn set_auto_hard_cut_mode(&mut self, mode: u8) {
self.auto_cut.set_mode(mode);
self.cfg.auto_hard_cut_mode = self.auto_cut.mode();
}
/// Whether an auto hard cut mode is on *and* the compat mode supports it
/// (`CompatRules::md3_auto_hard_cut`).
pub fn auto_hard_cut_active(&self) -> bool {
self.cfg.compat.rules().md3_auto_hard_cut && self.auto_cut.mode() > 0
}
/// The instant preset changes the auto hard cut asked for since the last call (mode 4 can ask for two in
/// one frame). The host loads another preset with [`Transition::Cut`]; with a
/// [`PresetControl`](crate::PresetControl) pass the count to
/// [`PresetControl::update_with_auto_cuts`](crate::PresetControl::update_with_auto_cuts).
pub fn take_auto_cuts(&mut self) -> u32 {
std::mem::take(&mut self.pending_auto_cuts)
}
/// Start of every frame (MD3 runs the block at the top of its render function): feed the auto cut with the
/// frame clock and the `bass` / `treb` the previous frame's per-frame code left in the running preset.
pub(super) fn auto_cut_step(&mut self, now: f64) {
if !self.auto_hard_cut_active() {
return;
}
let Some(act) = self.active.as_ref() else {
return;
};
let pf = &act.loaded.pf;
let (bass, treb) = (pf.ctx.get(pf.v.bass), pf.ctx.get(pf.v.treb));
let out = self.auto_cut.update(now, bass, treb);
self.pending_auto_cuts = self.pending_auto_cuts.saturating_add(u32::from(out.cuts));
if let Some(effect) = out.inject {
self.inject_comp_effect(effect);
}
}
/// Toggle `effect` in the running preset's composite shader (MD3 mode 7, see
/// [`toggle_effect`] for the text rules) and rebuild the shader. The text lives in the loaded preset, so
/// effects accumulate until the next preset load; injecting an effect that is already present removes it.
///
/// A preset without composite-shader text (`PSVERSION_COMP < 2`, or no `comp_N` lines) first gets MD3's
/// default composite shader generated from its legacy parameters (gamma, video echo, hue shader, brighten /
/// darken / solarize / invert) and PS version 4. If the text has no insertion point or the changed shader does
/// not translate / compile, nothing changes. Returns whether the effect was applied. MilkDrop 2 family only.
pub fn inject_comp_effect(&mut self, effect: Effect) -> bool {
if !self.cfg.compat.md2_family() {
return false;
}
let Some(mut slot) = self.active.take() else {
return false;
};
let applied = self.inject_into(&mut slot, effect);
self.active = Some(slot);
applied
}
fn inject_into(&mut self, slot: &mut Slot, effect: Effect) -> bool {
let p = &slot.loaded.preset;
let user = p
.comp_shader
.as_deref()
.filter(|t| !code_to_source(t).trim().is_empty());
let ps = p.comp_ps_version;
let base_text = match user {
Some(t) if ps >= 2 => t.to_string(),
_ => default_comp_shader(
p.base.gamma_adj,
p.base.video_echo_alpha,
p.base.video_echo_zoom,
p.base.video_echo_orientation,
p.base.shader,
p.base.brighten,
p.base.darken,
p.base.solarize,
p.base.invert,
self.cfg.compat.rules().md3_md2_comp_effects,
)
.replace('\n', "\u{1}"),
};
let new_ps = if ps < 2 { 4 } else { ps };
let Some(new_text) = toggle_effect(&base_text, effect) else {
return false;
};
// translate through the load path (same options as the preset's own shaders); warp is skipped (PS 0)
let mut tmp = Preset::default();
tmp.base = p.base.clone();
tmp.comp_shader = Some(new_text.clone());
tmp.comp_ps_version = new_ps;
let mut report = PresetReport::default();
let (_, comp) = self.build_shaders(&tmp, &mut report);
let Some(comp) = comp.filter(|_| report.comp == ShaderOutcome::User) else {
return false;
};
// on a rejected module the renderer keeps the previous composite shader
if slot.renderer.set_comp_shader(Some(comp)).is_err() {
return false;
}
// the effect snippets only sample `sampler_main`: the preset's user textures are already registered on the
// renderer, and re-resolving them would reload and re-upload every file from inside the render loop
let seed = self.cfg.seed;
let l = &mut slot.loaded;
l.preset.comp_shader = Some(new_text);
l.preset.comp_ps_version = new_ps;
l.has_comp = true;
// a preset that had no composite shader needs the shader random state its passes draw from
if self.cfg.compat.rules().shared_shader_rand {
l.warp_stream
.get_or_insert_with(|| ShaderStream::new(seed, "shader_warp"));
} else {
l.comp_stream
.get_or_insert_with(|| ShaderStream::new(seed, "shader_comp"));
}
true
}
}