Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 36 additions & 17 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,13 @@ jobs:
strategy:
fail-fast: false
matrix:
# TODO: re-enable windows-latest when Windows CI is green again
os: [macos-latest, ubuntu-24.04]
# TODO: re-enable windows-latest when Windows CI is green again.
# When enabling, add a windows-latest row below and use the DX12-only Goldy test steps.
include:
# Linux: lavapipe (software Vulkan) on free ubuntu-24.04 runners.
# Requires Mesa 25.0+ via ci/setup-ubuntu.sh for Vulkan 1.4.
- os: ubuntu-24.04
runner: linux-gpu
runner: ubuntu-24.04
gpu: 'yes'
- os: macos-latest
runner: macos-latest
Expand Down Expand Up @@ -225,22 +227,17 @@ jobs:
if: matrix.os == 'ubuntu-24.04'
run: bash ci/setup-ubuntu.sh

- name: Print Vulkan GPU information
- name: Verify lavapipe
if: matrix.os == 'ubuntu-24.04'
run: |
nvidia-smi
vulkaninfo --summary

NVIDIA_VK_ICD="$(find /usr /etc -path '*/vulkan/icd.d/nvidia_icd*.json' -print -quit 2>/dev/null)"
if [ -z "$NVIDIA_VK_ICD" ]; then
echo "ERROR: NVIDIA Vulkan ICD not found" >&2
if [ -z "${LAVAPIPE_ICD:-}" ]; then
echo "ERROR: LAVAPIPE_ICD not set by ci/setup-ubuntu.sh" >&2
exit 1
fi
echo "Using lavapipe ICD: $LAVAPIPE_ICD"
VK_ICD_FILENAMES="$LAVAPIPE_ICD" vulkaninfo --summary
VK_ICD_FILENAMES="$LAVAPIPE_ICD" vulkaninfo --summary | grep -Ei 'deviceName.*(llvmpipe|lavapipe)'

echo "NVIDIA_VK_ICD=$NVIDIA_VK_ICD" >> "$GITHUB_ENV"
echo "Using NVIDIA Vulkan ICD: $NVIDIA_VK_ICD"
VK_ICD_FILENAMES="$NVIDIA_VK_ICD" vulkaninfo --summary

- name: restore cache
uses: Swatinem/rust-cache@v2
with:
Expand Down Expand Up @@ -311,14 +308,27 @@ jobs:
# Skip tests that are too slow on software-emulated GPUs (lavapipe, WARP).
# These still run locally on real hardware.
EKRANO_CI_SKIP_SLOW: 'yes'
# Linux GPU runner: force the NVIDIA Vulkan ICD so tests do not fall back to lavapipe.
# Other platforms leave this empty, which is harmless for the current test matrix.
VK_ICD_FILENAMES: ${{ env.NVIDIA_VK_ICD }}
# Linux: force lavapipe ICD (set by ci/setup-ubuntu.sh). Empty on macOS/Windows.
VK_ICD_FILENAMES: ${{ env.LAVAPIPE_ICD }}
VK_LAYER_PATH: ""
# We are experimenting with git lfs, and we don't expect to run out of bandwidth.
# However, if we do, the tests are designed to be robust against that, if this environment variable is set.
# If we do run out of bandwidth, uncomment the following line.
# EKRANO_SKIP_LFS_SNAPSHOTS: all

# Ready when windows-latest is re-added to the matrix above.
- name: cargo test (Windows / DX12-only Goldy)
if: matrix.os == 'windows-latest'
run: >-
cargo test --workspace --locked
--no-default-features
-F goldy-dx12-only -F bump_estimate -F tracy
--no-fail-fast
env:
EKRANO_CI_GPU_SUPPORT: ${{ matrix.gpu }}
EKRANO_CI_SKIP_SLOW: 'yes'
GOLDY_BACKEND: dx12
VK_LAYER_PATH: ""

