Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07f28df03f | ||
|
|
cde1fc8e91 |
No files matched your search
Generated
+1
-1
@@ -1540,7 +1540,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ink-notes"
|
name = "ink-notes"
|
||||||
version = "0.45.4"
|
version = "0.45.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ab_glyph",
|
"ab_glyph",
|
||||||
"android_logger",
|
"android_logger",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ink-notes"
|
name = "ink-notes"
|
||||||
version = "0.45.4"
|
version = "0.45.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Notizbuch-App für Stift, Touch und Maus unter Linux"
|
description = "Notizbuch-App für Stift, Touch und Maus unter Linux"
|
||||||
repository = "https://gitea.pansi.eu/harald/ink-notes"
|
repository = "https://gitea.pansi.eu/harald/ink-notes"
|
||||||
|
|||||||
@@ -84,6 +84,11 @@
|
|||||||
</branding>
|
</branding>
|
||||||
<content_rating type="oars-1.1" />
|
<content_rating type="oars-1.1" />
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="0.45.5" date="2026-11-14">
|
||||||
|
<description>
|
||||||
|
<p>Extremely full pages, for example on an endless notebook, no longer end the app when they are opened.</p>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="0.45.4" date="2026-11-14">
|
<release version="0.45.4" date="2026-11-14">
|
||||||
<description>
|
<description>
|
||||||
<p>A page added on another device is no longer dropped from the notebook when a change is saved here at the same moment.</p>
|
<p>A page added on another device is no longer dropped from the notebook when a change is saved here at the same moment.</p>
|
||||||
|
|||||||
@@ -84,6 +84,11 @@
|
|||||||
</branding>
|
</branding>
|
||||||
<content_rating type="oars-1.1" />
|
<content_rating type="oars-1.1" />
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="0.45.5" date="2026-11-14">
|
||||||
|
<description>
|
||||||
|
<p>Extremely full pages, for example on an endless notebook, no longer end the app when they are opened.</p>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="0.45.4" date="2026-11-14">
|
<release version="0.45.4" date="2026-11-14">
|
||||||
<description>
|
<description>
|
||||||
<p>A page added on another device is no longer dropped from the notebook when a change is saved here at the same moment.</p>
|
<p>A page added on another device is no longer dropped from the notebook when a change is saved here at the same moment.</p>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
Notebook app for pen, touch and mouse on Linux (tested on Zorin OS 18 / Ubuntu 24.04).
|
Notebook app for pen, touch and mouse on Linux (tested on Zorin OS 18 / Ubuntu 24.04).
|
||||||
|
|
||||||
### Highlights:
|
### Highlights:
|
||||||
|
- **Extremely full pages**: A page with so many strokes that they no longer fit into one graphics buffer (possible on an endless notebook) ended the app when it was opened. Such pages are now drawn from several buffers
|
||||||
- **Sync while writing, page list**: If another device added a page just while you were writing, saving here could drop that page from the notebook's page list. The two page lists are now merged: pages added or removed on either device, titles and counters are combined
|
- **Sync while writing, page list**: If another device added a page just while you were writing, saving here could drop that page from the notebook's page list. The two page lists are now merged: pages added or removed on either device, titles and counters are combined
|
||||||
- **Undo after "make space"**: Making space moves the content of the following pages, sometimes onto the next page. Undoing and redoing an older step on such a page could then add a stroke a second time. The older steps of the following pages are now set aside while the space is there and come back when making space is undone
|
- **Undo after "make space"**: Making space moves the content of the following pages, sometimes onto the next page. Undoing and redoing an older step on such a page could then add a stroke a second time. The older steps of the following pages are now set aside while the space is there and come back when making space is undone
|
||||||
- **Sync while writing**: If a change from another device reached a page just while you were writing on it, saving your stroke overwrote that change. Now your version is saved and theirs is kept as a page of its own ("Conflict copy: …"), as for other sync conflicts; if you changed nothing on the page, their version is simply taken over
|
- **Sync while writing**: If a change from another device reached a page just while you were writing on it, saving your stroke overwrote that change. Now your version is saved and theirs is kept as a page of its own ("Conflict copy: …"), as for other sync conflicts; if you changed nothing on the page, their version is simply taken over
|
||||||
|
|||||||
+325
-72
@@ -160,8 +160,120 @@ struct NeighborPage {
|
|||||||
/// Papier (Dreiecke) und Muster (Rechteck für `fs_pattern`)
|
/// Papier (Dreiecke) und Muster (Rechteck für `fs_pattern`)
|
||||||
paper_buffer: wgpu::Buffer,
|
paper_buffer: wgpu::Buffer,
|
||||||
pattern_buffer: Option<wgpu::Buffer>,
|
pattern_buffer: Option<wgpu::Buffer>,
|
||||||
vertex_buffer: Option<wgpu::Buffer>,
|
strokes: StrokeBuffers,
|
||||||
stroke_ranges: Vec<Range<u32>>,
|
}
|
||||||
|
|
||||||
|
/// Part of a page's stroke vertices that goes into one GPU buffer.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
struct ChunkPlan {
|
||||||
|
/// Position in the page's vertex cache
|
||||||
|
vertices: Range<usize>,
|
||||||
|
/// The strokes in it, relative to the start of the chunk
|
||||||
|
strokes: Vec<Range<u32>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Splits the strokes of a page (ranges in its vertex cache) into runs of at most
|
||||||
|
/// `max_vertices`, without cutting a stroke. Nearly every page is a single run; only a huge
|
||||||
|
/// one, in practice an endless page, needs more than one buffer. A single stroke larger than
|
||||||
|
/// a buffer cannot be drawn and is left out.
|
||||||
|
fn plan_stroke_chunks(stroke_ranges: &[Range<u32>], max_vertices: usize) -> Vec<ChunkPlan> {
|
||||||
|
let mut chunks: Vec<ChunkPlan> = Vec::new();
|
||||||
|
for range in stroke_ranges {
|
||||||
|
let (start, end) = (range.start as usize, range.end as usize);
|
||||||
|
if end <= start || end - start > max_vertices {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let fits = chunks
|
||||||
|
.last()
|
||||||
|
.is_some_and(|c| c.vertices.end == start && end - c.vertices.start <= max_vertices);
|
||||||
|
if !fits {
|
||||||
|
chunks.push(ChunkPlan {
|
||||||
|
vertices: start..start,
|
||||||
|
strokes: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(chunk) = chunks.last_mut() {
|
||||||
|
let offset = (start - chunk.vertices.start) as u32;
|
||||||
|
chunk.strokes.push(offset..offset + (end - start) as u32);
|
||||||
|
chunk.vertices.end = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chunks
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StrokeChunk {
|
||||||
|
buffer: wgpu::Buffer,
|
||||||
|
/// In vertices
|
||||||
|
capacity: usize,
|
||||||
|
strokes: Vec<Range<u32>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The finished strokes of a page on the GPU, in as many vertex buffers as the size limit
|
||||||
|
/// of the device requires (one buffer beyond the limit is a validation error that ends the
|
||||||
|
/// app).
|
||||||
|
#[derive(Default)]
|
||||||
|
struct StrokeBuffers {
|
||||||
|
chunks: Vec<StrokeChunk>,
|
||||||
|
/// Chunks holding the current content; further ones are kept for their capacity
|
||||||
|
used: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StrokeBuffers {
|
||||||
|
/// Uploads the vertex cache of a page. `spare` leaves room for more strokes (at least
|
||||||
|
/// that many vertices and twice the need), so drawing does not reallocate every time.
|
||||||
|
fn upload(
|
||||||
|
&mut self,
|
||||||
|
device: &wgpu::Device,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
vertices: &[InkVertex],
|
||||||
|
stroke_ranges: &[Range<u32>],
|
||||||
|
spare: Option<usize>,
|
||||||
|
) {
|
||||||
|
let vertex_size = std::mem::size_of::<InkVertex>();
|
||||||
|
let max_vertices = (device.limits().max_buffer_size / vertex_size as u64) as usize;
|
||||||
|
let mut used = 0;
|
||||||
|
for part in plan_stroke_chunks(stroke_ranges, max_vertices) {
|
||||||
|
let Some(data) = vertices.get(part.vertices.clone()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if self
|
||||||
|
.chunks
|
||||||
|
.get(used)
|
||||||
|
.is_none_or(|c| c.capacity < data.len())
|
||||||
|
{
|
||||||
|
let capacity = match spare {
|
||||||
|
Some(initial) => grown_vertex_capacity(data.len(), initial, max_vertices),
|
||||||
|
None => data.len(),
|
||||||
|
};
|
||||||
|
let chunk = StrokeChunk {
|
||||||
|
buffer: device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some("Strokes Vertex Buffer"),
|
||||||
|
size: (capacity * vertex_size) as u64,
|
||||||
|
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
}),
|
||||||
|
capacity,
|
||||||
|
strokes: Vec::new(),
|
||||||
|
};
|
||||||
|
if used < self.chunks.len() {
|
||||||
|
self.chunks[used] = chunk;
|
||||||
|
} else {
|
||||||
|
self.chunks.push(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let chunk = &mut self.chunks[used];
|
||||||
|
queue.write_buffer(&chunk.buffer, 0, bytemuck::cast_slice(data));
|
||||||
|
chunk.strokes = part.strokes;
|
||||||
|
used += 1;
|
||||||
|
}
|
||||||
|
self.used = used;
|
||||||
|
// Keep the first buffer for the next page, give the memory of the others back
|
||||||
|
self.chunks.truncate(used.max(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn in_use(&self) -> &[StrokeChunk] {
|
||||||
|
&self.chunks[..self.used.min(self.chunks.len())]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CanvasState {
|
pub struct CanvasState {
|
||||||
@@ -238,10 +350,7 @@ pub struct CanvasState {
|
|||||||
pub spline_config: SplineConfig,
|
pub spline_config: SplineConfig,
|
||||||
|
|
||||||
// Pipeline-Trennung: Abgeschlossene Striche der aktuellen Seite
|
// Pipeline-Trennung: Abgeschlossene Striche der aktuellen Seite
|
||||||
pub committed_vertex_buffer: wgpu::Buffer,
|
committed: StrokeBuffers,
|
||||||
pub committed_vertex_capacity: usize,
|
|
||||||
pub committed_vertex_count: u32,
|
|
||||||
pub committed_strokes: Vec<Range<u32>>,
|
|
||||||
|
|
||||||
// Eingefügte Bilder der aktuellen Seite
|
// Eingefügte Bilder der aktuellen Seite
|
||||||
image_layer: crate::image_layer::ImageLayer,
|
image_layer: crate::image_layer::ImageLayer,
|
||||||
@@ -575,13 +684,6 @@ impl CanvasState {
|
|||||||
mapped_at_creation: false,
|
mapped_at_creation: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
let committed_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
||||||
label: Some("Committed Strokes Vertex Buffer"),
|
|
||||||
size: (Self::INITIAL_COMMITTED_CAPACITY * std::mem::size_of::<InkVertex>()) as u64,
|
|
||||||
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
|
||||||
mapped_at_creation: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
let image_layer = crate::image_layer::ImageLayer::new(
|
let image_layer = crate::image_layer::ImageLayer::new(
|
||||||
&device,
|
&device,
|
||||||
&shader,
|
&shader,
|
||||||
@@ -641,10 +743,7 @@ impl CanvasState {
|
|||||||
max_radius: 3.75, // 0.375 mm bei vollem Druck
|
max_radius: 3.75, // 0.375 mm bei vollem Druck
|
||||||
samples_per_segment: 16,
|
samples_per_segment: 16,
|
||||||
},
|
},
|
||||||
committed_vertex_buffer,
|
committed: StrokeBuffers::default(),
|
||||||
committed_vertex_capacity: Self::INITIAL_COMMITTED_CAPACITY,
|
|
||||||
committed_vertex_count: 0,
|
|
||||||
committed_strokes: Vec::with_capacity(128),
|
|
||||||
image_layer,
|
image_layer,
|
||||||
neighbors: Vec::new(),
|
neighbors: Vec::new(),
|
||||||
camera_layout: bind_group_layout,
|
camera_layout: bind_group_layout,
|
||||||
@@ -1013,33 +1112,12 @@ impl CanvasState {
|
|||||||
let quads = self.page_image_quads(page, self.hidden_text);
|
let quads = self.page_image_quads(page, self.hidden_text);
|
||||||
self.image_layer.set_quads(quads);
|
self.image_layer.set_quads(quads);
|
||||||
|
|
||||||
self.committed_vertex_count = page.vertex_cache.len() as u32;
|
self.committed.upload(
|
||||||
self.committed_strokes = page.stroke_ranges.clone();
|
&self.device,
|
||||||
|
&self.queue,
|
||||||
if page.vertex_cache.is_empty() {
|
&page.vertex_cache,
|
||||||
return;
|
&page.stroke_ranges,
|
||||||
}
|
Some(Self::INITIAL_COMMITTED_CAPACITY),
|
||||||
|
|
||||||
if page.vertex_cache.len() > self.committed_vertex_capacity {
|
|
||||||
let vertex_size = std::mem::size_of::<InkVertex>() as u64;
|
|
||||||
let max_vertices = (self.device.limits().max_buffer_size / vertex_size) as usize;
|
|
||||||
self.committed_vertex_capacity = grown_vertex_capacity(
|
|
||||||
page.vertex_cache.len(),
|
|
||||||
Self::INITIAL_COMMITTED_CAPACITY,
|
|
||||||
max_vertices,
|
|
||||||
);
|
|
||||||
self.committed_vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
|
||||||
label: Some("Committed Strokes Vertex Buffer (Reallocated)"),
|
|
||||||
size: (self.committed_vertex_capacity * std::mem::size_of::<InkVertex>()) as u64,
|
|
||||||
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
|
||||||
mapped_at_creation: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
self.queue.write_buffer(
|
|
||||||
&self.committed_vertex_buffer,
|
|
||||||
0,
|
|
||||||
bytemuck::cast_slice(&page.vertex_cache),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1084,19 +1162,14 @@ impl CanvasState {
|
|||||||
resource: camera_buffer.as_entire_binding(),
|
resource: camera_buffer.as_entire_binding(),
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
let vertex_buffer_with = |label: &str, vertices: &[InkVertex]| {
|
let mut strokes = StrokeBuffers::default();
|
||||||
(!vertices.is_empty()).then(|| {
|
strokes.upload(
|
||||||
let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
&self.device,
|
||||||
label: Some(label),
|
&self.queue,
|
||||||
size: std::mem::size_of_val(vertices) as u64,
|
&page.vertex_cache,
|
||||||
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
&page.stroke_ranges,
|
||||||
mapped_at_creation: false,
|
None,
|
||||||
});
|
);
|
||||||
self.queue
|
|
||||||
.write_buffer(&buffer, 0, bytemuck::cast_slice(vertices));
|
|
||||||
buffer
|
|
||||||
})
|
|
||||||
};
|
|
||||||
// Eigenes Papier und Muster (Nachbarseiten können anders groß sein)
|
// Eigenes Papier und Muster (Nachbarseiten können anders groß sein)
|
||||||
let paper = generate_paper_quad(self.paper_theme.paper_color(), page.size);
|
let paper = generate_paper_quad(self.paper_theme.paper_color(), page.size);
|
||||||
let pattern = generate_pattern_quad(
|
let pattern = generate_pattern_quad(
|
||||||
@@ -1130,8 +1203,7 @@ impl CanvasState {
|
|||||||
camera_bind_group,
|
camera_bind_group,
|
||||||
paper_buffer,
|
paper_buffer,
|
||||||
pattern_buffer,
|
pattern_buffer,
|
||||||
vertex_buffer: vertex_buffer_with("Neighbor Strokes Buffer", &page.vertex_cache),
|
strokes,
|
||||||
stroke_ranges: page.stroke_ranges.clone(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1492,17 +1564,15 @@ impl CanvasState {
|
|||||||
render_pass.draw(0..6, 0..1);
|
render_pass.draw(0..6, 0..1);
|
||||||
}
|
}
|
||||||
self.image_layer.draw(&mut render_pass, i + 1);
|
self.image_layer.draw(&mut render_pass, i + 1);
|
||||||
if let Some(buffer) = &neighbor.vertex_buffer {
|
|
||||||
render_pass.set_pipeline(&self.strip_pipeline);
|
render_pass.set_pipeline(&self.strip_pipeline);
|
||||||
render_pass.set_vertex_buffer(0, buffer.slice(..));
|
for chunk in neighbor.strokes.in_use() {
|
||||||
for range in &neighbor.stroke_ranges {
|
render_pass.set_vertex_buffer(0, chunk.buffer.slice(..));
|
||||||
if range.end > range.start {
|
for range in &chunk.strokes {
|
||||||
self.next_stroke_stencil(&mut render_pass, &mut stencil_ref);
|
self.next_stroke_stencil(&mut render_pass, &mut stencil_ref);
|
||||||
render_pass.draw(range.clone(), 0..1);
|
render_pass.draw(range.clone(), 0..1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 2D-Kamera Uniforms (Scale & Offset) binden
|
// 2D-Kamera Uniforms (Scale & Offset) binden
|
||||||
render_pass.set_bind_group(0, &self.camera_bind_group, &[]);
|
render_pass.set_bind_group(0, &self.camera_bind_group, &[]);
|
||||||
@@ -1525,15 +1595,13 @@ impl CanvasState {
|
|||||||
// 4. Abgeschlossene Striche der aktiven Seite rendern (TriangleStrip mit Vertex-Farben)
|
// 4. Abgeschlossene Striche der aktiven Seite rendern (TriangleStrip mit Vertex-Farben)
|
||||||
render_pass.set_pipeline(&self.strip_pipeline);
|
render_pass.set_pipeline(&self.strip_pipeline);
|
||||||
|
|
||||||
if self.committed_vertex_count > 0 {
|
for chunk in self.committed.in_use() {
|
||||||
render_pass.set_vertex_buffer(0, self.committed_vertex_buffer.slice(..));
|
render_pass.set_vertex_buffer(0, chunk.buffer.slice(..));
|
||||||
for range in &self.committed_strokes {
|
for range in &chunk.strokes {
|
||||||
if range.end > range.start {
|
|
||||||
self.next_stroke_stencil(&mut render_pass, &mut stencil_ref);
|
self.next_stroke_stencil(&mut render_pass, &mut stencil_ref);
|
||||||
render_pass.draw(range.clone(), 0..1);
|
render_pass.draw(range.clone(), 0..1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Temporäre Live-Geometrie des aktuellen Strichs rendern (Zero Latency)
|
// 5. Temporäre Live-Geometrie des aktuellen Strichs rendern (Zero Latency)
|
||||||
if self.live_vertex_count > 0 {
|
if self.live_vertex_count > 0 {
|
||||||
@@ -1741,9 +1809,7 @@ fn text_resolution(screen_px_per_unit: f32) -> f32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Capacity for a vertex buffer that has to hold `needed` vertices: twice as much, so that
|
/// Capacity for a vertex buffer that has to hold `needed` vertices: twice as much, so that
|
||||||
/// it is not reallocated with every stroke, but no more than the device allows. Doubling
|
/// it is not reallocated with every stroke, but no more than the device allows.
|
||||||
/// alone asked for a buffer beyond the limit as soon as a very full page used half of it,
|
|
||||||
/// which ends in a validation panic.
|
|
||||||
fn grown_vertex_capacity(needed: usize, initial: usize, max_vertices: usize) -> usize {
|
fn grown_vertex_capacity(needed: usize, initial: usize, max_vertices: usize) -> usize {
|
||||||
needed
|
needed
|
||||||
.saturating_mul(2)
|
.saturating_mul(2)
|
||||||
@@ -1756,6 +1822,193 @@ fn grown_vertex_capacity(needed: usize, initial: usize, max_vertices: usize) ->
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(clippy::single_range_in_vec_init)]
|
||||||
|
fn test_strokes_are_split_into_buffers_within_the_limit() {
|
||||||
|
// The usual case: one buffer, ranges as in the vertex cache
|
||||||
|
let ranges = vec![0..10, 10..10, 10..25, 25..40];
|
||||||
|
let plan = plan_stroke_chunks(&ranges, 1000);
|
||||||
|
assert_eq!(
|
||||||
|
plan,
|
||||||
|
vec![ChunkPlan {
|
||||||
|
vertices: 0..40,
|
||||||
|
strokes: vec![0..10, 10..25, 25..40],
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
|
||||||
|
// More than fits: several buffers, no stroke cut, ranges relative to their buffer
|
||||||
|
let plan = plan_stroke_chunks(&ranges, 30);
|
||||||
|
assert_eq!(
|
||||||
|
plan,
|
||||||
|
vec![
|
||||||
|
ChunkPlan {
|
||||||
|
vertices: 0..25,
|
||||||
|
strokes: vec![0..10, 10..25],
|
||||||
|
},
|
||||||
|
ChunkPlan {
|
||||||
|
vertices: 25..40,
|
||||||
|
strokes: vec![0..15],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert!(plan.iter().all(|c| c.vertices.len() <= 30));
|
||||||
|
|
||||||
|
// A stroke larger than a buffer is left out, the others are still drawn
|
||||||
|
let plan = plan_stroke_chunks(&[0..10, 10..60, 60..70], 30);
|
||||||
|
assert_eq!(
|
||||||
|
plan,
|
||||||
|
vec![
|
||||||
|
ChunkPlan {
|
||||||
|
vertices: 0..10,
|
||||||
|
strokes: vec![0..10],
|
||||||
|
},
|
||||||
|
ChunkPlan {
|
||||||
|
vertices: 60..70,
|
||||||
|
strokes: vec![0..10],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert!(plan_stroke_chunks(&[], 30).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Uploads and draws a page whose strokes exceed the buffer limit of a real device (the
|
||||||
|
/// limit is set very low for the test). Skipped where no graphics adapter is available.
|
||||||
|
#[test]
|
||||||
|
fn test_page_beyond_the_buffer_limit_is_drawn_from_several_buffers() {
|
||||||
|
let vertex_size = std::mem::size_of::<InkVertex>() as u64;
|
||||||
|
// 40 vertices per buffer
|
||||||
|
let limits = wgpu::Limits {
|
||||||
|
max_buffer_size: 40 * vertex_size,
|
||||||
|
..wgpu::Limits::default()
|
||||||
|
};
|
||||||
|
let instance = wgpu::Instance::default();
|
||||||
|
let mut found = None;
|
||||||
|
for force_fallback_adapter in [false, true] {
|
||||||
|
let options = wgpu::RequestAdapterOptions {
|
||||||
|
force_fallback_adapter,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
if let Some(adapter) = pollster::block_on(instance.request_adapter(&options)) {
|
||||||
|
let descriptor = wgpu::DeviceDescriptor {
|
||||||
|
label: None,
|
||||||
|
required_features: wgpu::Features::empty(),
|
||||||
|
required_limits: limits.clone(),
|
||||||
|
};
|
||||||
|
if let Ok(pair) = pollster::block_on(adapter.request_device(&descriptor, None)) {
|
||||||
|
found = Some(pair);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some((device, queue)) = found else {
|
||||||
|
eprintln!("no graphics adapter, test skipped");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 10 strokes of 12 vertices: 120 vertices, three times the limit
|
||||||
|
let vertices = vec![InkVertex::new(0.0, 0.0, [0.0, 0.0, 0.0, 1.0]); 120];
|
||||||
|
let ranges: Vec<Range<u32>> = (0..10).map(|i| i * 12..(i + 1) * 12).collect();
|
||||||
|
|
||||||
|
// The limit is real: one buffer for everything is refused
|
||||||
|
device.push_error_scope(wgpu::ErrorFilter::Validation);
|
||||||
|
let _ = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: None,
|
||||||
|
size: vertices.len() as u64 * vertex_size,
|
||||||
|
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
assert!(pollster::block_on(device.pop_error_scope()).is_some());
|
||||||
|
|
||||||
|
device.push_error_scope(wgpu::ErrorFilter::Validation);
|
||||||
|
let mut strokes = StrokeBuffers::default();
|
||||||
|
strokes.upload(&device, &queue, &vertices, &ranges, Some(16));
|
||||||
|
assert_eq!(strokes.in_use().len(), 4);
|
||||||
|
let drawn: usize = strokes.in_use().iter().map(|c| c.strokes.len()).sum();
|
||||||
|
assert_eq!(drawn, 10);
|
||||||
|
|
||||||
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: None,
|
||||||
|
source: wgpu::ShaderSource::Wgsl(
|
||||||
|
"@vertex fn vs(@location(0) p: vec2<f32>, @location(1) c: vec4<f32>)
|
||||||
|
-> @builtin(position) vec4<f32> { return vec4<f32>(p, 0.0, 1.0) + c * 0.0; }
|
||||||
|
@fragment fn fs() -> @location(0) vec4<f32> { return vec4<f32>(1.0); }"
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
let format = wgpu::TextureFormat::Rgba8Unorm;
|
||||||
|
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: None,
|
||||||
|
layout: None,
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: "vs",
|
||||||
|
buffers: &[InkVertex::desc()],
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: "fs",
|
||||||
|
targets: &[Some(format.into())],
|
||||||
|
}),
|
||||||
|
primitive: wgpu::PrimitiveState {
|
||||||
|
topology: wgpu::PrimitiveTopology::TriangleStrip,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
depth_stencil: None,
|
||||||
|
multisample: wgpu::MultisampleState::default(),
|
||||||
|
multiview: None,
|
||||||
|
});
|
||||||
|
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: None,
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format,
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
let mut encoder = device.create_command_encoder(&Default::default());
|
||||||
|
{
|
||||||
|
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
|
label: None,
|
||||||
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
|
view: &view,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||||
|
store: wgpu::StoreOp::Store,
|
||||||
|
},
|
||||||
|
})],
|
||||||
|
depth_stencil_attachment: None,
|
||||||
|
timestamp_writes: None,
|
||||||
|
occlusion_query_set: None,
|
||||||
|
});
|
||||||
|
pass.set_pipeline(&pipeline);
|
||||||
|
for chunk in strokes.in_use() {
|
||||||
|
pass.set_vertex_buffer(0, chunk.buffer.slice(..));
|
||||||
|
for range in &chunk.strokes {
|
||||||
|
pass.draw(range.clone(), 0..1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queue.submit(std::iter::once(encoder.finish()));
|
||||||
|
device.poll(wgpu::Maintain::Wait);
|
||||||
|
let error = pollster::block_on(device.pop_error_scope());
|
||||||
|
assert!(error.is_none(), "{error:?}");
|
||||||
|
|
||||||
|
// A smaller page afterwards uses one buffer again and frees the others
|
||||||
|
strokes.upload(&device, &queue, &vertices[..24], &ranges[..2], Some(16));
|
||||||
|
assert_eq!(strokes.in_use().len(), 1);
|
||||||
|
assert_eq!(strokes.chunks.len(), 1);
|
||||||
|
strokes.upload(&device, &queue, &[], &[], Some(16));
|
||||||
|
assert!(strokes.in_use().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_vertex_capacity_doubles_up_to_the_device_limit() {
|
fn test_vertex_capacity_doubles_up_to_the_device_limit() {
|
||||||
assert_eq!(grown_vertex_capacity(10, 1000, 1_000_000), 1000);
|
assert_eq!(grown_vertex_capacity(10, 1000, 1_000_000), 1000);
|
||||||
|
|||||||
Reference in new issue
Block a user