Compare commits
4
Commits
main
...
pr/freebsd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c736a97432 | ||
|
|
f2b36fe277 | ||
|
|
bd944ee5df | ||
|
|
005d09503b |
No files matched your search
@@ -0,0 +1,48 @@
|
||||
# Builds and tests the workspace on FreeBSD, inside a FreeBSD VM on a Linux runner
|
||||
# (GitHub has no FreeBSD runners). GPU tests skip themselves when the VM has no adapter.
|
||||
name: FreeBSD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
paths:
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
- "**/Cargo.toml"
|
||||
- .github/workflows/freebsd.yml
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: freebsd-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
freebsd:
|
||||
name: FreeBSD 14 (build + test)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 300
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build and test in a FreeBSD VM
|
||||
uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc # v1
|
||||
with:
|
||||
release: "14.3"
|
||||
usesh: true
|
||||
cpu: 4
|
||||
mem: 12288
|
||||
# Nothing to bring back; copying the target dir out of the VM fails on large builds.
|
||||
copyback: false
|
||||
prepare: |
|
||||
pkg install -y git curl pkgconf cmake python3 libxkbcommon wayland libX11 libXcursor libXrandr libXi libxcb mesa-libs vulkan-loader gtk3 fontconfig freetype2 alsa-lib
|
||||
curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal
|
||||
run: |
|
||||
. "$HOME/.cargo/env"
|
||||
# The checkout is synced in under another owner; let tests that call git read it.
|
||||
git config --global --add safe.directory "*"
|
||||
rustc --version
|
||||
CARGO_PROFILE_DEV_DEBUG=0 cargo build --workspace --all-targets --locked
|
||||
CARGO_PROFILE_DEV_DEBUG=0 cargo test --workspace --locked
|
||||
@@ -609,7 +609,11 @@ fn camera_keys_cut_and_select() {
|
||||
let keys: Vec<(String, String)> =
|
||||
s.shortcuts.bindings.iter().filter(|b| b.command.starts_with("multicam.")).map(|b| (b.command.clone(), b.keys.clone())).collect();
|
||||
assert!(keys.contains(&("multicam.selectCamera1".into(), "1".into())), "{keys:?}");
|
||||
assert!(keys.contains(&("multicam.cutToCamera9".into(), "Ctrl+9".into())), "{keys:?}");
|
||||
assert!(keys.contains(&("multicam.cutToCamera2".into(), "Ctrl+2".into())), "{keys:?}");
|
||||
// ⌃9 is Premiere's macOS key; off macOS Ctrl+9 is Toggle All Audio Targets (⌘9 there), which
|
||||
// wins, so Cut to Camera 9 has no default key on Windows, Linux and FreeBSD
|
||||
let nine = keys.contains(&("multicam.cutToCamera9".into(), "Ctrl+9".into()));
|
||||
assert_eq!(nine, cfg!(target_os = "macos"), "{keys:?}");
|
||||
}
|
||||
|
||||
/// Colour of camera `k` (0-based) in the many-angle tests: distinct reds and greens.
|
||||
|
||||
@@ -363,6 +363,22 @@ pub struct Binding {
|
||||
pub panel: Option<String>,
|
||||
}
|
||||
|
||||
/// Off macOS the Control and Command keys are the same key. The built-in tables are written for
|
||||
/// macOS, so a binding on the Mac Control key (`⌃T`) can land on the same key as a Command binding
|
||||
/// (`⌘T`) in the same context: the Command binding wins and the Control one is left out there
|
||||
/// (that command has no default key on that platform). macOS keeps both.
|
||||
fn drop_control_collisions(b: &mut Vec<Binding>, p: Platform) {
|
||||
if p.is_mac() {
|
||||
return;
|
||||
}
|
||||
let primary: Vec<(String, Option<Chord>)> =
|
||||
b.iter().filter(|x| x.chord().is_some_and(|c| !c.mods.ctrl)).map(|x| (x.context().to_string(), x.chord().map(|c| c.effective(p)))).collect();
|
||||
b.retain(|x| {
|
||||
let Some(c) = x.chord() else { return true };
|
||||
!(c.mods.ctrl && primary.iter().any(|(ctx, k)| ctx == x.context() && *k == Some(c.effective(p))))
|
||||
});
|
||||
}
|
||||
|
||||
impl Binding {
|
||||
fn new(command: &str, keys: &str, panel: Option<&str>) -> Self {
|
||||
Binding { command: command.to_string(), keys: normalize(keys).unwrap_or_else(|_| keys.to_string()), panel: panel.map(str::to_string) }
|
||||
@@ -564,8 +580,15 @@ impl Shortcuts {
|
||||
}
|
||||
|
||||
/// Premiere-default entries FilmCraft Default adopts: commands with no shortcut of their own
|
||||
/// whose Premiere key is free in that context.
|
||||
/// whose Premiere key is free in that context on this platform.
|
||||
pub fn audit(&self) -> Vec<Binding> {
|
||||
self.audit_for(Platform::current())
|
||||
}
|
||||
|
||||
/// [`Self::audit`] for platform `p`. The Premiere table is Premiere's macOS keyboard: off macOS
|
||||
/// the Control and Command keys are the same key, so an entry whose key would collide there
|
||||
/// with one already taken (`⌃T` vs `⌘T`) is left out, and the command keeps no default key.
|
||||
pub(crate) fn audit_for(&self, platform: Platform) -> Vec<Binding> {
|
||||
let base = self.base_defaults();
|
||||
let mut added: Vec<Binding> = Vec::new();
|
||||
for (c, k, p) in presets::PREMIERE {
|
||||
@@ -573,7 +596,8 @@ impl Shortcuts {
|
||||
continue;
|
||||
}
|
||||
let b = Binding::new(c, k, panel_opt(p));
|
||||
let taken = base.iter().chain(added.iter()).any(|o| o.context() == b.context() && o.chord() == b.chord());
|
||||
let key = b.chord().map(|c| c.effective(platform));
|
||||
let taken = base.iter().chain(added.iter()).any(|o| o.context() == b.context() && o.chord().map(|c| c.effective(platform)) == key);
|
||||
if !taken {
|
||||
added.push(b);
|
||||
}
|
||||
@@ -583,8 +607,13 @@ impl Shortcuts {
|
||||
|
||||
/// The bindings of a built-in preset.
|
||||
pub fn builtin(&self, name: &str) -> Option<Vec<Binding>> {
|
||||
self.builtin_for(name, Platform::current())
|
||||
}
|
||||
|
||||
/// [`Self::builtin`] as it is built on platform `p`.
|
||||
pub(crate) fn builtin_for(&self, name: &str, p: Platform) -> Option<Vec<Binding>> {
|
||||
let mut b = self.base_defaults();
|
||||
b.extend(self.audit());
|
||||
b.extend(self.audit_for(p));
|
||||
let table: Vec<Entry> = match name {
|
||||
DEFAULT_PRESET => Vec::new(),
|
||||
PREMIERE_PRESET => presets::premiere(),
|
||||
@@ -593,6 +622,7 @@ impl Shortcuts {
|
||||
_ => return None,
|
||||
};
|
||||
self.apply_table(&mut b, &table);
|
||||
drop_control_collisions(&mut b, p);
|
||||
Some(b)
|
||||
}
|
||||
|
||||
|
||||
@@ -218,3 +218,21 @@ fn list_searches_labels_and_keys() {
|
||||
let v = s.execute("shortcuts.list", json!({"panel": "History", "assigned": true})).unwrap();
|
||||
assert_eq!(v.as_array().unwrap().len(), 2, "{v}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_keymaps_have_no_conflicts_on_any_platform() {
|
||||
// The Premiere audit used to check keys on macOS only, so off macOS `⌃9`/`⌘9`, `⌃⇧M`/`⌘⇧M` and
|
||||
// `⌃T`/`⌘T` ended up on the same Ctrl key; CI only ran on macOS, so nobody saw it.
|
||||
use crate::shortcuts::Platform;
|
||||
let s = Session::default();
|
||||
for p in [Platform::Mac, Platform::Windows, Platform::Linux] {
|
||||
let mut sc = s.shortcuts.clone();
|
||||
sc.bindings = sc.builtin_for(crate::shortcuts::DEFAULT_PRESET, p).unwrap();
|
||||
assert!(sc.conflicts(p).is_empty(), "{p:?}: {:?}", sc.conflicts(p));
|
||||
}
|
||||
// macOS keeps every Premiere key it had
|
||||
let mac = s.shortcuts.builtin_for(crate::shortcuts::DEFAULT_PRESET, Platform::Mac).unwrap();
|
||||
for (cmd, keys) in [("multicam.cutToCamera9", "Ctrl+9"), ("markers.addRange", "Ctrl+Shift+M"), ("graphics.newText", "Cmd+T")] {
|
||||
assert!(mac.iter().any(|b| b.command == cmd && b.keys == keys), "{cmd} {keys} on macOS");
|
||||
}
|
||||
}
|
||||
+27
-1
@@ -317,7 +317,23 @@ impl GpuCompositor {
|
||||
view_formats: &[],
|
||||
});
|
||||
let dummy = dummy_tex.create_view(&Default::default());
|
||||
let fx = fx::FxStage::supported(device).then(|| fx::FxStage::new(device, &shader, &layer_bgl));
|
||||
// The effect stage's pipelines can still be invalid on a device that passes the limits
|
||||
// check (Rgba32Float is not renderable everywhere, e.g. software Vulkan/GL). Validation
|
||||
// errors would panic, so they are captured: without the stage, effects render on the CPU.
|
||||
let fx = fx::FxStage::supported(device)
|
||||
.then(|| {
|
||||
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
|
||||
let stage = fx::FxStage::new(device, &shader, &layer_bgl);
|
||||
match poll_now(scope.pop()) {
|
||||
Some(None) => Some(stage),
|
||||
Some(Some(e)) => {
|
||||
log::warn!("GPU effects unavailable on this device, rendering them on the CPU: {e}");
|
||||
None
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
})
|
||||
.flatten();
|
||||
Self {
|
||||
device: device.clone(),
|
||||
queue: queue.clone(),
|
||||
@@ -846,6 +862,16 @@ struct SrcInfo {
|
||||
color: filmcraft_color::ColorInfo,
|
||||
}
|
||||
|
||||
/// Poll a future once: wgpu's native error scopes resolve immediately. `None` if it is still
|
||||
/// pending (then the caller assumes the worst).
|
||||
fn poll_now<F: std::future::Future>(f: F) -> Option<F::Output> {
|
||||
let mut f = std::pin::pin!(f);
|
||||
match f.as_mut().poll(&mut std::task::Context::from_waker(std::task::Waker::noop())) {
|
||||
std::task::Poll::Ready(v) => Some(v),
|
||||
std::task::Poll::Pending => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A render pass drawing into the accumulator.
|
||||
fn accum_pass<'e>(enc: &'e mut wgpu::CommandEncoder, view: &wgpu::TextureView, load: wgpu::LoadOp<wgpu::Color>) -> wgpu::RenderPass<'e> {
|
||||
enc.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
|
||||
@@ -758,7 +758,7 @@ fn seed_sweep() {
|
||||
];
|
||||
for seed in 1..=8u64 {
|
||||
for c in cfgs {
|
||||
check(Cfg { seed: (seed * 0x9E37_79B9_7F4A_7C15) ^ c.w as u64, ..c });
|
||||
check(Cfg { seed: seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) ^ c.w as u64, ..c });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user