Compare commits

...

20 commits

Author SHA1 Message Date
Joseph T. Lyons
5deb404135 zed 0.196.4 2025-07-18 12:40:43 -04:00
Joseph T. Lyons
619282a8ed Revert "gpui: Improve path rendering & global multisample anti-aliasing" (#34722)
Reverts zed-industries/zed#29718

We've noticed some issues with Zed on Intel-based Macs where typing has
become sluggish, and git bisect has seemed to point towards this PR.
Reverting for now, until we can understand why it is causing this issue.
2025-07-18 12:36:45 -04:00
gcp-cherry-pick-bot[bot]
3f32020785
keymap_ui: Don't panic on KeybindSource::from_meta (cherry-pick #34652) (#34677)
Cherry-picked keymap_ui: Don't panic on `KeybindSource::from_meta`
(#34652)

Closes #ISSUE

Log error instead of panicking when `from_meta` is passed an invalid
value

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-07-18 12:45:44 +03:00
gcp-cherry-pick-bot[bot]
ce0de10147
keymap_ui: Fix various keymap editor issues (cherry-pick #34647) (#34670)
Cherry-picked keymap_ui: Fix various keymap editor issues (#34647)

This PR tackles miscellaneous nits for the new keymap editor UI.

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>

Co-authored-by: Finn Evers <finn@zed.dev>
Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-07-17 20:03:03 -04:00
Richard Feldman
c9b9b3194e
zed 0.196.3 2025-07-17 19:25:14 -04:00
Richard Feldman
eeb9e242b4
Retry on burn mode (#34669)
Now we only auto-retry if burn mode is enabled. We also show a "Retry"
button (so you don't have to type "continue") if you think that's the
right remedy, and additionally we show a "Retry and Enable Burn Mode"
button if you don't have it enabled.

<img width="484" height="260" alt="Screenshot 2025-07-17 at 6 25 27 PM"
src="https://github.com/user-attachments/assets/dc5bf1f6-8b11-4041-87aa-4f37c95ea9f0"
/>

<img width="478" height="307" alt="Screenshot 2025-07-17 at 6 22 36 PM"
src="https://github.com/user-attachments/assets/1ed6578a-1696-449d-96d1-e447d11959fa"
/>


Release Notes:

- Only auto-retry Agent requests when Burn Mode is enabled
2025-07-17 19:23:38 -04:00
Richard Feldman
f9c498318d
Improve upstream error reporting (#34668)
Now we handle more upstream error cases using the same auto-retry logic.

Release Notes:

- N/A
2025-07-17 19:23:34 -04:00
gcp-cherry-pick-bot[bot]
cb40bb755e
keymap ui: Fix keymap editor search bugs (cherry-pick #34579) (#34588)
Cherry-picked keymap ui: Fix keymap editor search bugs (#34579)

Keystroke input now gets cleared when toggling to normal search mode
Main search bar is focused when toggling to normal search mode

This also gets rid of highlight on focus from keystroke_editor because
it also matched the search bool field and was redundant

Release Notes:

- N/A

Co-authored-by: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com>
2025-07-17 18:16:33 -04:00
Umesh Yadav
991887a3ea keymap_ui: Open Keymap editor from settings dropdown (#34576)
@probably-neb I guess we should be opening the keymap editor from title
bar and menu as well. I believe this got missed in this: #34568.

Release Notes:

- Open Keymap editor from settings from menu and title bar.
2025-07-17 13:37:58 -04:00
Anthony Eid
f249ee481d keymap ui: Fix keymap editor search bugs (#34579)
Keystroke input now gets cleared when toggling to normal search mode
Main search bar is focused when toggling to normal search mode

This also gets rid of highlight on focus from keystroke_editor because
it also matched the search bool field and was redundant

Release Notes:

- N/A
2025-07-17 13:37:43 -04:00
gcp-cherry-pick-bot[bot]
484e39dcba
keymap_ui: Show edit icon on hovered and selected row (cherry-pick #34630) (#34635)
Cherry-picked keymap_ui: Show edit icon on hovered and selected row
(#34630)

Closes #ISSUE

Improves the behavior of the edit icon in the far left column of the
keymap UI table. It is now shown in both the selected and the hovered
row as an indicator that the row is editable in this configuration. When
hovered a row can be double clicked or the edit icon can be clicked, and
when selected it can be edited via keyboard shortcuts. Additionally, the
edit icon and all other hover tooltips will now disappear when the table
is navigated via keyboard shortcuts.

<details><summary>Video</summary>




https://github.com/user-attachments/assets/6584810f-4c6d-4e6f-bdca-25b16c920cfc

</details>

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-07-17 12:21:49 -04:00
gcp-cherry-pick-bot[bot]
ec7d6631a4
Add keymap editor UI telemetry events (cherry-pick #34571) (#34589)
Cherry-picked Add keymap editor UI telemetry events (#34571)

- Search queries
- Keybinding update or removed
- Copy action name
- Copy context name

cc @katie-z-geer 

Release Notes:

- N/A

Co-authored-by: Ben Kunkle <ben@zed.dev>

Co-authored-by: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com>
Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-07-17 11:58:31 -04:00
gcp-cherry-pick-bot[bot]
27691613c1
keymap_ui: Improve keybind display in menus (cherry-pick #34587) (#34632)
Cherry-picked keymap_ui: Improve keybind display in menus (#34587)

Closes #ISSUE

Defines keybindings for `keymap_editor::EditBinding` and
`keymap_editor::CreateBinding`, making sure those actions are used in
tooltips.

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Finn <dev@bahn.sh>

Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Finn <dev@bahn.sh>
2025-07-17 11:34:44 -04:00
Zed Bot
5f11e09a4b Bump to 0.196.2 for @osyvokon 2025-07-17 12:14:36 +00:00
gcp-cherry-pick-bot[bot]
34e63f9e55
agent: Disable project_notifications by default (cherry-pick #34615) (#34619)
Cherry-picked agent: Disable `project_notifications` by default (#34615)

This tool needs more polishing before being generally available.

Release Notes:

- agent: Disabled `project_notifications` tool by default for the time
being

Co-authored-by: Oleksiy Syvokon <oleksiy@zed.dev>
2025-07-17 15:09:12 +03:00
gcp-cherry-pick-bot[bot]
cbdca4e090
Fix shortcuts with Shift (cherry-pick #34614) (#34616)
Cherry-picked Fix shortcuts with `Shift` (#34614)

Closes #34605, #34606, #34609

Release Notes:

- (Preview only) Fixed shortcuts involving Shift

Co-authored-by: Oleksiy Syvokon <oleksiy@zed.dev>
2025-07-17 14:31:57 +03:00
Conrad Irwin
92105e92c3 Fix ctrl-q on AZERTY on Linux (#34597)
Closes #ISSUE

Release Notes:

- N/A
2025-07-16 21:28:51 -06:00
Zed Bot
632f09efd6 Bump to 0.196.1 for @ConradIrwin 2025-07-17 02:18:34 +00:00
gcp-cherry-pick-bot[bot]
192e0e32dd
Don't override ascii graphical shortcuts (cherry-pick #34592) (#34595)
Cherry-picked Don't override ascii graphical shortcuts (#34592)

Closes #34536

Release Notes:

- (preview only) Fix shortcuts on Extended Latin keyboards on Linux

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-07-16 20:16:34 -06:00
Joseph T. Lyons
30cc8bd824 v0.196.x preview 2025-07-16 14:30:48 -04:00
36 changed files with 1831 additions and 759 deletions

10
Cargo.lock generated
View file

@ -2148,7 +2148,7 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.6.0"
source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad"
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
dependencies = [
"ash",
"ash-window",
@ -2181,7 +2181,7 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.3.0"
source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad"
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
dependencies = [
"proc-macro2",
"quote",
@ -2191,7 +2191,7 @@ dependencies = [
[[package]]
name = "blade-util"
version = "0.2.0"
source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad"
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
dependencies = [
"blade-graphics",
"bytemuck",
@ -14720,6 +14720,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
"telemetry",
"theme",
"tree-sitter-json",
"tree-sitter-rust",
@ -16451,6 +16452,7 @@ dependencies = [
"schemars",
"serde",
"settings",
"settings_ui",
"smallvec",
"story",
"telemetry",
@ -20095,7 +20097,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.196.0"
version = "0.196.4"
dependencies = [
"activity_indicator",
"agent",

View file

@ -434,9 +434,9 @@ aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
base64 = "0.22"
bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
blade-util = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
blade-util = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
blake3 = "1.5.3"
bytes = "1.0"
cargo_metadata = "0.19"
@ -489,7 +489,7 @@ json_dotpath = "1.1"
jsonschema = "0.30.0"
jsonwebtoken = "9.3"
jupyter-protocol = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed" ,rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
@ -500,7 +500,7 @@ metal = "0.29"
moka = { version = "0.12.10", features = ["sync"] }
naga = { version = "25.0", features = ["wgsl-in"] }
nanoid = "0.4"
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
nix = "0.29"
num-format = "0.4.4"
objc = "0.2"
@ -541,7 +541,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77
"stream",
] }
rsa = "0.9.6"
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
"async-dispatcher-runtime",
] }
rust-embed = { version = "8.4", features = ["include-exclude"] }

View file

@ -1118,7 +1118,9 @@
"ctrl-f": "search::FocusSearch",
"alt-find": "keymap_editor::ToggleKeystrokeSearch",
"alt-ctrl-f": "keymap_editor::ToggleKeystrokeSearch",
"alt-c": "keymap_editor::ToggleConflictFilter"
"alt-c": "keymap_editor::ToggleConflictFilter",
"enter": "keymap_editor::EditBinding",
"alt-enter": "keymap_editor::CreateBinding"
}
},
{

View file

@ -1217,7 +1217,9 @@
"use_key_equivalents": true,
"bindings": {
"cmd-alt-f": "keymap_editor::ToggleKeystrokeSearch",
"cmd-alt-c": "keymap_editor::ToggleConflictFilter"
"cmd-alt-c": "keymap_editor::ToggleConflictFilter",
"enter": "keymap_editor::EditBinding",
"alt-enter": "keymap_editor::CreateBinding"
}
},
{

View file

@ -817,7 +817,7 @@
"edit_file": true,
"fetch": true,
"list_directory": true,
"project_notifications": true,
"project_notifications": false,
"move_path": true,
"now": true,
"find_path": true,
@ -837,7 +837,7 @@
"diagnostics": true,
"fetch": true,
"list_directory": true,
"project_notifications": true,
"project_notifications": false,
"now": true,
"find_path": true,
"read_file": true,

View file

@ -396,6 +396,7 @@ pub struct Thread {
remaining_turns: u32,
configured_model: Option<ConfiguredModel>,
profile: AgentProfile,
last_error_context: Option<(Arc<dyn LanguageModel>, CompletionIntent)>,
}
#[derive(Clone, Debug)]
@ -489,10 +490,11 @@ impl Thread {
retry_state: None,
message_feedback: HashMap::default(),
last_auto_capture_at: None,
last_error_context: None,
last_received_chunk_at: None,
request_callback: None,
remaining_turns: u32::MAX,
configured_model,
configured_model: configured_model.clone(),
profile: AgentProfile::new(profile_id, tools),
}
}
@ -613,6 +615,7 @@ impl Thread {
feedback: None,
message_feedback: HashMap::default(),
last_auto_capture_at: None,
last_error_context: None,
last_received_chunk_at: None,
request_callback: None,
remaining_turns: u32::MAX,
@ -1264,9 +1267,58 @@ impl Thread {
self.flush_notifications(model.clone(), intent, cx);
let request = self.to_completion_request(model.clone(), intent, cx);
let _checkpoint = self.finalize_pending_checkpoint(cx);
self.stream_completion(
self.to_completion_request(model.clone(), intent, cx),
model,
intent,
window,
cx,
);
}
self.stream_completion(request, model, intent, window, cx);
pub fn retry_last_completion(
&mut self,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) {
// Clear any existing error state
self.retry_state = None;
// Use the last error context if available, otherwise fall back to configured model
let (model, intent) = if let Some((model, intent)) = self.last_error_context.take() {
(model, intent)
} else if let Some(configured_model) = self.configured_model.as_ref() {
let model = configured_model.model.clone();
let intent = if self.has_pending_tool_uses() {
CompletionIntent::ToolResults
} else {
CompletionIntent::UserPrompt
};
(model, intent)
} else if let Some(configured_model) = self.get_or_init_configured_model(cx) {
let model = configured_model.model.clone();
let intent = if self.has_pending_tool_uses() {
CompletionIntent::ToolResults
} else {
CompletionIntent::UserPrompt
};
(model, intent)
} else {
return;
};
self.send_to_model(model, intent, window, cx);
}
pub fn enable_burn_mode_and_retry(
&mut self,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) {
self.completion_mode = CompletionMode::Burn;
cx.emit(ThreadEvent::ProfileChanged);
self.retry_last_completion(window, cx);
}
pub fn used_tools_since_last_user_message(&self) -> bool {
@ -2146,6 +2198,35 @@ impl Thread {
max_attempts: MAX_RETRY_ATTEMPTS,
})
}
UpstreamProviderError {
status,
retry_after,
..
} => match *status {
StatusCode::TOO_MANY_REQUESTS | StatusCode::SERVICE_UNAVAILABLE => {
Some(RetryStrategy::Fixed {
delay: retry_after.unwrap_or(BASE_RETRY_DELAY),
max_attempts: MAX_RETRY_ATTEMPTS,
})
}
StatusCode::INTERNAL_SERVER_ERROR => Some(RetryStrategy::Fixed {
delay: retry_after.unwrap_or(BASE_RETRY_DELAY),
// Internal Server Error could be anything, so only retry once.
max_attempts: 1,
}),
status => {
// There is no StatusCode variant for the unofficial HTTP 529 ("The service is overloaded"),
// but we frequently get them in practice. See https://http.dev/529
if status.as_u16() == 529 {
Some(RetryStrategy::Fixed {
delay: retry_after.unwrap_or(BASE_RETRY_DELAY),
max_attempts: MAX_RETRY_ATTEMPTS,
})
} else {
None
}
}
},
ApiInternalServerError { .. } => Some(RetryStrategy::Fixed {
delay: BASE_RETRY_DELAY,
max_attempts: 1,
@ -2193,6 +2274,23 @@ impl Thread {
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) -> bool {
// Store context for the Retry button
self.last_error_context = Some((model.clone(), intent));
// Only auto-retry if Burn Mode is enabled
if self.completion_mode != CompletionMode::Burn {
// Show error with retry options
cx.emit(ThreadEvent::ShowError(ThreadError::RetryableError {
message: format!(
"{}\n\nTo automatically retry when similar errors happen, enable Burn Mode.",
error
)
.into(),
can_enable_burn_mode: true,
}));
return false;
}
let Some(strategy) = strategy.or_else(|| Self::get_retry_strategy(error)) else {
return false;
};
@ -2273,6 +2371,13 @@ impl Thread {
// Stop generating since we're giving up on retrying.
self.pending_completions.clear();
// Show error alongside a Retry button, but no
// Enable Burn Mode button (since it's already enabled)
cx.emit(ThreadEvent::ShowError(ThreadError::RetryableError {
message: format!("Failed after retrying: {}", error).into(),
can_enable_burn_mode: false,
}));
false
}
}
@ -3183,6 +3288,11 @@ pub enum ThreadError {
header: SharedString,
message: SharedString,
},
#[error("Retryable error: {message}")]
RetryableError {
message: SharedString,
can_enable_burn_mode: bool,
},
}
#[derive(Debug, Clone)]
@ -3583,6 +3693,7 @@ fn main() {{
}
#[gpui::test]
#[ignore] // turn this test on when project_notifications tool is re-enabled
async fn test_stale_buffer_notification(cx: &mut TestAppContext) {
init_test_settings(cx);
@ -4137,6 +4248,11 @@ fn main() {{
let project = create_test_project(cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
thread.update(cx, |thread, _| {
thread.set_completion_mode(CompletionMode::Burn);
});
// Create model that returns overloaded error
let model = Arc::new(ErrorInjector::new(TestError::Overloaded));
@ -4210,6 +4326,11 @@ fn main() {{
let project = create_test_project(cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
thread.update(cx, |thread, _| {
thread.set_completion_mode(CompletionMode::Burn);
});
// Create model that returns internal server error
let model = Arc::new(ErrorInjector::new(TestError::InternalServerError));
@ -4286,6 +4407,11 @@ fn main() {{
let project = create_test_project(cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
thread.update(cx, |thread, _| {
thread.set_completion_mode(CompletionMode::Burn);
});
// Create model that returns internal server error
let model = Arc::new(ErrorInjector::new(TestError::InternalServerError));
@ -4393,6 +4519,11 @@ fn main() {{
let project = create_test_project(cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
thread.update(cx, |thread, _| {
thread.set_completion_mode(CompletionMode::Burn);
});
// Create model that returns overloaded error
let model = Arc::new(ErrorInjector::new(TestError::Overloaded));
@ -4479,6 +4610,11 @@ fn main() {{
let project = create_test_project(cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
thread.update(cx, |thread, _| {
thread.set_completion_mode(CompletionMode::Burn);
});
// We'll use a wrapper to switch behavior after first failure
struct RetryTestModel {
inner: Arc<FakeLanguageModel>,
@ -4647,6 +4783,11 @@ fn main() {{
let project = create_test_project(cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
thread.update(cx, |thread, _| {
thread.set_completion_mode(CompletionMode::Burn);
});
// Create a model that fails once then succeeds
struct FailOnceModel {
inner: Arc<FakeLanguageModel>,
@ -4808,6 +4949,11 @@ fn main() {{
let project = create_test_project(cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
thread.update(cx, |thread, _| {
thread.set_completion_mode(CompletionMode::Burn);
});
// Create a model that returns rate limit error with retry_after
struct RateLimitModel {
inner: Arc<FakeLanguageModel>,
@ -5081,6 +5227,79 @@ fn main() {{
);
}
#[gpui::test]
async fn test_no_retry_without_burn_mode(cx: &mut TestAppContext) {
init_test_settings(cx);
let project = create_test_project(cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Ensure we're in Normal mode (not Burn mode)
thread.update(cx, |thread, _| {
thread.set_completion_mode(CompletionMode::Normal);
});
// Track error events
let error_events = Arc::new(Mutex::new(Vec::new()));
let error_events_clone = error_events.clone();
let _subscription = thread.update(cx, |_, cx| {
cx.subscribe(&thread, move |_, _, event: &ThreadEvent, _| {
if let ThreadEvent::ShowError(error) = event {
error_events_clone.lock().push(error.clone());
}
})
});
// Create model that returns overloaded error
let model = Arc::new(ErrorInjector::new(TestError::Overloaded));
// Insert a user message
thread.update(cx, |thread, cx| {
thread.insert_user_message("Hello!", ContextLoadResult::default(), None, vec![], cx);
});
// Start completion
thread.update(cx, |thread, cx| {
thread.send_to_model(model.clone(), CompletionIntent::UserPrompt, None, cx);
});
cx.run_until_parked();
// Verify no retry state was created
thread.read_with(cx, |thread, _| {
assert!(
thread.retry_state.is_none(),
"Should not have retry state in Normal mode"
);
});
// Check that a retryable error was reported
let errors = error_events.lock();
assert!(!errors.is_empty(), "Should have received an error event");
if let ThreadError::RetryableError {
message: _,
can_enable_burn_mode,
} = &errors[0]
{
assert!(
*can_enable_burn_mode,
"Error should indicate burn mode can be enabled"
);
} else {
panic!("Expected RetryableError, got {:?}", errors[0]);
}
// Verify the thread is no longer generating
thread.read_with(cx, |thread, _| {
assert!(
!thread.is_generating(),
"Should not be generating after error without retry"
);
});
}
#[gpui::test]
async fn test_retry_cancelled_on_stop(cx: &mut TestAppContext) {
init_test_settings(cx);
@ -5088,6 +5307,11 @@ fn main() {{
let project = create_test_project(cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
thread.update(cx, |thread, _| {
thread.set_completion_mode(CompletionMode::Burn);
});
// Create model that returns overloaded error
let model = Arc::new(ErrorInjector::new(TestError::Overloaded));

View file

@ -1036,7 +1036,7 @@ impl ActiveThread {
.collect::<Vec<_>>()
.join("\n");
self.last_error = Some(ThreadError::Message {
header: "Error interacting with language model".into(),
header: "Error".into(),
message: error_message.into(),
});
}

View file

@ -64,8 +64,9 @@ use theme::ThemeSettings;
use time::UtcOffset;
use ui::utils::WithRemSize;
use ui::{
Banner, Callout, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu,
PopoverMenuHandle, ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*,
Banner, Button, Callout, CheckboxWithLabel, ContextMenu, ElevationIndex, IconPosition,
KeyBinding, PopoverMenu, PopoverMenuHandle, ProgressBar, Tab, Tooltip, Vector, VectorName,
prelude::*,
};
use util::ResultExt as _;
use workspace::{
@ -2913,6 +2914,21 @@ impl AgentPanel {
.size(IconSize::Small)
.color(Color::Error);
let retry_button = Button::new("retry", "Retry")
.icon(IconName::RotateCw)
.icon_position(IconPosition::Start)
.on_click({
let thread = thread.clone();
move |_, window, cx| {
thread.update(cx, |thread, cx| {
thread.clear_last_error();
thread.thread().update(cx, |thread, cx| {
thread.retry_last_completion(Some(window.window_handle()), cx);
});
});
}
});
div()
.border_t_1()
.border_color(cx.theme().colors().border)
@ -2921,13 +2937,72 @@ impl AgentPanel {
.icon(icon)
.title(header)
.description(message.clone())
.primary_action(self.dismiss_error_button(thread, cx))
.secondary_action(self.create_copy_button(message_with_header))
.primary_action(retry_button)
.secondary_action(self.dismiss_error_button(thread, cx))
.tertiary_action(self.create_copy_button(message_with_header))
.bg_color(self.error_callout_bg(cx)),
)
.into_any_element()
}
fn render_retryable_error(
&self,
message: SharedString,
can_enable_burn_mode: bool,
thread: &Entity<ActiveThread>,
cx: &mut Context<Self>,
) -> AnyElement {
let icon = Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error);
let retry_button = Button::new("retry", "Retry")
.icon(IconName::RotateCw)
.icon_position(IconPosition::Start)
.on_click({
let thread = thread.clone();
move |_, window, cx| {
thread.update(cx, |thread, cx| {
thread.clear_last_error();
thread.thread().update(cx, |thread, cx| {
thread.retry_last_completion(Some(window.window_handle()), cx);
});
});
}
});
let mut callout = Callout::new()
.icon(icon)
.title("Error")
.description(message.clone())
.bg_color(self.error_callout_bg(cx))
.primary_action(retry_button);
if can_enable_burn_mode {
let burn_mode_button = Button::new("enable_burn_retry", "Enable Burn Mode and Retry")
.icon(IconName::ZedBurnMode)
.icon_position(IconPosition::Start)
.on_click({
let thread = thread.clone();
move |_, window, cx| {
thread.update(cx, |thread, cx| {
thread.clear_last_error();
thread.thread().update(cx, |thread, cx| {
thread.enable_burn_mode_and_retry(Some(window.window_handle()), cx);
});
});
}
});
callout = callout.secondary_action(burn_mode_button);
}
div()
.border_t_1()
.border_color(cx.theme().colors().border)
.child(callout)
.into_any_element()
}
fn render_prompt_editor(
&self,
context_editor: &Entity<TextThreadEditor>,
@ -3169,6 +3244,15 @@ impl Render for AgentPanel {
ThreadError::Message { header, message } => {
self.render_error_message(header, message, thread, cx)
}
ThreadError::RetryableError {
message,
can_enable_burn_mode,
} => self.render_retryable_error(
message,
can_enable_burn_mode,
thread,
cx,
),
})
.into_any(),
)

View file

@ -12,6 +12,7 @@ use collections::HashMap;
use fs::FakeFs;
use futures::{FutureExt, future::LocalBoxFuture};
use gpui::{AppContext, TestAppContext, Timer};
use http_client::StatusCode;
use indoc::{formatdoc, indoc};
use language_model::{
LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult,
@ -1675,6 +1676,30 @@ async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) ->
Timer::after(retry_after + jitter).await;
continue;
}
LanguageModelCompletionError::UpstreamProviderError {
status,
retry_after,
..
} => {
// Only retry for specific status codes
let should_retry = matches!(
*status,
StatusCode::TOO_MANY_REQUESTS | StatusCode::SERVICE_UNAVAILABLE
) || status.as_u16() == 529;
if !should_retry {
return Err(err.into());
}
// Use server-provided retry_after if available, otherwise use default
let retry_after = retry_after.unwrap_or(Duration::from_secs(5));
let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0));
eprintln!(
"Attempt #{attempt}: {err}. Retry after {retry_after:?} + jitter of {jitter:?}"
);
Timer::after(retry_after + jitter).await;
continue;
}
_ => return Err(err.into()),
},
Err(err) => return Err(err),

View file

@ -126,7 +126,7 @@ mod macos {
"ContentMask".into(),
"Uniforms".into(),
"AtlasTile".into(),
"PathInputIndex".into(),
"PathRasterizationInputIndex".into(),
"PathVertex_ScaledPixels".into(),
"ShadowInputIndex".into(),
"Shadow".into(),

View file

@ -1,13 +1,9 @@
use gpui::{
Application, Background, Bounds, ColorSpace, Context, MouseDownEvent, Path, PathBuilder,
PathStyle, Pixels, Point, Render, SharedString, StrokeOptions, Window, WindowBounds,
WindowOptions, canvas, div, linear_color_stop, linear_gradient, point, prelude::*, px, rgb,
size,
PathStyle, Pixels, Point, Render, SharedString, StrokeOptions, Window, WindowOptions, canvas,
div, linear_color_stop, linear_gradient, point, prelude::*, px, rgb, size,
};
const DEFAULT_WINDOW_WIDTH: Pixels = px(1024.0);
const DEFAULT_WINDOW_HEIGHT: Pixels = px(768.0);
struct PaintingViewer {
default_lines: Vec<(Path<Pixels>, Background)>,
lines: Vec<Vec<Point<Pixels>>>,
@ -151,6 +147,8 @@ impl PaintingViewer {
px(320.0 + (i as f32 * 10.0).sin() * 40.0),
));
}
let path = builder.build().unwrap();
lines.push((path, gpui::green().into()));
Self {
default_lines: lines.clone(),
@ -185,13 +183,9 @@ fn button(
}
impl Render for PaintingViewer {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
window.request_animation_frame();
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let default_lines = self.default_lines.clone();
let lines = self.lines.clone();
let window_size = window.bounds().size;
let scale = window_size.width / DEFAULT_WINDOW_WIDTH;
let dashed = self.dashed;
div()
@ -228,7 +222,7 @@ impl Render for PaintingViewer {
move |_, _, _| {},
move |_, _, window, _| {
for (path, color) in default_lines {
window.paint_path(path.clone().scale(scale), color);
window.paint_path(path, color);
}
for points in lines {
@ -304,11 +298,6 @@ fn main() {
cx.open_window(
WindowOptions {
focus: true,
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
None,
size(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT),
cx,
))),
..Default::default()
},
|window, cx| cx.new(|cx| PaintingViewer::new(window, cx)),

View file

@ -336,7 +336,10 @@ impl PathBuilder {
let v1 = buf.vertices[i1];
let v2 = buf.vertices[i2];
path.push_triangle((v0.into(), v1.into(), v2.into()));
path.push_triangle(
(v0.into(), v1.into(), v2.into()),
(point(0., 1.), point(0., 1.), point(0., 1.)),
);
}
path

View file

@ -794,6 +794,7 @@ pub(crate) struct AtlasTextureId {
pub(crate) enum AtlasTextureKind {
Monochrome = 0,
Polychrome = 1,
Path = 2,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]

View file

@ -10,6 +10,8 @@ use etagere::BucketedAtlasAllocator;
use parking_lot::Mutex;
use std::{borrow::Cow, ops, sync::Arc};
pub(crate) const PATH_TEXTURE_FORMAT: gpu::TextureFormat = gpu::TextureFormat::R16Float;
pub(crate) struct BladeAtlas(Mutex<BladeAtlasState>);
struct PendingUpload {
@ -25,6 +27,7 @@ struct BladeAtlasState {
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
initializations: Vec<AtlasTextureId>,
uploads: Vec<PendingUpload>,
path_sample_count: u32,
}
#[cfg(gles)]
@ -38,13 +41,13 @@ impl BladeAtlasState {
}
pub struct BladeTextureInfo {
#[allow(dead_code)]
pub size: gpu::Extent,
pub raw_view: gpu::TextureView,
pub msaa_view: Option<gpu::TextureView>,
}
impl BladeAtlas {
pub(crate) fn new(gpu: &Arc<gpu::Context>) -> Self {
pub(crate) fn new(gpu: &Arc<gpu::Context>, path_sample_count: u32) -> Self {
BladeAtlas(Mutex::new(BladeAtlasState {
gpu: Arc::clone(gpu),
upload_belt: BufferBelt::new(BufferBeltDescriptor {
@ -56,6 +59,7 @@ impl BladeAtlas {
tiles_by_key: Default::default(),
initializations: Vec::new(),
uploads: Vec::new(),
path_sample_count,
}))
}
@ -63,7 +67,6 @@ impl BladeAtlas {
self.0.lock().destroy();
}
#[allow(dead_code)]
pub(crate) fn clear_textures(&self, texture_kind: AtlasTextureKind) {
let mut lock = self.0.lock();
let textures = &mut lock.storage[texture_kind];
@ -72,6 +75,19 @@ impl BladeAtlas {
}
}
/// Allocate a rectangle and make it available for rendering immediately (without waiting for `before_frame`)
pub fn allocate_for_rendering(
&self,
size: Size<DevicePixels>,
texture_kind: AtlasTextureKind,
gpu_encoder: &mut gpu::CommandEncoder,
) -> AtlasTile {
let mut lock = self.0.lock();
let tile = lock.allocate(size, texture_kind);
lock.flush_initializations(gpu_encoder);
tile
}
pub fn before_frame(&self, gpu_encoder: &mut gpu::CommandEncoder) {
let mut lock = self.0.lock();
lock.flush(gpu_encoder);
@ -93,6 +109,7 @@ impl BladeAtlas {
depth: 1,
},
raw_view: texture.raw_view,
msaa_view: texture.msaa_view,
}
}
}
@ -183,8 +200,48 @@ impl BladeAtlasState {
format = gpu::TextureFormat::Bgra8UnormSrgb;
usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE;
}
AtlasTextureKind::Path => {
format = PATH_TEXTURE_FORMAT;
usage = gpu::TextureUsage::COPY
| gpu::TextureUsage::RESOURCE
| gpu::TextureUsage::TARGET;
}
}
// We currently only enable MSAA for path textures.
let (msaa, msaa_view) = if self.path_sample_count > 1 && kind == AtlasTextureKind::Path {
let msaa = self.gpu.create_texture(gpu::TextureDesc {
name: "msaa path texture",
format,
size: gpu::Extent {
width: size.width.into(),
height: size.height.into(),
depth: 1,
},
array_layer_count: 1,
mip_level_count: 1,
sample_count: self.path_sample_count,
dimension: gpu::TextureDimension::D2,
usage: gpu::TextureUsage::TARGET,
external: None,
});
(
Some(msaa),
Some(self.gpu.create_texture_view(
msaa,
gpu::TextureViewDesc {
name: "msaa texture view",
format,
dimension: gpu::ViewDimension::D2,
subresources: &Default::default(),
},
)),
)
} else {
(None, None)
};
let raw = self.gpu.create_texture(gpu::TextureDesc {
name: "atlas",
format,
@ -222,6 +279,8 @@ impl BladeAtlasState {
format,
raw,
raw_view,
msaa,
msaa_view,
live_atlas_keys: 0,
};
@ -281,6 +340,7 @@ impl BladeAtlasState {
struct BladeAtlasStorage {
monochrome_textures: AtlasTextureList<BladeAtlasTexture>,
polychrome_textures: AtlasTextureList<BladeAtlasTexture>,
path_textures: AtlasTextureList<BladeAtlasTexture>,
}
impl ops::Index<AtlasTextureKind> for BladeAtlasStorage {
@ -289,6 +349,7 @@ impl ops::Index<AtlasTextureKind> for BladeAtlasStorage {
match kind {
crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
crate::AtlasTextureKind::Path => &self.path_textures,
}
}
}
@ -298,6 +359,7 @@ impl ops::IndexMut<AtlasTextureKind> for BladeAtlasStorage {
match kind {
crate::AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
crate::AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
crate::AtlasTextureKind::Path => &mut self.path_textures,
}
}
}
@ -308,6 +370,7 @@ impl ops::Index<AtlasTextureId> for BladeAtlasStorage {
let textures = match id.kind {
crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
crate::AtlasTextureKind::Path => &self.path_textures,
};
textures[id.index as usize].as_ref().unwrap()
}
@ -321,6 +384,9 @@ impl BladeAtlasStorage {
for mut texture in self.polychrome_textures.drain().flatten() {
texture.destroy(gpu);
}
for mut texture in self.path_textures.drain().flatten() {
texture.destroy(gpu);
}
}
}
@ -329,6 +395,8 @@ struct BladeAtlasTexture {
allocator: BucketedAtlasAllocator,
raw: gpu::Texture,
raw_view: gpu::TextureView,
msaa: Option<gpu::Texture>,
msaa_view: Option<gpu::TextureView>,
format: gpu::TextureFormat,
live_atlas_keys: u32,
}
@ -356,6 +424,12 @@ impl BladeAtlasTexture {
fn destroy(&mut self, gpu: &gpu::Context) {
gpu.destroy_texture(self.raw);
gpu.destroy_texture_view(self.raw_view);
if let Some(msaa) = self.msaa {
gpu.destroy_texture(msaa);
}
if let Some(msaa_view) = self.msaa_view {
gpu.destroy_texture_view(msaa_view);
}
}
fn bytes_per_pixel(&self) -> u8 {

View file

@ -1,19 +1,24 @@
// Doing `if let` gives you nice scoping with passes/encoders
#![allow(irrefutable_let_patterns)]
use super::{BladeAtlas, BladeContext};
use super::{BladeAtlas, BladeContext, PATH_TEXTURE_FORMAT};
use crate::{
Background, Bounds, ContentMask, DevicePixels, GpuSpecs, MonochromeSprite, PathVertex,
PolychromeSprite, PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size, Underline,
AtlasTextureKind, AtlasTile, Background, Bounds, ContentMask, DevicePixels, GpuSpecs,
MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, Quad,
ScaledPixels, Scene, Shadow, Size, Underline,
};
use blade_graphics::{self as gpu};
use blade_graphics as gpu;
use blade_util::{BufferBelt, BufferBeltDescriptor};
use bytemuck::{Pod, Zeroable};
use collections::HashMap;
#[cfg(target_os = "macos")]
use media::core_video::CVMetalTextureCache;
use std::{mem, sync::Arc};
const MAX_FRAME_TIME_MS: u32 = 10000;
// Use 4x MSAA, all devices support it.
// https://developer.apple.com/documentation/metal/mtldevice/1433355-supportstexturesamplecount
const DEFAULT_PATH_SAMPLE_COUNT: u32 = 4;
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
@ -61,9 +66,16 @@ struct ShaderShadowsData {
}
#[derive(blade_macros::ShaderData)]
struct ShaderPathsData {
struct ShaderPathRasterizationData {
globals: GlobalParams,
b_path_vertices: gpu::BufferPiece,
}
#[derive(blade_macros::ShaderData)]
struct ShaderPathsData {
globals: GlobalParams,
t_sprite: gpu::TextureView,
s_sprite: gpu::Sampler,
b_path_sprites: gpu::BufferPiece,
}
@ -103,27 +115,13 @@ struct ShaderSurfacesData {
struct PathSprite {
bounds: Bounds<ScaledPixels>,
color: Background,
}
/// Argument buffer layout for `draw_indirect` commands.
#[repr(C)]
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable)]
pub struct DrawIndirectArgs {
/// The number of vertices to draw.
pub vertex_count: u32,
/// The number of instances to draw.
pub instance_count: u32,
/// The Index of the first vertex to draw.
pub first_vertex: u32,
/// The instance ID of the first instance to draw.
///
/// Has to be 0, unless [`Features::INDIRECT_FIRST_INSTANCE`](crate::Features::INDIRECT_FIRST_INSTANCE) is enabled.
pub first_instance: u32,
tile: AtlasTile,
}
struct BladePipelines {
quads: gpu::RenderPipeline,
shadows: gpu::RenderPipeline,
path_rasterization: gpu::RenderPipeline,
paths: gpu::RenderPipeline,
underlines: gpu::RenderPipeline,
mono_sprites: gpu::RenderPipeline,
@ -132,7 +130,7 @@ struct BladePipelines {
}
impl BladePipelines {
fn new(gpu: &gpu::Context, surface_info: gpu::SurfaceInfo, sample_count: u32) -> Self {
fn new(gpu: &gpu::Context, surface_info: gpu::SurfaceInfo, path_sample_count: u32) -> Self {
use gpu::ShaderData as _;
log::info!(
@ -180,10 +178,7 @@ impl BladePipelines {
depth_stencil: None,
fragment: Some(shader.at("fs_quad")),
color_targets,
multisample_state: gpu::MultisampleState {
sample_count,
..Default::default()
},
multisample_state: gpu::MultisampleState::default(),
}),
shadows: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
name: "shadows",
@ -197,8 +192,26 @@ impl BladePipelines {
depth_stencil: None,
fragment: Some(shader.at("fs_shadow")),
color_targets,
multisample_state: gpu::MultisampleState::default(),
}),
path_rasterization: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
name: "path_rasterization",
data_layouts: &[&ShaderPathRasterizationData::layout()],
vertex: shader.at("vs_path_rasterization"),
vertex_fetches: &[],
primitive: gpu::PrimitiveState {
topology: gpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
fragment: Some(shader.at("fs_path_rasterization")),
color_targets: &[gpu::ColorTargetState {
format: PATH_TEXTURE_FORMAT,
blend: Some(gpu::BlendState::ADDITIVE),
write_mask: gpu::ColorWrites::default(),
}],
multisample_state: gpu::MultisampleState {
sample_count,
sample_count: path_sample_count,
..Default::default()
},
}),
@ -208,16 +221,13 @@ impl BladePipelines {
vertex: shader.at("vs_path"),
vertex_fetches: &[],
primitive: gpu::PrimitiveState {
topology: gpu::PrimitiveTopology::TriangleList,
topology: gpu::PrimitiveTopology::TriangleStrip,
..Default::default()
},
depth_stencil: None,
fragment: Some(shader.at("fs_path")),
color_targets,
multisample_state: gpu::MultisampleState {
sample_count,
..Default::default()
},
multisample_state: gpu::MultisampleState::default(),
}),
underlines: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
name: "underlines",
@ -231,10 +241,7 @@ impl BladePipelines {
depth_stencil: None,
fragment: Some(shader.at("fs_underline")),
color_targets,
multisample_state: gpu::MultisampleState {
sample_count,
..Default::default()
},
multisample_state: gpu::MultisampleState::default(),
}),
mono_sprites: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
name: "mono-sprites",
@ -248,10 +255,7 @@ impl BladePipelines {
depth_stencil: None,
fragment: Some(shader.at("fs_mono_sprite")),
color_targets,
multisample_state: gpu::MultisampleState {
sample_count,
..Default::default()
},
multisample_state: gpu::MultisampleState::default(),
}),
poly_sprites: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
name: "poly-sprites",
@ -265,10 +269,7 @@ impl BladePipelines {
depth_stencil: None,
fragment: Some(shader.at("fs_poly_sprite")),
color_targets,
multisample_state: gpu::MultisampleState {
sample_count,
..Default::default()
},
multisample_state: gpu::MultisampleState::default(),
}),
surfaces: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
name: "surfaces",
@ -282,10 +283,7 @@ impl BladePipelines {
depth_stencil: None,
fragment: Some(shader.at("fs_surface")),
color_targets,
multisample_state: gpu::MultisampleState {
sample_count,
..Default::default()
},
multisample_state: gpu::MultisampleState::default(),
}),
}
}
@ -293,6 +291,7 @@ impl BladePipelines {
fn destroy(&mut self, gpu: &gpu::Context) {
gpu.destroy_render_pipeline(&mut self.quads);
gpu.destroy_render_pipeline(&mut self.shadows);
gpu.destroy_render_pipeline(&mut self.path_rasterization);
gpu.destroy_render_pipeline(&mut self.paths);
gpu.destroy_render_pipeline(&mut self.underlines);
gpu.destroy_render_pipeline(&mut self.mono_sprites);
@ -318,13 +317,12 @@ pub struct BladeRenderer {
last_sync_point: Option<gpu::SyncPoint>,
pipelines: BladePipelines,
instance_belt: BufferBelt,
path_tiles: HashMap<PathId, AtlasTile>,
atlas: Arc<BladeAtlas>,
atlas_sampler: gpu::Sampler,
#[cfg(target_os = "macos")]
core_video_texture_cache: CVMetalTextureCache,
sample_count: u32,
texture_msaa: Option<gpu::Texture>,
texture_view_msaa: Option<gpu::TextureView>,
path_sample_count: u32,
}
impl BladeRenderer {
@ -333,18 +331,6 @@ impl BladeRenderer {
window: &I,
config: BladeSurfaceConfig,
) -> anyhow::Result<Self> {
// workaround for https://github.com/zed-industries/zed/issues/26143
let sample_count = std::env::var("ZED_SAMPLE_COUNT")
.ok()
.or_else(|| std::env::var("ZED_PATH_SAMPLE_COUNT").ok())
.and_then(|v| v.parse().ok())
.or_else(|| {
[4, 2, 1]
.into_iter()
.find(|count| context.gpu.supports_texture_sample_count(*count))
})
.unwrap_or(1);
let surface_config = gpu::SurfaceConfig {
size: config.size,
usage: gpu::TextureUsage::TARGET,
@ -358,27 +344,22 @@ impl BladeRenderer {
.create_surface_configured(window, surface_config)
.map_err(|err| anyhow::anyhow!("Failed to create surface: {err:?}"))?;
let (texture_msaa, texture_view_msaa) = create_msaa_texture_if_needed(
&context.gpu,
surface.info().format,
config.size.width,
config.size.height,
sample_count,
)
.unzip();
let command_encoder = context.gpu.create_command_encoder(gpu::CommandEncoderDesc {
name: "main",
buffer_count: 2,
});
let pipelines = BladePipelines::new(&context.gpu, surface.info(), sample_count);
// workaround for https://github.com/zed-industries/zed/issues/26143
let path_sample_count = std::env::var("ZED_PATH_SAMPLE_COUNT")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_PATH_SAMPLE_COUNT);
let pipelines = BladePipelines::new(&context.gpu, surface.info(), path_sample_count);
let instance_belt = BufferBelt::new(BufferBeltDescriptor {
memory: gpu::Memory::Shared,
min_chunk_size: 0x1000,
alignment: 0x40, // Vulkan `minStorageBufferOffsetAlignment` on Intel Xe
});
let atlas = Arc::new(BladeAtlas::new(&context.gpu));
let atlas = Arc::new(BladeAtlas::new(&context.gpu, path_sample_count));
let atlas_sampler = context.gpu.create_sampler(gpu::SamplerDesc {
name: "atlas",
mag_filter: gpu::FilterMode::Linear,
@ -402,13 +383,12 @@ impl BladeRenderer {
last_sync_point: None,
pipelines,
instance_belt,
path_tiles: HashMap::default(),
atlas,
atlas_sampler,
#[cfg(target_os = "macos")]
core_video_texture_cache,
sample_count,
texture_msaa,
texture_view_msaa,
path_sample_count,
})
}
@ -461,24 +441,6 @@ impl BladeRenderer {
self.surface_config.size = gpu_size;
self.gpu
.reconfigure_surface(&mut self.surface, self.surface_config);
if let Some(texture_msaa) = self.texture_msaa {
self.gpu.destroy_texture(texture_msaa);
}
if let Some(texture_view_msaa) = self.texture_view_msaa {
self.gpu.destroy_texture_view(texture_view_msaa);
}
let (texture_msaa, texture_view_msaa) = create_msaa_texture_if_needed(
&self.gpu,
self.surface.info().format,
gpu_size.width,
gpu_size.height,
self.sample_count,
)
.unzip();
self.texture_msaa = texture_msaa;
self.texture_view_msaa = texture_view_msaa;
}
}
@ -489,7 +451,8 @@ impl BladeRenderer {
self.gpu
.reconfigure_surface(&mut self.surface, self.surface_config);
self.pipelines.destroy(&self.gpu);
self.pipelines = BladePipelines::new(&self.gpu, self.surface.info(), self.sample_count);
self.pipelines =
BladePipelines::new(&self.gpu, self.surface.info(), self.path_sample_count);
}
}
@ -527,6 +490,80 @@ impl BladeRenderer {
objc2::rc::Retained::as_ptr(&self.surface.metal_layer()) as *mut _
}
#[profiling::function]
fn rasterize_paths(&mut self, paths: &[Path<ScaledPixels>]) {
self.path_tiles.clear();
let mut vertices_by_texture_id = HashMap::default();
for path in paths {
let clipped_bounds = path
.bounds
.intersect(&path.content_mask.bounds)
.map_origin(|origin| origin.floor())
.map_size(|size| size.ceil());
let tile = self.atlas.allocate_for_rendering(
clipped_bounds.size.map(Into::into),
AtlasTextureKind::Path,
&mut self.command_encoder,
);
vertices_by_texture_id
.entry(tile.texture_id)
.or_insert(Vec::new())
.extend(path.vertices.iter().map(|vertex| PathVertex {
xy_position: vertex.xy_position - clipped_bounds.origin
+ tile.bounds.origin.map(Into::into),
st_position: vertex.st_position,
content_mask: ContentMask {
bounds: tile.bounds.map(Into::into),
},
}));
self.path_tiles.insert(path.id, tile);
}
for (texture_id, vertices) in vertices_by_texture_id {
let tex_info = self.atlas.get_texture_info(texture_id);
let globals = GlobalParams {
viewport_size: [tex_info.size.width as f32, tex_info.size.height as f32],
premultiplied_alpha: 0,
pad: 0,
};
let vertex_buf = unsafe { self.instance_belt.alloc_typed(&vertices, &self.gpu) };
let frame_view = tex_info.raw_view;
let color_target = if let Some(msaa_view) = tex_info.msaa_view {
gpu::RenderTarget {
view: msaa_view,
init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack),
finish_op: gpu::FinishOp::ResolveTo(frame_view),
}
} else {
gpu::RenderTarget {
view: frame_view,
init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack),
finish_op: gpu::FinishOp::Store,
}
};
if let mut pass = self.command_encoder.render(
"paths",
gpu::RenderTargetSet {
colors: &[color_target],
depth_stencil: None,
},
) {
let mut encoder = pass.with(&self.pipelines.path_rasterization);
encoder.bind(
0,
&ShaderPathRasterizationData {
globals,
b_path_vertices: vertex_buf,
},
);
encoder.draw(0, vertices.len() as u32, 0, 1);
}
}
}
pub fn destroy(&mut self) {
self.wait_for_gpu();
self.atlas.destroy();
@ -535,26 +572,17 @@ impl BladeRenderer {
self.gpu.destroy_command_encoder(&mut self.command_encoder);
self.pipelines.destroy(&self.gpu);
self.gpu.destroy_surface(&mut self.surface);
if let Some(texture_msaa) = self.texture_msaa {
self.gpu.destroy_texture(texture_msaa);
}
if let Some(texture_view_msaa) = self.texture_view_msaa {
self.gpu.destroy_texture_view(texture_view_msaa);
}
}
pub fn draw(&mut self, scene: &Scene) {
self.command_encoder.start();
self.atlas.before_frame(&mut self.command_encoder);
self.rasterize_paths(scene.paths());
let frame = {
profiling::scope!("acquire frame");
self.surface.acquire_frame()
};
let frame_view = frame.texture_view();
if let Some(texture_msaa) = self.texture_msaa {
self.command_encoder.init_texture(texture_msaa);
}
self.command_encoder.init_texture(frame.texture());
let globals = GlobalParams {
@ -569,25 +597,14 @@ impl BladeRenderer {
pad: 0,
};
let target = if let Some(texture_view_msaa) = self.texture_view_msaa {
gpu::RenderTarget {
view: texture_view_msaa,
init_op: gpu::InitOp::Clear(gpu::TextureColor::TransparentBlack),
finish_op: gpu::FinishOp::ResolveTo(frame_view),
}
} else {
gpu::RenderTarget {
view: frame_view,
init_op: gpu::InitOp::Clear(gpu::TextureColor::TransparentBlack),
finish_op: gpu::FinishOp::Store,
}
};
// draw to the target texture
if let mut pass = self.command_encoder.render(
"main",
gpu::RenderTargetSet {
colors: &[target],
colors: &[gpu::RenderTarget {
view: frame.texture_view(),
init_op: gpu::InitOp::Clear(gpu::TextureColor::TransparentBlack),
finish_op: gpu::FinishOp::Store,
}],
depth_stencil: None,
},
) {
@ -622,55 +639,32 @@ impl BladeRenderer {
}
PrimitiveBatch::Paths(paths) => {
let mut encoder = pass.with(&self.pipelines.paths);
let mut vertices = Vec::new();
let mut sprites = Vec::with_capacity(paths.len());
let mut draw_indirect_commands = Vec::with_capacity(paths.len());
let mut first_vertex = 0;
for (i, path) in paths.iter().enumerate() {
draw_indirect_commands.push(DrawIndirectArgs {
vertex_count: path.vertices.len() as u32,
instance_count: 1,
first_vertex,
first_instance: i as u32,
});
first_vertex += path.vertices.len() as u32;
vertices.extend(path.vertices.iter().map(|v| PathVertex {
xy_position: v.xy_position,
content_mask: ContentMask {
bounds: path.content_mask.bounds,
// todo(linux): group by texture ID
for path in paths {
let tile = &self.path_tiles[&path.id];
let tex_info = self.atlas.get_texture_info(tile.texture_id);
let origin = path.bounds.intersect(&path.content_mask.bounds).origin;
let sprites = [PathSprite {
bounds: Bounds {
origin: origin.map(|p| p.floor()),
size: tile.bounds.size.map(Into::into),
},
}));
sprites.push(PathSprite {
bounds: path.bounds,
color: path.color,
});
}
tile: (*tile).clone(),
}];
let b_path_vertices =
unsafe { self.instance_belt.alloc_typed(&vertices, &self.gpu) };
let instance_buf =
unsafe { self.instance_belt.alloc_typed(&sprites, &self.gpu) };
let indirect_buf = unsafe {
self.instance_belt
.alloc_typed(&draw_indirect_commands, &self.gpu)
};
encoder.bind(
0,
&ShaderPathsData {
globals,
b_path_vertices,
b_path_sprites: instance_buf,
},
);
for i in 0..paths.len() {
encoder.draw_indirect(indirect_buf.buffer.at(indirect_buf.offset
+ (i * mem::size_of::<DrawIndirectArgs>()) as u64));
let instance_buf =
unsafe { self.instance_belt.alloc_typed(&sprites, &self.gpu) };
encoder.bind(
0,
&ShaderPathsData {
globals,
t_sprite: tex_info.raw_view,
s_sprite: self.atlas_sampler,
b_path_sprites: instance_buf,
},
);
encoder.draw(0, 4, 0, sprites.len() as u32);
}
}
PrimitiveBatch::Underlines(underlines) => {
@ -823,47 +817,9 @@ impl BladeRenderer {
profiling::scope!("finish");
self.instance_belt.flush(&sync_point);
self.atlas.after_frame(&sync_point);
self.atlas.clear_textures(AtlasTextureKind::Path);
self.wait_for_gpu();
self.last_sync_point = Some(sync_point);
}
}
fn create_msaa_texture_if_needed(
gpu: &gpu::Context,
format: gpu::TextureFormat,
width: u32,
height: u32,
sample_count: u32,
) -> Option<(gpu::Texture, gpu::TextureView)> {
if sample_count <= 1 {
return None;
}
let texture_msaa = gpu.create_texture(gpu::TextureDesc {
name: "msaa",
format,
size: gpu::Extent {
width,
height,
depth: 1,
},
array_layer_count: 1,
mip_level_count: 1,
sample_count,
dimension: gpu::TextureDimension::D2,
usage: gpu::TextureUsage::TARGET,
external: None,
});
let texture_view_msaa = gpu.create_texture_view(
texture_msaa,
gpu::TextureViewDesc {
name: "msaa view",
format,
dimension: gpu::ViewDimension::D2,
subresources: &Default::default(),
},
);
Some((texture_msaa, texture_view_msaa))
}

View file

@ -922,23 +922,59 @@ fn fs_shadow(input: ShadowVarying) -> @location(0) vec4<f32> {
return blend_color(input.color, alpha);
}
// --- paths --- //
// --- path rasterization --- //
struct PathVertex {
xy_position: vec2<f32>,
st_position: vec2<f32>,
content_mask: Bounds,
}
var<storage, read> b_path_vertices: array<PathVertex>;
struct PathRasterizationVarying {
@builtin(position) position: vec4<f32>,
@location(0) st_position: vec2<f32>,
//TODO: use `clip_distance` once Naga supports it
@location(3) clip_distances: vec4<f32>,
}
@vertex
fn vs_path_rasterization(@builtin(vertex_index) vertex_id: u32) -> PathRasterizationVarying {
let v = b_path_vertices[vertex_id];
var out = PathRasterizationVarying();
out.position = to_device_position_impl(v.xy_position);
out.st_position = v.st_position;
out.clip_distances = distance_from_clip_rect_impl(v.xy_position, v.content_mask);
return out;
}
@fragment
fn fs_path_rasterization(input: PathRasterizationVarying) -> @location(0) f32 {
let dx = dpdx(input.st_position);
let dy = dpdy(input.st_position);
if (any(input.clip_distances < vec4<f32>(0.0))) {
return 0.0;
}
let gradient = 2.0 * input.st_position.xx * vec2<f32>(dx.x, dy.x) - vec2<f32>(dx.y, dy.y);
let f = input.st_position.x * input.st_position.x - input.st_position.y;
let distance = f / length(gradient);
return saturate(0.5 - distance);
}
// --- paths --- //
struct PathSprite {
bounds: Bounds,
color: Background,
tile: AtlasTile,
}
var<storage, read> b_path_vertices: array<PathVertex>;
var<storage, read> b_path_sprites: array<PathSprite>;
struct PathVarying {
@builtin(position) position: vec4<f32>,
@location(0) clip_distances: vec4<f32>,
@location(0) tile_position: vec2<f32>,
@location(1) @interpolate(flat) instance_id: u32,
@location(2) @interpolate(flat) color_solid: vec4<f32>,
@location(3) @interpolate(flat) color0: vec4<f32>,
@ -947,12 +983,13 @@ struct PathVarying {
@vertex
fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> PathVarying {
let v = b_path_vertices[vertex_id];
let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
let sprite = b_path_sprites[instance_id];
// Don't apply content mask because it was already accounted for when rasterizing the path.
var out = PathVarying();
out.position = to_device_position_impl(v.xy_position);
out.clip_distances = distance_from_clip_rect_impl(v.xy_position, v.content_mask);
out.position = to_device_position(unit_vertex, sprite.bounds);
out.tile_position = to_tile_position(unit_vertex, sprite.tile);
out.instance_id = instance_id;
let gradient = prepare_gradient_color(
@ -969,15 +1006,13 @@ fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) insta
@fragment
fn fs_path(input: PathVarying) -> @location(0) vec4<f32> {
if any(input.clip_distances < vec4<f32>(0.0)) {
return vec4<f32>(0.0);
}
let sample = textureSample(t_sprite, s_sprite, input.tile_position).r;
let mask = 1.0 - abs(1.0 - sample % 2.0);
let sprite = b_path_sprites[input.instance_id];
let background = sprite.color;
let color = gradient_color(background, input.position.xy, sprite.bounds,
input.color_solid, input.color0, input.color1);
return blend_color(color, 1.0);
return blend_color(color, mask);
}
// --- underlines --- //

View file

@ -822,11 +822,28 @@ impl crate::Keystroke {
Keysym::underscore => "_".to_owned(),
Keysym::equal => "=".to_owned(),
Keysym::plus => "+".to_owned(),
Keysym::space => "space".to_owned(),
Keysym::BackSpace => "backspace".to_owned(),
Keysym::Tab => "tab".to_owned(),
Keysym::Delete => "delete".to_owned(),
Keysym::Escape => "escape".to_owned(),
_ => {
let name = xkb::keysym_get_name(key_sym).to_lowercase();
if key_sym.is_keypad_key() {
name.replace("kp_", "")
} else if let Some(key) = key_utf8.chars().next()
&& key_utf8.len() == 1
&& key.is_ascii()
{
if key.is_ascii_graphic() {
key_utf8.to_lowercase()
// map ctrl-a to a
} else if key_utf32 <= 0x1f {
((key_utf32 as u8 + 0x60) as char).to_string()
} else {
name
}
} else if let Some(key_en) = guess_ascii(keycode, modifiers.shift) {
String::from(key_en)
} else {

View file

@ -13,12 +13,14 @@ use std::borrow::Cow;
pub(crate) struct MetalAtlas(Mutex<MetalAtlasState>);
impl MetalAtlas {
pub(crate) fn new(device: Device) -> Self {
pub(crate) fn new(device: Device, path_sample_count: u32) -> Self {
MetalAtlas(Mutex::new(MetalAtlasState {
device: AssertSend(device),
monochrome_textures: Default::default(),
polychrome_textures: Default::default(),
path_textures: Default::default(),
tiles_by_key: Default::default(),
path_sample_count,
}))
}
@ -26,7 +28,10 @@ impl MetalAtlas {
self.0.lock().texture(id).metal_texture.clone()
}
#[allow(dead_code)]
pub(crate) fn msaa_texture(&self, id: AtlasTextureId) -> Option<metal::Texture> {
self.0.lock().texture(id).msaa_texture.clone()
}
pub(crate) fn allocate(
&self,
size: Size<DevicePixels>,
@ -35,12 +40,12 @@ impl MetalAtlas {
self.0.lock().allocate(size, texture_kind)
}
#[allow(dead_code)]
pub(crate) fn clear_textures(&self, texture_kind: AtlasTextureKind) {
let mut lock = self.0.lock();
let textures = match texture_kind {
AtlasTextureKind::Monochrome => &mut lock.monochrome_textures,
AtlasTextureKind::Polychrome => &mut lock.polychrome_textures,
AtlasTextureKind::Path => &mut lock.path_textures,
};
for texture in textures.iter_mut() {
texture.clear();
@ -52,7 +57,9 @@ struct MetalAtlasState {
device: AssertSend<Device>,
monochrome_textures: AtlasTextureList<MetalAtlasTexture>,
polychrome_textures: AtlasTextureList<MetalAtlasTexture>,
path_textures: AtlasTextureList<MetalAtlasTexture>,
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
path_sample_count: u32,
}
impl PlatformAtlas for MetalAtlas {
@ -87,6 +94,7 @@ impl PlatformAtlas for MetalAtlas {
let textures = match id.kind {
AtlasTextureKind::Monochrome => &mut lock.monochrome_textures,
AtlasTextureKind::Polychrome => &mut lock.polychrome_textures,
AtlasTextureKind::Path => &mut lock.polychrome_textures,
};
let Some(texture_slot) = textures
@ -120,6 +128,7 @@ impl MetalAtlasState {
let textures = match texture_kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
AtlasTextureKind::Path => &mut self.path_textures,
};
if let Some(tile) = textures
@ -164,14 +173,31 @@ impl MetalAtlasState {
pixel_format = metal::MTLPixelFormat::BGRA8Unorm;
usage = metal::MTLTextureUsage::ShaderRead;
}
AtlasTextureKind::Path => {
pixel_format = metal::MTLPixelFormat::R16Float;
usage = metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead;
}
}
texture_descriptor.set_pixel_format(pixel_format);
texture_descriptor.set_usage(usage);
let metal_texture = self.device.new_texture(&texture_descriptor);
// We currently only enable MSAA for path textures.
let msaa_texture = if self.path_sample_count > 1 && kind == AtlasTextureKind::Path {
let mut descriptor = texture_descriptor.clone();
descriptor.set_texture_type(metal::MTLTextureType::D2Multisample);
descriptor.set_storage_mode(metal::MTLStorageMode::Private);
descriptor.set_sample_count(self.path_sample_count as _);
let msaa_texture = self.device.new_texture(&descriptor);
Some(msaa_texture)
} else {
None
};
let texture_list = match kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
AtlasTextureKind::Path => &mut self.path_textures,
};
let index = texture_list.free_list.pop();
@ -183,6 +209,7 @@ impl MetalAtlasState {
},
allocator: etagere::BucketedAtlasAllocator::new(size.into()),
metal_texture: AssertSend(metal_texture),
msaa_texture: AssertSend(msaa_texture),
live_atlas_keys: 0,
};
@ -199,6 +226,7 @@ impl MetalAtlasState {
let textures = match id.kind {
crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
crate::AtlasTextureKind::Path => &self.path_textures,
};
textures[id.index as usize].as_ref().unwrap()
}
@ -208,6 +236,7 @@ struct MetalAtlasTexture {
id: AtlasTextureId,
allocator: BucketedAtlasAllocator,
metal_texture: AssertSend<metal::Texture>,
msaa_texture: AssertSend<Option<metal::Texture>>,
live_atlas_keys: u32,
}

View file

@ -1,28 +1,27 @@
use super::metal_atlas::MetalAtlas;
use crate::{
AtlasTextureId, Background, Bounds, ContentMask, DevicePixels, MonochromeSprite, PaintSurface,
Path, PathVertex, PolychromeSprite, PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size,
Surface, Underline, point, size,
AtlasTextureId, AtlasTextureKind, AtlasTile, Background, Bounds, ContentMask, DevicePixels,
MonochromeSprite, PaintSurface, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch,
Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline, point, size,
};
use anyhow::Result;
use anyhow::{Context as _, Result};
use block::ConcreteBlock;
use cocoa::{
base::{NO, YES},
foundation::{NSSize, NSUInteger},
quartzcore::AutoresizingMask,
};
use collections::HashMap;
use core_foundation::base::TCFType;
use core_video::{
metal_texture::CVMetalTextureGetTexture, metal_texture_cache::CVMetalTextureCache,
pixel_buffer::kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
};
use foreign_types::{ForeignType, ForeignTypeRef};
use metal::{
CAMetalLayer, CommandQueue, MTLDrawPrimitivesIndirectArguments, MTLPixelFormat,
MTLResourceOptions, NSRange,
};
use metal::{CAMetalLayer, CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange};
use objc::{self, msg_send, sel, sel_impl};
use parking_lot::Mutex;
use smallvec::SmallVec;
use std::{cell::Cell, ffi::c_void, mem, ptr, sync::Arc};
// Exported to metal
@ -32,6 +31,9 @@ pub(crate) type PointF = crate::Point<f32>;
const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib"));
#[cfg(feature = "runtime_shaders")]
const SHADERS_SOURCE_FILE: &str = include_str!(concat!(env!("OUT_DIR"), "/stitched_shaders.metal"));
// Use 4x MSAA, all devices support it.
// https://developer.apple.com/documentation/metal/mtldevice/1433355-supportstexturesamplecount
const PATH_SAMPLE_COUNT: u32 = 4;
pub type Context = Arc<Mutex<InstanceBufferPool>>;
pub type Renderer = MetalRenderer;
@ -96,7 +98,8 @@ pub(crate) struct MetalRenderer {
layer: metal::MetalLayer,
presents_with_transaction: bool,
command_queue: CommandQueue,
path_pipeline_state: metal::RenderPipelineState,
paths_rasterization_pipeline_state: metal::RenderPipelineState,
path_sprites_pipeline_state: metal::RenderPipelineState,
shadows_pipeline_state: metal::RenderPipelineState,
quads_pipeline_state: metal::RenderPipelineState,
underlines_pipeline_state: metal::RenderPipelineState,
@ -108,8 +111,6 @@ pub(crate) struct MetalRenderer {
instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>,
sprite_atlas: Arc<MetalAtlas>,
core_video_texture_cache: core_video::metal_texture_cache::CVMetalTextureCache,
sample_count: u64,
msaa_texture: Option<metal::Texture>,
}
impl MetalRenderer {
@ -168,19 +169,22 @@ impl MetalRenderer {
MTLResourceOptions::StorageModeManaged,
);
let sample_count = [4, 2, 1]
.into_iter()
.find(|count| device.supports_texture_sample_count(*count))
.unwrap_or(1);
let path_pipeline_state = build_pipeline_state(
let paths_rasterization_pipeline_state = build_path_rasterization_pipeline_state(
&device,
&library,
"paths",
"path_vertex",
"path_fragment",
"paths_rasterization",
"path_rasterization_vertex",
"path_rasterization_fragment",
MTLPixelFormat::R16Float,
PATH_SAMPLE_COUNT,
);
let path_sprites_pipeline_state = build_pipeline_state(
&device,
&library,
"path_sprites",
"path_sprite_vertex",
"path_sprite_fragment",
MTLPixelFormat::BGRA8Unorm,
sample_count,
);
let shadows_pipeline_state = build_pipeline_state(
&device,
@ -189,7 +193,6 @@ impl MetalRenderer {
"shadow_vertex",
"shadow_fragment",
MTLPixelFormat::BGRA8Unorm,
sample_count,
);
let quads_pipeline_state = build_pipeline_state(
&device,
@ -198,7 +201,6 @@ impl MetalRenderer {
"quad_vertex",
"quad_fragment",
MTLPixelFormat::BGRA8Unorm,
sample_count,
);
let underlines_pipeline_state = build_pipeline_state(
&device,
@ -207,7 +209,6 @@ impl MetalRenderer {
"underline_vertex",
"underline_fragment",
MTLPixelFormat::BGRA8Unorm,
sample_count,
);
let monochrome_sprites_pipeline_state = build_pipeline_state(
&device,
@ -216,7 +217,6 @@ impl MetalRenderer {
"monochrome_sprite_vertex",
"monochrome_sprite_fragment",
MTLPixelFormat::BGRA8Unorm,
sample_count,
);
let polychrome_sprites_pipeline_state = build_pipeline_state(
&device,
@ -225,7 +225,6 @@ impl MetalRenderer {
"polychrome_sprite_vertex",
"polychrome_sprite_fragment",
MTLPixelFormat::BGRA8Unorm,
sample_count,
);
let surfaces_pipeline_state = build_pipeline_state(
&device,
@ -234,21 +233,20 @@ impl MetalRenderer {
"surface_vertex",
"surface_fragment",
MTLPixelFormat::BGRA8Unorm,
sample_count,
);
let command_queue = device.new_command_queue();
let sprite_atlas = Arc::new(MetalAtlas::new(device.clone()));
let sprite_atlas = Arc::new(MetalAtlas::new(device.clone(), PATH_SAMPLE_COUNT));
let core_video_texture_cache =
CVMetalTextureCache::new(None, device.clone(), None).unwrap();
let msaa_texture = create_msaa_texture(&device, &layer, sample_count);
Self {
device,
layer,
presents_with_transaction: false,
command_queue,
path_pipeline_state,
paths_rasterization_pipeline_state,
path_sprites_pipeline_state,
shadows_pipeline_state,
quads_pipeline_state,
underlines_pipeline_state,
@ -259,8 +257,6 @@ impl MetalRenderer {
instance_buffer_pool,
sprite_atlas,
core_video_texture_cache,
sample_count,
msaa_texture,
}
}
@ -293,8 +289,6 @@ impl MetalRenderer {
setDrawableSize: size
];
}
self.msaa_texture = create_msaa_texture(&self.device, &self.layer, self.sample_count);
}
pub fn update_transparency(&self, _transparent: bool) {
@ -381,23 +375,25 @@ impl MetalRenderer {
let command_queue = self.command_queue.clone();
let command_buffer = command_queue.new_command_buffer();
let mut instance_offset = 0;
let path_tiles = self
.rasterize_paths(
scene.paths(),
instance_buffer,
&mut instance_offset,
command_buffer,
)
.with_context(|| format!("rasterizing {} paths", scene.paths().len()))?;
let render_pass_descriptor = metal::RenderPassDescriptor::new();
let color_attachment = render_pass_descriptor
.color_attachments()
.object_at(0)
.unwrap();
if let Some(msaa_texture_ref) = self.msaa_texture.as_deref() {
color_attachment.set_texture(Some(msaa_texture_ref));
color_attachment.set_load_action(metal::MTLLoadAction::Clear);
color_attachment.set_store_action(metal::MTLStoreAction::MultisampleResolve);
color_attachment.set_resolve_texture(Some(drawable.texture()));
} else {
color_attachment.set_load_action(metal::MTLLoadAction::Clear);
color_attachment.set_texture(Some(drawable.texture()));
color_attachment.set_store_action(metal::MTLStoreAction::Store);
}
color_attachment.set_texture(Some(drawable.texture()));
color_attachment.set_load_action(metal::MTLLoadAction::Clear);
color_attachment.set_store_action(metal::MTLStoreAction::Store);
let alpha = if self.layer.is_opaque() { 1. } else { 0. };
color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., alpha));
let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor);
@ -429,6 +425,7 @@ impl MetalRenderer {
),
PrimitiveBatch::Paths(paths) => self.draw_paths(
paths,
&path_tiles,
instance_buffer,
&mut instance_offset,
viewport_size,
@ -496,6 +493,106 @@ impl MetalRenderer {
Ok(command_buffer.to_owned())
}
fn rasterize_paths(
&self,
paths: &[Path<ScaledPixels>],
instance_buffer: &mut InstanceBuffer,
instance_offset: &mut usize,
command_buffer: &metal::CommandBufferRef,
) -> Option<HashMap<PathId, AtlasTile>> {
self.sprite_atlas.clear_textures(AtlasTextureKind::Path);
let mut tiles = HashMap::default();
let mut vertices_by_texture_id = HashMap::default();
for path in paths {
let clipped_bounds = path.bounds.intersect(&path.content_mask.bounds);
let tile = self
.sprite_atlas
.allocate(clipped_bounds.size.map(Into::into), AtlasTextureKind::Path)?;
vertices_by_texture_id
.entry(tile.texture_id)
.or_insert(Vec::new())
.extend(path.vertices.iter().map(|vertex| PathVertex {
xy_position: vertex.xy_position - clipped_bounds.origin
+ tile.bounds.origin.map(Into::into),
st_position: vertex.st_position,
content_mask: ContentMask {
bounds: tile.bounds.map(Into::into),
},
}));
tiles.insert(path.id, tile);
}
for (texture_id, vertices) in vertices_by_texture_id {
align_offset(instance_offset);
let vertices_bytes_len = mem::size_of_val(vertices.as_slice());
let next_offset = *instance_offset + vertices_bytes_len;
if next_offset > instance_buffer.size {
return None;
}
let render_pass_descriptor = metal::RenderPassDescriptor::new();
let color_attachment = render_pass_descriptor
.color_attachments()
.object_at(0)
.unwrap();
let texture = self.sprite_atlas.metal_texture(texture_id);
let msaa_texture = self.sprite_atlas.msaa_texture(texture_id);
if let Some(msaa_texture) = msaa_texture {
color_attachment.set_texture(Some(&msaa_texture));
color_attachment.set_resolve_texture(Some(&texture));
color_attachment.set_load_action(metal::MTLLoadAction::Clear);
color_attachment.set_store_action(metal::MTLStoreAction::MultisampleResolve);
} else {
color_attachment.set_texture(Some(&texture));
color_attachment.set_load_action(metal::MTLLoadAction::Clear);
color_attachment.set_store_action(metal::MTLStoreAction::Store);
}
color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 1.));
let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor);
command_encoder.set_render_pipeline_state(&self.paths_rasterization_pipeline_state);
command_encoder.set_vertex_buffer(
PathRasterizationInputIndex::Vertices as u64,
Some(&instance_buffer.metal_buffer),
*instance_offset as u64,
);
let texture_size = Size {
width: DevicePixels::from(texture.width()),
height: DevicePixels::from(texture.height()),
};
command_encoder.set_vertex_bytes(
PathRasterizationInputIndex::AtlasTextureSize as u64,
mem::size_of_val(&texture_size) as u64,
&texture_size as *const Size<DevicePixels> as *const _,
);
let buffer_contents = unsafe {
(instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset)
};
unsafe {
ptr::copy_nonoverlapping(
vertices.as_ptr() as *const u8,
buffer_contents,
vertices_bytes_len,
);
}
command_encoder.draw_primitives(
metal::MTLPrimitiveType::Triangle,
0,
vertices.len() as u64,
);
command_encoder.end_encoding();
*instance_offset = next_offset;
}
Some(tiles)
}
fn draw_shadows(
&self,
shadows: &[Shadow],
@ -621,6 +718,7 @@ impl MetalRenderer {
fn draw_paths(
&self,
paths: &[Path<ScaledPixels>],
tiles_by_path_id: &HashMap<PathId, AtlasTile>,
instance_buffer: &mut InstanceBuffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
@ -630,108 +728,100 @@ impl MetalRenderer {
return true;
}
command_encoder.set_render_pipeline_state(&self.path_pipeline_state);
command_encoder.set_render_pipeline_state(&self.path_sprites_pipeline_state);
command_encoder.set_vertex_buffer(
SpriteInputIndex::Vertices as u64,
Some(&self.unit_vertices),
0,
);
command_encoder.set_vertex_bytes(
SpriteInputIndex::ViewportSize as u64,
mem::size_of_val(&viewport_size) as u64,
&viewport_size as *const Size<DevicePixels> as *const _,
);
unsafe {
let base_addr = instance_buffer.metal_buffer.contents();
let mut p = (base_addr as *mut u8).add(*instance_offset);
let mut draw_indirect_commands = Vec::with_capacity(paths.len());
let mut prev_texture_id = None;
let mut sprites = SmallVec::<[_; 1]>::new();
let mut paths_and_tiles = paths
.iter()
.map(|path| (path, tiles_by_path_id.get(&path.id).unwrap()))
.peekable();
// copy vertices
let vertices_offset = (p as usize) - (base_addr as usize);
let mut first_vertex = 0;
for (i, path) in paths.iter().enumerate() {
if (p as usize) - (base_addr as usize)
+ (mem::size_of::<PathVertex<ScaledPixels>>() * path.vertices.len())
> instance_buffer.size
{
loop {
if let Some((path, tile)) = paths_and_tiles.peek() {
if prev_texture_id.map_or(true, |texture_id| texture_id == tile.texture_id) {
prev_texture_id = Some(tile.texture_id);
let origin = path.bounds.intersect(&path.content_mask.bounds).origin;
sprites.push(PathSprite {
bounds: Bounds {
origin: origin.map(|p| p.floor()),
size: tile.bounds.size.map(Into::into),
},
color: path.color,
tile: (*tile).clone(),
});
paths_and_tiles.next();
continue;
}
}
if sprites.is_empty() {
break;
} else {
align_offset(instance_offset);
let texture_id = prev_texture_id.take().unwrap();
let texture: metal::Texture = self.sprite_atlas.metal_texture(texture_id);
let texture_size = size(
DevicePixels(texture.width() as i32),
DevicePixels(texture.height() as i32),
);
command_encoder.set_vertex_buffer(
SpriteInputIndex::Sprites as u64,
Some(&instance_buffer.metal_buffer),
*instance_offset as u64,
);
command_encoder.set_vertex_bytes(
SpriteInputIndex::AtlasTextureSize as u64,
mem::size_of_val(&texture_size) as u64,
&texture_size as *const Size<DevicePixels> as *const _,
);
command_encoder.set_fragment_buffer(
SpriteInputIndex::Sprites as u64,
Some(&instance_buffer.metal_buffer),
*instance_offset as u64,
);
command_encoder
.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
let sprite_bytes_len = mem::size_of_val(sprites.as_slice());
let next_offset = *instance_offset + sprite_bytes_len;
if next_offset > instance_buffer.size {
return false;
}
for v in &path.vertices {
*(p as *mut PathVertex<ScaledPixels>) = PathVertex {
xy_position: v.xy_position,
content_mask: ContentMask {
bounds: path.content_mask.bounds,
},
};
p = p.add(mem::size_of::<PathVertex<ScaledPixels>>());
let buffer_contents = unsafe {
(instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset)
};
unsafe {
ptr::copy_nonoverlapping(
sprites.as_ptr() as *const u8,
buffer_contents,
sprite_bytes_len,
);
}
draw_indirect_commands.push(MTLDrawPrimitivesIndirectArguments {
vertexCount: path.vertices.len() as u32,
instanceCount: 1,
vertexStart: first_vertex,
baseInstance: i as u32,
});
first_vertex += path.vertices.len() as u32;
}
// copy sprites
let sprites_offset = (p as u64) - (base_addr as u64);
if (p as usize) - (base_addr as usize) + (mem::size_of::<PathSprite>() * paths.len())
> instance_buffer.size
{
return false;
}
for path in paths {
*(p as *mut PathSprite) = PathSprite {
bounds: path.bounds,
color: path.color,
};
p = p.add(mem::size_of::<PathSprite>());
}
// copy indirect commands
let icb_bytes_len = mem::size_of_val(draw_indirect_commands.as_slice());
let icb_offset = (p as u64) - (base_addr as u64);
if (p as usize) - (base_addr as usize) + icb_bytes_len > instance_buffer.size {
return false;
}
ptr::copy_nonoverlapping(
draw_indirect_commands.as_ptr() as *const u8,
p,
icb_bytes_len,
);
p = p.add(icb_bytes_len);
// draw path
command_encoder.set_vertex_buffer(
PathInputIndex::Vertices as u64,
Some(&instance_buffer.metal_buffer),
vertices_offset as u64,
);
command_encoder.set_vertex_bytes(
PathInputIndex::ViewportSize as u64,
mem::size_of_val(&viewport_size) as u64,
&viewport_size as *const Size<DevicePixels> as *const _,
);
command_encoder.set_vertex_buffer(
PathInputIndex::Sprites as u64,
Some(&instance_buffer.metal_buffer),
sprites_offset,
);
command_encoder.set_fragment_buffer(
PathInputIndex::Sprites as u64,
Some(&instance_buffer.metal_buffer),
sprites_offset,
);
for i in 0..paths.len() {
command_encoder.draw_primitives_indirect(
command_encoder.draw_primitives_instanced(
metal::MTLPrimitiveType::Triangle,
&instance_buffer.metal_buffer,
icb_offset
+ (i * std::mem::size_of::<MTLDrawPrimitivesIndirectArguments>()) as u64,
0,
6,
sprites.len() as u64,
);
*instance_offset = next_offset;
sprites.clear();
}
*instance_offset = (p as usize) - (base_addr as usize);
}
true
}
@ -1053,7 +1143,6 @@ fn build_pipeline_state(
vertex_fn_name: &str,
fragment_fn_name: &str,
pixel_format: metal::MTLPixelFormat,
sample_count: u64,
) -> metal::RenderPipelineState {
let vertex_fn = library
.get_function(vertex_fn_name, None)
@ -1066,7 +1155,6 @@ fn build_pipeline_state(
descriptor.set_label(label);
descriptor.set_vertex_function(Some(vertex_fn.as_ref()));
descriptor.set_fragment_function(Some(fragment_fn.as_ref()));
descriptor.set_sample_count(sample_count);
let color_attachment = descriptor.color_attachments().object_at(0).unwrap();
color_attachment.set_pixel_format(pixel_format);
color_attachment.set_blending_enabled(true);
@ -1082,45 +1170,50 @@ fn build_pipeline_state(
.expect("could not create render pipeline state")
}
fn build_path_rasterization_pipeline_state(
device: &metal::DeviceRef,
library: &metal::LibraryRef,
label: &str,
vertex_fn_name: &str,
fragment_fn_name: &str,
pixel_format: metal::MTLPixelFormat,
path_sample_count: u32,
) -> metal::RenderPipelineState {
let vertex_fn = library
.get_function(vertex_fn_name, None)
.expect("error locating vertex function");
let fragment_fn = library
.get_function(fragment_fn_name, None)
.expect("error locating fragment function");
let descriptor = metal::RenderPipelineDescriptor::new();
descriptor.set_label(label);
descriptor.set_vertex_function(Some(vertex_fn.as_ref()));
descriptor.set_fragment_function(Some(fragment_fn.as_ref()));
if path_sample_count > 1 {
descriptor.set_raster_sample_count(path_sample_count as _);
descriptor.set_alpha_to_coverage_enabled(true);
}
let color_attachment = descriptor.color_attachments().object_at(0).unwrap();
color_attachment.set_pixel_format(pixel_format);
color_attachment.set_blending_enabled(true);
color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add);
color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add);
color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::One);
color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One);
color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::One);
color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::One);
device
.new_render_pipeline_state(&descriptor)
.expect("could not create render pipeline state")
}
// Align to multiples of 256 make Metal happy.
fn align_offset(offset: &mut usize) {
*offset = (*offset).div_ceil(256) * 256;
}
fn create_msaa_texture(
device: &metal::Device,
layer: &metal::MetalLayer,
sample_count: u64,
) -> Option<metal::Texture> {
let viewport_size = layer.drawable_size();
let width = viewport_size.width.ceil() as u64;
let height = viewport_size.height.ceil() as u64;
if width == 0 || height == 0 {
return None;
}
if sample_count <= 1 {
return None;
}
let texture_descriptor = metal::TextureDescriptor::new();
texture_descriptor.set_texture_type(metal::MTLTextureType::D2Multisample);
// MTLStorageMode default is `shared` only for Apple silicon GPUs. Use `private` for Apple and Intel GPUs both.
// Reference: https://developer.apple.com/documentation/metal/choosing-a-resource-storage-mode-for-apple-gpus
texture_descriptor.set_storage_mode(metal::MTLStorageMode::Private);
texture_descriptor.set_width(width);
texture_descriptor.set_height(height);
texture_descriptor.set_pixel_format(layer.pixel_format());
texture_descriptor.set_usage(metal::MTLTextureUsage::RenderTarget);
texture_descriptor.set_sample_count(sample_count);
let metal_texture = device.new_texture(&texture_descriptor);
Some(metal_texture)
}
#[repr(C)]
enum ShadowInputIndex {
Vertices = 0,
@ -1162,10 +1255,9 @@ enum SurfaceInputIndex {
}
#[repr(C)]
enum PathInputIndex {
enum PathRasterizationInputIndex {
Vertices = 0,
ViewportSize = 1,
Sprites = 2,
AtlasTextureSize = 1,
}
#[derive(Clone, Debug, Eq, PartialEq)]
@ -1173,6 +1265,7 @@ enum PathInputIndex {
pub struct PathSprite {
pub bounds: Bounds<ScaledPixels>,
pub color: Background,
pub tile: AtlasTile,
}
#[derive(Clone, Debug, Eq, PartialEq)]

View file

@ -698,27 +698,76 @@ fragment float4 polychrome_sprite_fragment(
return color;
}
struct PathVertexOutput {
struct PathRasterizationVertexOutput {
float4 position [[position]];
float2 st_position;
float clip_rect_distance [[clip_distance]][4];
};
struct PathRasterizationFragmentInput {
float4 position [[position]];
float2 st_position;
};
vertex PathRasterizationVertexOutput path_rasterization_vertex(
uint vertex_id [[vertex_id]],
constant PathVertex_ScaledPixels *vertices
[[buffer(PathRasterizationInputIndex_Vertices)]],
constant Size_DevicePixels *atlas_size
[[buffer(PathRasterizationInputIndex_AtlasTextureSize)]]) {
PathVertex_ScaledPixels v = vertices[vertex_id];
float2 vertex_position = float2(v.xy_position.x, v.xy_position.y);
float2 viewport_size = float2(atlas_size->width, atlas_size->height);
return PathRasterizationVertexOutput{
float4(vertex_position / viewport_size * float2(2., -2.) +
float2(-1., 1.),
0., 1.),
float2(v.st_position.x, v.st_position.y),
{v.xy_position.x - v.content_mask.bounds.origin.x,
v.content_mask.bounds.origin.x + v.content_mask.bounds.size.width -
v.xy_position.x,
v.xy_position.y - v.content_mask.bounds.origin.y,
v.content_mask.bounds.origin.y + v.content_mask.bounds.size.height -
v.xy_position.y}};
}
fragment float4 path_rasterization_fragment(PathRasterizationFragmentInput input
[[stage_in]]) {
float2 dx = dfdx(input.st_position);
float2 dy = dfdy(input.st_position);
float2 gradient = float2((2. * input.st_position.x) * dx.x - dx.y,
(2. * input.st_position.x) * dy.x - dy.y);
float f = (input.st_position.x * input.st_position.x) - input.st_position.y;
float distance = f / length(gradient);
float alpha = saturate(0.5 - distance);
return float4(alpha, 0., 0., 1.);
}
struct PathSpriteVertexOutput {
float4 position [[position]];
float2 tile_position;
uint sprite_id [[flat]];
float4 solid_color [[flat]];
float4 color0 [[flat]];
float4 color1 [[flat]];
float4 clip_distance;
};
vertex PathVertexOutput path_vertex(
uint vertex_id [[vertex_id]],
constant PathVertex_ScaledPixels *vertices [[buffer(PathInputIndex_Vertices)]],
uint sprite_id [[instance_id]],
constant PathSprite *sprites [[buffer(PathInputIndex_Sprites)]],
constant Size_DevicePixels *input_viewport_size [[buffer(PathInputIndex_ViewportSize)]]) {
PathVertex_ScaledPixels v = vertices[vertex_id];
float2 vertex_position = float2(v.xy_position.x, v.xy_position.y);
float2 viewport_size = float2((float)input_viewport_size->width,
(float)input_viewport_size->height);
vertex PathSpriteVertexOutput path_sprite_vertex(
uint unit_vertex_id [[vertex_id]], uint sprite_id [[instance_id]],
constant float2 *unit_vertices [[buffer(SpriteInputIndex_Vertices)]],
constant PathSprite *sprites [[buffer(SpriteInputIndex_Sprites)]],
constant Size_DevicePixels *viewport_size
[[buffer(SpriteInputIndex_ViewportSize)]],
constant Size_DevicePixels *atlas_size
[[buffer(SpriteInputIndex_AtlasTextureSize)]]) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
PathSprite sprite = sprites[sprite_id];
float4 device_position = float4(vertex_position / viewport_size * float2(2., -2.) + float2(-1., 1.), 0., 1.);
// Don't apply content mask because it was already accounted for when
// rasterizing the path.
float4 device_position =
to_device_position(unit_vertex, sprite.bounds, viewport_size);
float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size);
GradientColor gradient = prepare_fill_color(
sprite.color.tag,
@ -728,32 +777,30 @@ vertex PathVertexOutput path_vertex(
sprite.color.colors[1].color
);
return PathVertexOutput{
return PathSpriteVertexOutput{
device_position,
tile_position,
sprite_id,
gradient.solid,
gradient.color0,
gradient.color1,
{v.xy_position.x - v.content_mask.bounds.origin.x,
v.content_mask.bounds.origin.x + v.content_mask.bounds.size.width -
v.xy_position.x,
v.xy_position.y - v.content_mask.bounds.origin.y,
v.content_mask.bounds.origin.y + v.content_mask.bounds.size.height -
v.xy_position.y}
gradient.color1
};
}
fragment float4 path_fragment(
PathVertexOutput input [[stage_in]],
constant PathSprite *sprites [[buffer(PathInputIndex_Sprites)]]) {
if (any(input.clip_distance < float4(0.0))) {
return float4(0.0);
}
fragment float4 path_sprite_fragment(
PathSpriteVertexOutput input [[stage_in]],
constant PathSprite *sprites [[buffer(SpriteInputIndex_Sprites)]],
texture2d<float> atlas_texture [[texture(SpriteInputIndex_AtlasTexture)]]) {
constexpr sampler atlas_texture_sampler(mag_filter::linear,
min_filter::linear);
float4 sample =
atlas_texture.sample(atlas_texture_sampler, input.tile_position);
float mask = 1. - abs(1. - fmod(sample.r, 2.));
PathSprite sprite = sprites[input.sprite_id];
Background background = sprite.color;
float4 color = fill_color(background, input.position.xy, sprite.bounds,
input.solid_color, input.color0, input.color1);
color.a *= mask;
return color;
}

View file

@ -341,7 +341,7 @@ impl PlatformAtlas for TestAtlas {
crate::AtlasTile {
texture_id: AtlasTextureId {
index: texture_id,
kind: crate::AtlasTextureKind::Polychrome,
kind: crate::AtlasTextureKind::Path,
},
tile_id: TileId(tile_id),
padding: 0,

View file

@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
use crate::{
AtlasTextureId, AtlasTile, Background, Bounds, ContentMask, Corners, Edges, Hsla, Pixels,
Point, Radians, ScaledPixels, Size, bounds_tree::BoundsTree,
Point, Radians, ScaledPixels, Size, bounds_tree::BoundsTree, point,
};
use std::{fmt::Debug, iter::Peekable, ops::Range, slice};
@ -43,7 +43,13 @@ impl Scene {
self.surfaces.clear();
}
#[allow(dead_code)]
#[cfg_attr(
all(
any(target_os = "linux", target_os = "freebsd"),
not(any(feature = "x11", feature = "wayland"))
),
allow(dead_code)
)]
pub fn paths(&self) -> &[Path<ScaledPixels>] {
&self.paths
}
@ -683,7 +689,6 @@ pub struct Path<P: Clone + Debug + Default + PartialEq> {
start: Point<P>,
current: Point<P>,
contour_count: usize,
base_scale: f32,
}
impl Path<Pixels> {
@ -702,35 +707,25 @@ impl Path<Pixels> {
content_mask: Default::default(),
color: Default::default(),
contour_count: 0,
base_scale: 1.0,
}
}
/// Set the base scale of the path.
pub fn scale(mut self, factor: f32) -> Self {
self.base_scale = factor;
self
}
/// Apply a scale to the path.
pub(crate) fn apply_scale(&self, factor: f32) -> Path<ScaledPixels> {
/// Scale this path by the given factor.
pub fn scale(&self, factor: f32) -> Path<ScaledPixels> {
Path {
id: self.id,
order: self.order,
bounds: self.bounds.scale(self.base_scale * factor),
content_mask: self.content_mask.scale(self.base_scale * factor),
bounds: self.bounds.scale(factor),
content_mask: self.content_mask.scale(factor),
vertices: self
.vertices
.iter()
.map(|vertex| vertex.scale(self.base_scale * factor))
.map(|vertex| vertex.scale(factor))
.collect(),
start: self
.start
.map(|start| start.scale(self.base_scale * factor)),
current: self.current.scale(self.base_scale * factor),
start: self.start.map(|start| start.scale(factor)),
current: self.current.scale(factor),
contour_count: self.contour_count,
color: self.color,
base_scale: 1.0,
}
}
@ -745,7 +740,10 @@ impl Path<Pixels> {
pub fn line_to(&mut self, to: Point<Pixels>) {
self.contour_count += 1;
if self.contour_count > 1 {
self.push_triangle((self.start, self.current, to));
self.push_triangle(
(self.start, self.current, to),
(point(0., 1.), point(0., 1.), point(0., 1.)),
);
}
self.current = to;
}
@ -754,15 +752,25 @@ impl Path<Pixels> {
pub fn curve_to(&mut self, to: Point<Pixels>, ctrl: Point<Pixels>) {
self.contour_count += 1;
if self.contour_count > 1 {
self.push_triangle((self.start, self.current, to));
self.push_triangle(
(self.start, self.current, to),
(point(0., 1.), point(0., 1.), point(0., 1.)),
);
}
self.push_triangle((self.current, ctrl, to));
self.push_triangle(
(self.current, ctrl, to),
(point(0., 0.), point(0.5, 0.), point(1., 1.)),
);
self.current = to;
}
/// Push a triangle to the Path.
pub fn push_triangle(&mut self, xy: (Point<Pixels>, Point<Pixels>, Point<Pixels>)) {
pub fn push_triangle(
&mut self,
xy: (Point<Pixels>, Point<Pixels>, Point<Pixels>),
st: (Point<f32>, Point<f32>, Point<f32>),
) {
self.bounds = self
.bounds
.union(&Bounds {
@ -780,14 +788,17 @@ impl Path<Pixels> {
self.vertices.push(PathVertex {
xy_position: xy.0,
st_position: st.0,
content_mask: Default::default(),
});
self.vertices.push(PathVertex {
xy_position: xy.1,
st_position: st.1,
content_mask: Default::default(),
});
self.vertices.push(PathVertex {
xy_position: xy.2,
st_position: st.2,
content_mask: Default::default(),
});
}
@ -803,6 +814,7 @@ impl From<Path<ScaledPixels>> for Primitive {
#[repr(C)]
pub(crate) struct PathVertex<P: Clone + Debug + Default + PartialEq> {
pub(crate) xy_position: Point<P>,
pub(crate) st_position: Point<f32>,
pub(crate) content_mask: ContentMask<P>,
}
@ -810,6 +822,7 @@ impl PathVertex<Pixels> {
pub fn scale(&self, factor: f32) -> PathVertex<ScaledPixels> {
PathVertex {
xy_position: self.xy_position.scale(factor),
st_position: self.st_position,
content_mask: self.content_mask.scale(factor),
}
}

View file

@ -2658,7 +2658,7 @@ impl Window {
path.color = color.opacity(opacity);
self.next_frame
.scene
.insert_primitive(path.apply_scale(scale_factor));
.insert_primitive(path.scale(scale_factor));
}
/// Paint an underline into the scene for the next frame at the current z-index.

View file

@ -116,6 +116,12 @@ pub enum LanguageModelCompletionError {
provider: LanguageModelProviderName,
message: String,
},
#[error("{message}")]
UpstreamProviderError {
message: String,
status: StatusCode,
retry_after: Option<Duration>,
},
#[error("HTTP response error from {provider}'s API: status {status_code} - {message:?}")]
HttpResponseError {
provider: LanguageModelProviderName,

View file

@ -644,8 +644,62 @@ struct ApiError {
headers: HeaderMap<HeaderValue>,
}
/// Represents error responses from Zed's cloud API.
///
/// Example JSON for an upstream HTTP error:
/// ```json
/// {
/// "code": "upstream_http_error",
/// "message": "Received an error from the Anthropic API: upstream connect error or disconnect/reset before headers, reset reason: connection timeout",
/// "upstream_status": 503
/// }
/// ```
#[derive(Debug, serde::Deserialize)]
struct CloudApiError {
code: String,
message: String,
#[serde(default)]
#[serde(deserialize_with = "deserialize_optional_status_code")]
upstream_status: Option<StatusCode>,
#[serde(default)]
retry_after: Option<f64>,
}
fn deserialize_optional_status_code<'de, D>(deserializer: D) -> Result<Option<StatusCode>, D::Error>
where
D: serde::Deserializer<'de>,
{
let opt: Option<u16> = Option::deserialize(deserializer)?;
Ok(opt.and_then(|code| StatusCode::from_u16(code).ok()))
}
impl From<ApiError> for LanguageModelCompletionError {
fn from(error: ApiError) -> Self {
if let Ok(cloud_error) = serde_json::from_str::<CloudApiError>(&error.body) {
if cloud_error.code.starts_with("upstream_http_") {
let status = if let Some(status) = cloud_error.upstream_status {
status
} else if cloud_error.code.ends_with("_error") {
error.status
} else {
// If there's a status code in the code string (e.g. "upstream_http_429")
// then use that; otherwise, see if the JSON contains a status code.
cloud_error
.code
.strip_prefix("upstream_http_")
.and_then(|code_str| code_str.parse::<u16>().ok())
.and_then(|code| StatusCode::from_u16(code).ok())
.unwrap_or(error.status)
};
return LanguageModelCompletionError::UpstreamProviderError {
message: cloud_error.message,
status,
retry_after: cloud_error.retry_after.map(Duration::from_secs_f64),
};
}
}
let retry_after = None;
LanguageModelCompletionError::from_http_status(
PROVIDER_NAME,
@ -1279,3 +1333,155 @@ impl Component for ZedAiConfiguration {
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use http_client::http::{HeaderMap, StatusCode};
use language_model::LanguageModelCompletionError;
#[test]
fn test_api_error_conversion_with_upstream_http_error() {
// upstream_http_error with 503 status should become ServerOverloaded
let error_body = r#"{"code":"upstream_http_error","message":"Received an error from the Anthropic API: upstream connect error or disconnect/reset before headers, reset reason: connection timeout","upstream_status":503}"#;
let api_error = ApiError {
status: StatusCode::INTERNAL_SERVER_ERROR,
body: error_body.to_string(),
headers: HeaderMap::new(),
};
let completion_error: LanguageModelCompletionError = api_error.into();
match completion_error {
LanguageModelCompletionError::UpstreamProviderError { message, .. } => {
assert_eq!(
message,
"Received an error from the Anthropic API: upstream connect error or disconnect/reset before headers, reset reason: connection timeout"
);
}
_ => panic!(
"Expected UpstreamProviderError for upstream 503, got: {:?}",
completion_error
),
}
// upstream_http_error with 500 status should become ApiInternalServerError
let error_body = r#"{"code":"upstream_http_error","message":"Received an error from the OpenAI API: internal server error","upstream_status":500}"#;
let api_error = ApiError {
status: StatusCode::INTERNAL_SERVER_ERROR,
body: error_body.to_string(),
headers: HeaderMap::new(),
};
let completion_error: LanguageModelCompletionError = api_error.into();
match completion_error {
LanguageModelCompletionError::UpstreamProviderError { message, .. } => {
assert_eq!(
message,
"Received an error from the OpenAI API: internal server error"
);
}
_ => panic!(
"Expected UpstreamProviderError for upstream 500, got: {:?}",
completion_error
),
}
// upstream_http_error with 429 status should become RateLimitExceeded
let error_body = r#"{"code":"upstream_http_error","message":"Received an error from the Google API: rate limit exceeded","upstream_status":429}"#;
let api_error = ApiError {
status: StatusCode::INTERNAL_SERVER_ERROR,
body: error_body.to_string(),
headers: HeaderMap::new(),
};
let completion_error: LanguageModelCompletionError = api_error.into();
match completion_error {
LanguageModelCompletionError::UpstreamProviderError { message, .. } => {
assert_eq!(
message,
"Received an error from the Google API: rate limit exceeded"
);
}
_ => panic!(
"Expected UpstreamProviderError for upstream 429, got: {:?}",
completion_error
),
}
// Regular 500 error without upstream_http_error should remain ApiInternalServerError for Zed
let error_body = "Regular internal server error";
let api_error = ApiError {
status: StatusCode::INTERNAL_SERVER_ERROR,
body: error_body.to_string(),
headers: HeaderMap::new(),
};
let completion_error: LanguageModelCompletionError = api_error.into();
match completion_error {
LanguageModelCompletionError::ApiInternalServerError { provider, message } => {
assert_eq!(provider, PROVIDER_NAME);
assert_eq!(message, "Regular internal server error");
}
_ => panic!(
"Expected ApiInternalServerError for regular 500, got: {:?}",
completion_error
),
}
// upstream_http_429 format should be converted to UpstreamProviderError
let error_body = r#"{"code":"upstream_http_429","message":"Upstream Anthropic rate limit exceeded.","retry_after":30.5}"#;
let api_error = ApiError {
status: StatusCode::INTERNAL_SERVER_ERROR,
body: error_body.to_string(),
headers: HeaderMap::new(),
};
let completion_error: LanguageModelCompletionError = api_error.into();
match completion_error {
LanguageModelCompletionError::UpstreamProviderError {
message,
status,
retry_after,
} => {
assert_eq!(message, "Upstream Anthropic rate limit exceeded.");
assert_eq!(status, StatusCode::TOO_MANY_REQUESTS);
assert_eq!(retry_after, Some(Duration::from_secs_f64(30.5)));
}
_ => panic!(
"Expected UpstreamProviderError for upstream_http_429, got: {:?}",
completion_error
),
}
// Invalid JSON in error body should fall back to regular error handling
let error_body = "Not JSON at all";
let api_error = ApiError {
status: StatusCode::INTERNAL_SERVER_ERROR,
body: error_body.to_string(),
headers: HeaderMap::new(),
};
let completion_error: LanguageModelCompletionError = api_error.into();
match completion_error {
LanguageModelCompletionError::ApiInternalServerError { provider, .. } => {
assert_eq!(provider, PROVIDER_NAME);
}
_ => panic!(
"Expected ApiInternalServerError for invalid JSON, got: {:?}",
completion_error
),
}
}
}

View file

@ -847,6 +847,7 @@ impl KeymapFile {
}
}
#[derive(Clone)]
pub enum KeybindUpdateOperation<'a> {
Replace {
/// Describes the keybind to create
@ -865,6 +866,47 @@ pub enum KeybindUpdateOperation<'a> {
},
}
impl KeybindUpdateOperation<'_> {
pub fn generate_telemetry(
&self,
) -> (
// The keybind that is created
String,
// The keybinding that was removed
String,
// The source of the keybinding
String,
) {
let (new_binding, removed_binding, source) = match &self {
KeybindUpdateOperation::Replace {
source,
target,
target_keybind_source,
} => (Some(source), Some(target), Some(*target_keybind_source)),
KeybindUpdateOperation::Add { source, .. } => (Some(source), None, None),
KeybindUpdateOperation::Remove {
target,
target_keybind_source,
} => (None, Some(target), Some(*target_keybind_source)),
};
let new_binding = new_binding
.map(KeybindUpdateTarget::telemetry_string)
.unwrap_or("null".to_owned());
let removed_binding = removed_binding
.map(KeybindUpdateTarget::telemetry_string)
.unwrap_or("null".to_owned());
let source = source
.as_ref()
.map(KeybindSource::name)
.map(ToOwned::to_owned)
.unwrap_or("null".to_owned());
(new_binding, removed_binding, source)
}
}
impl<'a> KeybindUpdateOperation<'a> {
pub fn add(source: KeybindUpdateTarget<'a>) -> Self {
Self::Add { source, from: None }
@ -905,6 +947,16 @@ impl<'a> KeybindUpdateTarget<'a> {
keystrokes.pop();
keystrokes
}
fn telemetry_string(&self) -> String {
format!(
"action_name: {}, context: {}, action_arguments: {}, keystrokes: {}",
self.action_name,
self.context.unwrap_or("global"),
self.action_arguments.unwrap_or("none"),
self.keystrokes_unparsed()
)
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
@ -940,13 +992,17 @@ impl KeybindSource {
}
pub fn from_meta(index: KeyBindingMetaIndex) -> Self {
match index {
Self::try_from_meta(index).unwrap()
}
pub fn try_from_meta(index: KeyBindingMetaIndex) -> Result<Self> {
Ok(match index {
Self::USER => KeybindSource::User,
Self::BASE => KeybindSource::Base,
Self::DEFAULT => KeybindSource::Default,
Self::VIM => KeybindSource::Vim,
_ => unreachable!(),
}
_ => anyhow::bail!("Invalid keybind source {:?}", index),
})
}
}

View file

@ -34,6 +34,7 @@ search.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
telemetry.workspace = true
theme.workspace = true
tree-sitter-json.workspace = true
tree-sitter-rust.workspace = true

File diff suppressed because it is too large Load diff

View file

@ -40,6 +40,10 @@ impl<const COLS: usize> TableContents<COLS> {
TableContents::UniformList(data) => data.row_count,
}
}
fn is_empty(&self) -> bool {
self.len() == 0
}
}
pub struct TableInteractionState {
@ -375,6 +379,7 @@ pub struct Table<const COLS: usize = 3> {
interaction_state: Option<WeakEntity<TableInteractionState>>,
column_widths: Option<[Length; COLS]>,
map_row: Option<Rc<dyn Fn((usize, Div), &mut Window, &mut App) -> AnyElement>>,
empty_table_callback: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
}
impl<const COLS: usize> Table<COLS> {
@ -388,6 +393,7 @@ impl<const COLS: usize> Table<COLS> {
interaction_state: None,
column_widths: None,
map_row: None,
empty_table_callback: None,
}
}
@ -460,6 +466,15 @@ impl<const COLS: usize> Table<COLS> {
self.map_row = Some(Rc::new(callback));
self
}
/// Provide a callback that is invoked when the table is rendered without any rows
pub fn empty_table_callback(
mut self,
callback: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
) -> Self {
self.empty_table_callback = Some(Rc::new(callback));
self
}
}
fn base_cell_style(width: Option<Length>, cx: &App) -> Div {
@ -582,6 +597,7 @@ impl<const COLS: usize> RenderOnce for Table<COLS> {
};
let width = self.width;
let no_rows_rendered = self.rows.is_empty();
let table = div()
.when_some(width, |this, width| this.w(width))
@ -662,6 +678,21 @@ impl<const COLS: usize> RenderOnce for Table<COLS> {
})
}),
)
.when_some(
no_rows_rendered
.then_some(self.empty_table_callback)
.flatten(),
|this, callback| {
this.child(
h_flex()
.size_full()
.p_3()
.items_start()
.justify_center()
.child(callback(window, cx)),
)
},
)
.when_some(
width.and(interaction_state.as_ref()),
|this, interaction_state| {

View file

@ -40,6 +40,7 @@ rpc.workspace = true
schemars.workspace = true
serde.workspace = true
settings.workspace = true
settings_ui.workspace = true
smallvec.workspace = true
story = { workspace = true, optional = true }
telemetry.workspace = true

View file

@ -30,6 +30,7 @@ use onboarding_banner::OnboardingBanner;
use project::Project;
use rpc::proto;
use settings::Settings as _;
use settings_ui::keybindings;
use std::sync::Arc;
use theme::ActiveTheme;
use title_bar_settings::TitleBarSettings;
@ -683,7 +684,7 @@ impl TitleBar {
)
.separator()
.action("Settings", zed_actions::OpenSettings.boxed_clone())
.action("Key Bindings", Box::new(zed_actions::OpenKeymap))
.action("Key Bindings", Box::new(keybindings::OpenKeymapEditor))
.action(
"Themes…",
zed_actions::theme_selector::Toggle::default().boxed_clone(),
@ -727,7 +728,7 @@ impl TitleBar {
.menu(|window, cx| {
ContextMenu::build(window, cx, |menu, _, _| {
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
.action("Key Bindings", Box::new(zed_actions::OpenKeymap))
.action("Key Bindings", Box::new(keybindings::OpenKeymapEditor))
.action(
"Themes…",
zed_actions::theme_selector::Toggle::default().boxed_clone(),

View file

@ -972,12 +972,10 @@ impl ContextMenu {
.children(action.as_ref().and_then(|action| {
self.action_context
.as_ref()
.map(|focus| {
.and_then(|focus| {
KeyBinding::for_action_in(&**action, focus, window, cx)
})
.unwrap_or_else(|| {
KeyBinding::for_action(&**action, window, cx)
})
.or_else(|| KeyBinding::for_action(&**action, window, cx))
.map(|binding| {
div().ml_4().child(binding.disabled(*disabled)).when(
*disabled && documentation_aside.is_some(),

View file

@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition.workspace = true
name = "zed"
version = "0.196.0"
version = "0.196.4"
publish.workspace = true
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View file

@ -1 +1 @@
dev
preview

View file

@ -1,5 +1,6 @@
use collab_ui::collab_panel;
use gpui::{Menu, MenuItem, OsAction};
use settings_ui::keybindings;
use terminal_view::terminal_panel;
pub fn app_menus() -> Vec<Menu> {
@ -16,7 +17,7 @@ pub fn app_menus() -> Vec<Menu> {
name: "Settings".into(),
items: vec![
MenuItem::action("Open Settings", super::OpenSettings),
MenuItem::action("Open Key Bindings", zed_actions::OpenKeymap),
MenuItem::action("Open Key Bindings", keybindings::OpenKeymapEditor),
MenuItem::action("Open Default Settings", super::OpenDefaultSettings),
MenuItem::action(
"Open Default Key Bindings",

View file

@ -148,7 +148,7 @@ On some systems the file `/etc/prime-discrete` can be used to enforce the use of
On others, you may be able to the environment variable `DRI_PRIME=1` when running Zed to force the use of the discrete GPU.
If you're using an AMD GPU and Zed crashes when selecting long lines, try setting the `ZED_SAMPLE_COUNT=0` environment variable. (See [#26143](https://github.com/zed-industries/zed/issues/26143))
If you're using an AMD GPU and Zed crashes when selecting long lines, try setting the `ZED_PATH_SAMPLE_COUNT=0` environment variable. (See [#26143](https://github.com/zed-industries/zed/issues/26143))
If you're using an AMD GPU, you might get a 'Broken Pipe' error. Try using the RADV or Mesa drivers. (See [#13880](https://github.com/zed-industries/zed/issues/13880))