- name: Upload test results due to failure
uses: actions/upload-artifact@v4
Expand All @@ -331,6 +341,15 @@ jobs:
- name: cargo test --doc
run: cargo test --doc --workspace --locked --all-features --no-fail-fast

# Ready when windows-latest is re-added to the matrix above.
- name: cargo test --doc (Windows / DX12-only Goldy)
if: matrix.os == 'windows-latest'
run: >-
cargo test --doc --workspace --locked
--no-default-features
-F goldy-dx12-only -F bump_estimate -F tracy
--no-fail-fast

check-msrv:
name: cargo check (msrv)
runs-on: ${{ matrix.os }}
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions ci/setup-ubuntu.sh
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ fi
if [ -n "${GITHUB_ENV:-}" ]; then
# Running inside GitHub Actions
echo "LAVAPIPE_ICD=$LAVAPIPE_ICD" >> "$GITHUB_ENV"
echo "VK_ICD_FILENAMES=$LAVAPIPE_ICD" >> "$GITHUB_ENV"
echo "VK_LAYER_PATH=" >> "$GITHUB_ENV"
echo "GOLDY_BACKEND=vulkan" >> "$GITHUB_ENV"
else
# Running in Docker or locally -- write to a sourceable env file
Expand Down
7 changes: 5 additions & 2 deletions ekrano/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ default-target = "x86_64-unknown-linux-gnu"
targets = []

[features]
default = []
default = ["goldy-default"]
# Mirrors `goldy` default backends for local dev; CI can pass `--no-default-features -F goldy-dx12-only`.
goldy-default = ["goldy/vulkan", "goldy/dx12", "goldy/metal", "goldy/instrumentation"]
goldy-dx12-only = ["goldy/dx12"]
# Enables GPU memory usage estimation. This performs additional computations
# in order to estimate the minimum required allocations for buffers backing
# bump-allocated GPU memory.
Expand Down Expand Up @@ -46,4 +49,4 @@ static_assertions = { workspace = true }
thiserror = { workspace = true }
# TODO: Add feature for built-in bitmap emoji support?
png = { workspace = true }
goldy = { path = "../../goldy" }
goldy = { path = "../../goldy", default-features = false }
29 changes: 27 additions & 2 deletions ekrano/src/goldy_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -594,11 +594,30 @@ impl PersistentState {
}

pub(crate) fn drain_ready_bump_readbacks(&mut self, _ctx: &Context) -> Result<()> {
self.drain_bump_readbacks(false)
}

/// Wait for the pending bump submission (if any), then claim the readback.
///
/// Required on headless/`render_to_buffer` paths when
/// `host_sidecar_on_submit_worker` makes the frame orchestrator close with
/// `end_frame_externally_ordered` — `drain_all` then does not wait for the
/// scheme submission, so a non-blocking drain would skip bump feedback and
/// never retry after overflow.
pub(crate) fn wait_and_drain_bump_readbacks(&mut self, _ctx: &Context) -> Result<()> {
self.drain_bump_readbacks(true)
}

fn drain_bump_readbacks(&mut self, wait: bool) -> Result<()> {
let Some(mut submission) = self.pending_bump_submission.take() else {
return Ok(());
};
if let Some(withdraw) = self.cached_bump_withdraw.as_ref() {
if !submission.is_settled() {
if wait {
submission
.wait_until_settled()
.map_err(|e| Error::Shader(e.to_string()))?;
} else if !submission.is_settled() {
self.pending_bump_submission = Some(submission);
return Ok(());
}
Expand All @@ -611,7 +630,13 @@ impl PersistentState {
read_bump_bytes(self, &bytes);
return Ok(());
}
self.pending_bump_submission = Some(submission);
if wait {
submission
.wait_until_settled()
.map_err(|e| Error::Shader(e.to_string()))?;
} else {
self.pending_bump_submission = Some(submission);
}
Ok(())
}

Expand Down
18 changes: 8 additions & 10 deletions ekrano/src/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -477,11 +477,14 @@ impl Scene {
let encoded_stroke = self.encoding.encode_stroke_style(style);
debug_assert!(encoded_stroke, "Stroke width is non-zero");

// 2-element dash patterns are handled on the GPU (encoded in the Style).
// Longer patterns still fall back to CPU dashing.
let use_cpu_dash = !style.dash_pattern.is_empty() && style.dash_pattern.len() != 2;

if use_cpu_dash {
// Match Vello's established behavior: dashes are expanded into path segments before the
// GPU stroke stage.
if style.dash_pattern.is_empty() {
#[cfg(feature = "bump_estimate")]
self.estimator
.count_path(shape.path_elements(SHAPE_TOLERANCE), &t, Some(style));
self.encoding.encode_shape(shape, false)
} else {
let dashed = peniko::kurbo::dash(
shape.path_elements(SHAPE_TOLERANCE),
style.dash_offset,
Expand All @@ -491,11 +494,6 @@ impl Scene {
#[cfg(feature = "bump_estimate")]
self.estimator.count_path(dashed.iter().copied(), &t, Some(style));
self.encoding.encode_path_elements(dashed.into_iter(), false)
} else {
#[cfg(feature = "bump_estimate")]
self.estimator
.count_path(shape.path_elements(SHAPE_TOLERANCE), &t, Some(style));
self.encoding.encode_shape(shape, false)
}
}

Expand Down
8 changes: 4 additions & 4 deletions ekrano/src/scheme_gpu_resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,10 @@ pub(crate) fn write_image_region(
}
let raw_bytes = image_data.data.data();

// The atlas is always sampled with hardware bilinear, which requires premultiplied-alpha
// texels to avoid fringing on transparent edges. Straight-alpha images (ImageAlphaType::Alpha)
// are converted to premultiplied on the CPU before upload; premultiplied sources are used
// as-is. Callers' ImageData is never mutated.
// Fine samples the atlas with an explicit 4-tap bilinear (or nearest Load), which still
// requires premultiplied-alpha texels to avoid fringing on transparent edges.
// Straight-alpha images (ImageAlphaType::Alpha) are converted to premultiplied on the CPU
// before upload; premultiplied sources are used as-is. Callers' ImageData is never mutated.
let premul_storage;
let bytes: &[u8] = if image_data.alpha_type == peniko::ImageAlphaType::Alpha {
premul_storage = premultiply_rgba8(raw_bytes);
Expand Down
134 changes: 65 additions & 69 deletions ekrano/src/scheme_render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,8 @@ pub struct CapturedBuffers {
pub sizes: ekrano_encoding::BufferSizes,
}

/// Max flatten workgroups per queue submit. Large single dispatches can exceed the
/// Windows ~2s GPU timeout (TDR) on stressed dashed paths.
const MAX_FLATTEN_WG_PER_SUBMIT: u32 = 8;
/// Must match `FLATTEN_WG` in `ekrano_encoding` (threads per flatten workgroup).
const FLATTEN_THREADS_PER_GROUP: u32 = 256;

/// Flatten uses a single dispatch (`ConfigUniform::flatten_thread_base` stays 0).
/// Chunking via push-constant `thread_base` under-counted work on DX12.
fn dispatch_stage(
recorder: &mut SchemeRecorder<'_>,
indirect: &Buffer,
Expand Down Expand Up @@ -174,6 +170,14 @@ impl Render {
.as_ref()
.expect("alloc_or_reuse_scheme_indirect must produce indirect buffer");

if wg_counts.use_large_path_scan {
// First-level reduce only fills `path_tag_wgs` slots; `reduced` is aligned up
// to a multiple of 256 for reduce2. Zero padding so unused slots are identity.
recorder
.scheme()
.clear_parcel(&pipeline.scratch.reduced, 0, 0)
.expect("clear path_reduced before large pathtag scan");
}
dispatch_stage(
recorder,
indirect_buf,
Expand All @@ -185,51 +189,54 @@ impl Render {
pipeline.scratch.reduced.as_binding(),
],
);
dispatch_stage(
recorder,
indirect_buf,
shaders.pathtag_reduce2,
STAGE_PATHTAG_REDUCE2,
&[
pipeline.scratch.reduced.as_binding(),
pipeline.scratch.reduced2.as_binding(),
],
);
dispatch_stage(
recorder,
indirect_buf,
shaders.pathtag_scan1,
STAGE_PATHTAG_SCAN1,
&[
pipeline.scratch.reduced.as_binding(),
pipeline.scratch.reduced2.as_binding(),
pipeline.scratch.reduced_scan.as_binding(),
],
);
dispatch_stage(
recorder,
indirect_buf,
shaders.pathtag_scan,
STAGE_PATHTAG_SCAN,
&[
pipeline.config.as_binding(),
pipeline.scene.as_binding(),
pipeline.scratch.reduced.as_binding(),
pipeline.scratch.tagmonoid.as_binding(),
],
);
dispatch_stage(
recorder,
indirect_buf,
shaders.pathtag_scan_large,
STAGE_PATHTAG_SCAN_LARGE,
&[
pipeline.config.as_binding(),
pipeline.scene.as_binding(),
pipeline.scratch.reduced_scan.as_binding(),
pipeline.scratch.tagmonoid.as_binding(),
],
);
if wg_counts.use_large_path_scan {
dispatch_stage(
recorder,
indirect_buf,
shaders.pathtag_reduce2,
STAGE_PATHTAG_REDUCE2,
&[
pipeline.scratch.reduced.as_binding(),
pipeline.scratch.reduced2.as_binding(),
],
);
dispatch_stage(
recorder,
indirect_buf,
shaders.pathtag_scan1,
STAGE_PATHTAG_SCAN1,
&[
pipeline.scratch.reduced.as_binding(),
pipeline.scratch.reduced2.as_binding(),
pipeline.scratch.reduced_scan.as_binding(),
],
);
dispatch_stage(
recorder,
indirect_buf,
shaders.pathtag_scan_large,
STAGE_PATHTAG_SCAN_LARGE,
&[
pipeline.config.as_binding(),
pipeline.scene.as_binding(),
pipeline.scratch.reduced_scan.as_binding(),
pipeline.scratch.tagmonoid.as_binding(),
],
);
} else {
dispatch_stage(
recorder,
indirect_buf,
shaders.pathtag_scan,
STAGE_PATHTAG_SCAN,
&[
pipeline.config.as_binding(),
pipeline.scene.as_binding(),
pipeline.scratch.reduced.as_binding(),
pipeline.scratch.tagmonoid.as_binding(),
],
);
}

dispatch_stage(
recorder,
Expand All @@ -247,24 +254,13 @@ impl Render {
pipeline.bump.as_binding(),
pipeline.stable.lines.as_binding(),
];
let flat_wg_x = wg_counts.flatten.0;
if flat_wg_x > MAX_FLATTEN_WG_PER_SUBMIT {
let mut base_wg = 0_u32;
while base_wg < flat_wg_x {
let chunk = (flat_wg_x - base_wg).min(MAX_FLATTEN_WG_PER_SUBMIT);
let thread_base = base_wg * FLATTEN_THREADS_PER_GROUP;
recorder.dispatch_with_push_tail(shaders.flatten, (chunk, 1, 1), &flatten_bindings, &[thread_base]);
base_wg += chunk;
}
} else {
dispatch_stage(
recorder,
indirect_buf,
shaders.flatten,
STAGE_FLATTEN,
&flatten_bindings,
);
}
dispatch_stage(
recorder,
indirect_buf,
shaders.flatten,
STAGE_FLATTEN,
&flatten_bindings,
);

dispatch_stage(
recorder,
Expand Down
Loading
Loading