core: Initial Debug UI

This commit is contained in:
Nathan Adams 2023-05-31 11:32:58 +02:00
parent eb70a4f361
commit e12e2a2e54
13 changed files with 407 additions and 4 deletions

1
Cargo.lock generated
View File

@ -3696,6 +3696,7 @@ dependencies = [
"clap",
"dasp",
"downcast-rs",
"egui",
"encoding_rs",
"enumset",
"flash-lso",

View File

@ -51,6 +51,7 @@ realfft = "3.3.0"
hashbrown = { version = "0.13.2", features = ["raw"] }
scopeguard = "1.1.0"
fluent-templates = "0.8.0"
egui = { version = "0.22.0", optional = true }
[target.'cfg(not(target_family = "wasm"))'.dependencies.futures]
version = "0.3.28"

View File

@ -26,7 +26,7 @@ use crate::stub::StubCollection;
use crate::tag_utils::{SwfMovie, SwfSlice};
use crate::timer::Timers;
use core::fmt;
use gc_arena::{Collect, MutationContext};
use gc_arena::{Collect, DynamicRootSet, MutationContext};
use instant::Instant;
use rand::rngs::SmallRng;
use ruffle_render::backend::RenderBackend;
@ -216,6 +216,9 @@ pub struct UpdateContext<'a, 'gc> {
/// Manager of in-progress media streams.
pub stream_manager: &'a mut StreamManager<'gc>,
/// Dynamic root for allowing handles to GC objects to exist outside of the GC.
pub dynamic_root: &'a mut DynamicRootSet<'gc>,
}
/// Convenience methods for controlling audio.
@ -373,6 +376,7 @@ impl<'a, 'gc> UpdateContext<'a, 'gc> {
actions_since_timeout_check: self.actions_since_timeout_check,
frame_phase: self.frame_phase,
stream_manager: self.stream_manager,
dynamic_root: self.dynamic_root,
}
}

45
core/src/debug_ui.rs Normal file
View File

@ -0,0 +1,45 @@
mod display_object;
mod handle;
use crate::context::UpdateContext;
use crate::debug_ui::display_object::DisplayObjectWindow;
use crate::debug_ui::handle::DisplayObjectHandle;
use hashbrown::HashMap;
#[derive(Default)]
pub struct DebugUi {
display_objects: HashMap<DisplayObjectHandle, DisplayObjectWindow>,
queued_messages: Vec<Message>,
}
#[derive(Debug)]
pub enum Message {
TrackDisplayObject(DisplayObjectHandle),
TrackStage,
}
impl DebugUi {
pub fn show(&mut self, egui_ctx: &egui::Context, context: &mut UpdateContext) {
let mut messages = std::mem::take(&mut self.queued_messages);
self.display_objects.retain(|object, window| {
let object = object.fetch(context);
window.show(egui_ctx, context, object, &mut messages)
});
for message in messages {
match message {
Message::TrackDisplayObject(object) => self.track_display_object(object),
Message::TrackStage => {
self.track_display_object(DisplayObjectHandle::new(context, context.stage))
}
}
}
}
pub fn queue_message(&mut self, message: Message) {
self.queued_messages.push(message);
}
pub fn track_display_object(&mut self, handle: DisplayObjectHandle) {
self.display_objects.insert(handle, Default::default());
}
}

View File

@ -0,0 +1,233 @@
use crate::context::UpdateContext;
use crate::debug_ui::handle::DisplayObjectHandle;
use crate::debug_ui::Message;
use crate::display_object::{DisplayObject, TDisplayObject, TDisplayObjectContainer};
use egui::{CollapsingHeader, ComboBox, Grid, Id, Ui, Window};
use std::borrow::Cow;
use swf::BlendMode;
const ALL_BLEND_MODES: [BlendMode; 14] = [
BlendMode::Normal,
BlendMode::Layer,
BlendMode::Multiply,
BlendMode::Screen,
BlendMode::Lighten,
BlendMode::Darken,
BlendMode::Difference,
BlendMode::Add,
BlendMode::Subtract,
BlendMode::Invert,
BlendMode::Alpha,
BlendMode::Erase,
BlendMode::Overlay,
BlendMode::HardLight,
];
#[derive(Debug, Eq, PartialEq, Hash, Default, Copy, Clone)]
enum Panel {
#[default]
Position,
Display,
Children,
}
#[derive(Debug, Default)]
pub struct DisplayObjectWindow {
open_panel: Panel,
}
impl DisplayObjectWindow {
pub fn show<'gc>(
&mut self,
egui_ctx: &egui::Context,
context: &mut UpdateContext<'_, 'gc>,
object: DisplayObject<'gc>,
messages: &mut Vec<Message>,
) -> bool {
let mut keep_open = true;
Window::new(summary_name(object))
.id(Id::new(object.as_ptr()))
.open(&mut keep_open)
.show(egui_ctx, |ui| {
ui.horizontal(|ui| {
ui.selectable_value(&mut self.open_panel, Panel::Position, "Position");
ui.selectable_value(&mut self.open_panel, Panel::Display, "Display");
if let Some(ctr) = object.as_container() {
if !ctr.is_empty() {
ui.selectable_value(
&mut self.open_panel,
Panel::Children,
format!("Children ({})", ctr.num_children()),
);
}
}
});
ui.separator();
match self.open_panel {
Panel::Position => self.show_position(ui, object),
Panel::Display => self.show_display(ui, context, object, messages),
Panel::Children => self.show_children(ui, context, object, messages),
}
});
keep_open
}
pub fn show_display<'gc>(
&mut self,
ui: &mut Ui,
context: &mut UpdateContext<'_, 'gc>,
object: DisplayObject<'gc>,
messages: &mut Vec<Message>,
) {
Grid::new(ui.id().with("display"))
.num_columns(2)
.show(ui, |ui| {
if let Some(parent) = object.parent() {
ui.label("Parent");
if ui.button(summary_name(parent)).clicked() {
messages.push(Message::TrackDisplayObject(DisplayObjectHandle::new(
context, parent,
)));
}
ui.end_row();
}
let was_visible = object.visible();
let mut is_visible = was_visible;
ui.label("Visibility");
ui.checkbox(&mut is_visible, "Visible");
ui.end_row();
if is_visible != was_visible {
object.set_visible(context.gc_context, is_visible);
}
ui.label("Blend mode");
let old_blend = object.blend_mode();
let mut new_blend = old_blend;
ComboBox::from_id_source(ui.id().with("blendmode"))
.selected_text(blend_mode_name(old_blend))
.show_ui(ui, |ui| {
for mode in ALL_BLEND_MODES {
ui.selectable_value(&mut new_blend, mode, blend_mode_name(mode));
}
});
ui.end_row();
if new_blend != old_blend {
object.set_blend_mode(context.gc_context, new_blend);
}
});
}
pub fn show_position(&mut self, ui: &mut Ui, object: DisplayObject<'_>) {
Grid::new(ui.id().with("position"))
.num_columns(2)
.show(ui, |ui| {
ui.label("Name");
// &mut of a temporary thing because we don't want to actually be able to change this
// If we disable it, the user can't highlight or interact with it, so this makes it readonly but enabled
ui.text_edit_singleline(&mut object.name().to_string());
ui.end_row();
ui.label("World Bounds");
ui.label(object.world_bounds().to_string());
ui.end_row();
ui.label("Local Bounds");
ui.label(object.local_bounds().to_string());
ui.end_row();
let base = object.base();
let matrix = base.matrix();
ui.label("Local Position");
ui.label(format!("{}, {}", matrix.tx, matrix.ty));
ui.end_row();
ui.label("Local Rotation");
ui.label(format!("{}, {}", matrix.b, matrix.c));
ui.end_row();
ui.label("Local Scale");
ui.label(format!("{}, {}", matrix.a, matrix.d));
ui.end_row();
});
}
pub fn show_children<'gc>(
&mut self,
ui: &mut Ui,
context: &mut UpdateContext<'_, 'gc>,
object: DisplayObject<'gc>,
messages: &mut Vec<Message>,
) {
if let Some(ctr) = object.as_container() {
for child in ctr.iter_render_list() {
show_display_tree(ui, context, child, messages);
}
}
}
}
pub fn show_display_tree<'gc>(
ui: &mut Ui,
context: &mut UpdateContext<'_, 'gc>,
object: DisplayObject<'gc>,
messages: &mut Vec<Message>,
) {
CollapsingHeader::new(summary_name(object))
.id_source(ui.id().with(object.as_ptr()))
.show(ui, |ui| {
if ui.button("Track").clicked() {
messages.push(Message::TrackDisplayObject(DisplayObjectHandle::new(
context, object,
)));
}
if let Some(ctr) = object.as_container() {
for child in ctr.iter_render_list() {
show_display_tree(ui, context, child, messages);
}
}
});
}
fn summary_name(object: DisplayObject) -> Cow<str> {
let do_type = match object {
DisplayObject::Stage(_) => "Stage",
DisplayObject::Bitmap(_) => "Bitmap",
DisplayObject::Avm1Button(_) => "Avm1Button",
DisplayObject::Avm2Button(_) => "Avm2Button",
DisplayObject::EditText(_) => "EditText",
DisplayObject::Graphic(_) => "Graphic",
DisplayObject::MorphShape(_) => "MorphShape",
DisplayObject::MovieClip(_) => "MovieClip",
DisplayObject::Text(_) => "Text",
DisplayObject::Video(_) => "Video",
DisplayObject::LoaderDisplay(_) => "LoaderDisplay",
};
let name = object.name();
if name.is_empty() {
Cow::Borrowed(do_type)
} else {
Cow::Owned(format!("{do_type} \"{name}\""))
}
}
fn blend_mode_name(mode: BlendMode) -> &'static str {
match mode {
BlendMode::Normal => "Normal",
BlendMode::Layer => "Layer",
BlendMode::Multiply => "Multiply",
BlendMode::Screen => "Screen",
BlendMode::Lighten => "Lighten",
BlendMode::Darken => "Darken",
BlendMode::Difference => "Difference",
BlendMode::Add => "Add",
BlendMode::Subtract => "Subtract",
BlendMode::Invert => "Invert",
BlendMode::Alpha => "Alpha",
BlendMode::Erase => "Erase",
BlendMode::Overlay => "Overlay",
BlendMode::HardLight => "HardLight",
}
}

View File

@ -0,0 +1,52 @@
use crate::context::UpdateContext;
use crate::display_object::{DisplayObject, DisplayObjectPtr, TDisplayObject};
use gc_arena::{DynamicRoot, Rootable};
use std::fmt::{Debug, Formatter};
use std::hash::{Hash, Hasher};
// TODO: Make this generic somehow, we'll want AVM1 and AVM2 object handles too
#[derive(Clone)]
pub struct DisplayObjectHandle {
root: DynamicRoot<Rootable![DisplayObject<'gc>]>,
ptr: *const DisplayObjectPtr,
}
impl DisplayObjectHandle {
pub fn new<'gc>(
context: &mut UpdateContext<'_, 'gc>,
object: impl Into<DisplayObject<'gc>>,
) -> Self {
let object = object.into();
Self {
root: context.dynamic_root.stash(context.gc_context, object),
ptr: object.as_ptr(),
}
}
pub fn fetch<'gc>(&self, context: &mut UpdateContext<'_, 'gc>) -> DisplayObject<'gc> {
*context.dynamic_root.fetch(&self.root)
}
}
impl Debug for DisplayObjectHandle {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("DisplayObjectHandle")
.field(&self.ptr)
.finish()
}
}
impl PartialEq<DisplayObjectHandle> for DisplayObjectHandle {
#[inline(always)]
fn eq(&self, other: &DisplayObjectHandle) -> bool {
self.ptr == other.ptr
}
}
impl Hash for DisplayObjectHandle {
fn hash<H: Hasher>(&self, state: &mut H) {
self.ptr.hash(state);
}
}
impl Eq for DisplayObjectHandle {}

View File

@ -17,6 +17,7 @@ use ruffle_macros::enum_trait_object;
use ruffle_render::transform::Transform;
use std::cell::{Ref, RefMut};
use std::fmt::Debug;
use std::hash::Hash;
use std::sync::Arc;
use swf::{BlendMode, ColorTransform, Fixed8};

View File

@ -48,6 +48,8 @@ mod xml;
pub mod backend;
pub mod compatibility_rules;
pub mod config;
#[cfg(feature = "egui")]
pub mod debug_ui;
pub mod external;
pub mod i18n;
pub mod stub;

View File

@ -46,7 +46,7 @@ use crate::stub::StubCollection;
use crate::tag_utils::SwfMovie;
use crate::timer::Timers;
use crate::vminterface::Instantiator;
use gc_arena::{ArenaParameters, Collect, GcCell};
use gc_arena::{ArenaParameters, Collect, DynamicRootSet, GcCell};
use gc_arena::{MutationContext, Rootable};
use instant::Instant;
use rand::{rngs::SmallRng, SeedableRng};
@ -160,6 +160,9 @@ struct GcRootData<'gc> {
/// List of actively playing streams to decode.
stream_manager: StreamManager<'gc>,
/// Dynamic root for allowing handles to GC objects to exist outside of the GC.
dynamic_root: DynamicRootSet<'gc>,
}
impl<'gc> GcRootData<'gc> {
@ -185,6 +188,7 @@ impl<'gc> GcRootData<'gc> {
&mut ExternalInterface<'gc>,
&mut AudioManager<'gc>,
&mut StreamManager<'gc>,
&mut DynamicRootSet<'gc>,
) {
(
self.stage,
@ -203,6 +207,7 @@ impl<'gc> GcRootData<'gc> {
&mut self.external_interface,
&mut self.audio_manager,
&mut self.stream_manager,
&mut self.dynamic_root,
)
}
}
@ -313,6 +318,10 @@ pub struct Player {
/// Any compatibility rules to apply for this movie.
compatibility_rules: CompatibilityRules,
/// Debug UI windows
#[cfg(feature = "egui")]
debug_ui: Rc<RefCell<crate::debug_ui::DebugUi>>,
}
impl Player {
@ -1750,6 +1759,7 @@ impl Player {
external_interface,
audio_manager,
stream_manager,
dynamic_root,
) = root_data.update_context_params();
let mut update_context = UpdateContext {
@ -1799,6 +1809,7 @@ impl Player {
frame_phase: &mut self.frame_phase,
stub_tracker: &mut self.stub_tracker,
stream_manager,
dynamic_root,
};
let prev_frame_rate = *update_context.frame_rate;
@ -1845,6 +1856,23 @@ impl Player {
)
}
#[cfg(feature = "egui")]
pub fn show_debug_ui(&mut self, egui_ctx: &egui::Context) {
// To allow using `mutate_with_update_context` and passing the context inside the debug ui,
// we avoid borrowing directly from self here.
// This method should only be called once and it will panic if it tries to recursively render.
let debug_ui = self.debug_ui.clone();
let mut debug_ui = debug_ui.borrow_mut();
self.mutate_with_update_context(|context| {
debug_ui.show(egui_ctx, context);
});
}
#[cfg(feature = "egui")]
pub fn debug_ui_message(&mut self, message: crate::debug_ui::Message) {
self.debug_ui.borrow_mut().queue_message(message)
}
/// Update the current state of the player.
///
/// The given function will be called with the current stage root, current
@ -2203,6 +2231,7 @@ impl PlayerBuilder {
gc_context,
interner: &mut interner,
};
let dynamic_root = DynamicRootSet::new(gc_context);
GcRoot {
callstack: GcCell::allocate(gc_context, GcCallstack::default()),
@ -2228,6 +2257,7 @@ impl PlayerBuilder {
timers: Timers::new(),
unbound_text_fields: Vec::new(),
stream_manager: StreamManager::new(),
dynamic_root,
},
),
}
@ -2317,6 +2347,8 @@ impl PlayerBuilder {
spoofed_url: self.spoofed_url.clone(),
compatibility_rules: self.compatibility_rules.clone(),
stub_tracker: StubCollection::new(),
#[cfg(feature = "egui")]
debug_ui: Default::default(),
// GC data
gc_arena: Rc::new(RefCell::new(GcArena::new(

View File

@ -13,7 +13,7 @@ cpal = "0.15.2"
egui = "0.22.0"
egui-wgpu = { version = "0.22.0", features = ["winit"] }
egui-winit = "0.22.0"
ruffle_core = { path = "../core", features = ["audio", "clap", "mp3", "nellymoser", "default_compatibility_rules"] }
ruffle_core = { path = "../core", features = ["audio", "clap", "mp3", "nellymoser", "default_compatibility_rules", "egui"] }
ruffle_render = { path = "../render", features = ["clap"] }
ruffle_render_wgpu = { path = "../render/wgpu", features = ["clap"] }
ruffle_video_software = { path = "../video/software", optional = true }

View File

@ -22,4 +22,7 @@ help-menu-join-discord = Join Discord
help-menu-report-a-bug = Report a Bug...
help-menu-sponsor-development = Sponsor Development...
help-menu-translate-ruffle = Translate Ruffle...
help-menu-about = About Ruffle
help-menu-about = About Ruffle
debug-menu = Debug Tools
debug-menu-open-stage = View Stage Info

View File

@ -14,6 +14,7 @@ use egui::*;
use fluent_templates::fluent_bundle::FluentValue;
use fluent_templates::{static_loader, Loader};
use ruffle_core::backend::ui::US_ENGLISH;
use ruffle_core::debug_ui::Message as DebugMessage;
use ruffle_core::Player;
use std::collections::HashMap;
use sys_locale::get_locale;
@ -105,6 +106,10 @@ impl RuffleGui {
self.as3_warning(egui_ctx);
if let Some(player) = player {
player.show_debug_ui(egui_ctx);
}
if !self.context_menu.is_empty() {
self.context_menu(egui_ctx);
}
@ -194,6 +199,16 @@ impl RuffleGui {
}
});
});
menu::menu_button(ui, text(&self.locale, "debug-menu"), |ui| {
ui.add_enabled_ui(player.is_some(), |ui| {
if Button::new(text(&self.locale, "debug-menu-open-stage")).ui(ui).clicked() {
ui.close_menu();
if let Some(player) = player {
player.debug_ui_message(DebugMessage::TrackStage);
}
}
});
});
menu::menu_button(ui, text(&self.locale, "help-menu"), |ui| {
if ui.button(text(&self.locale, "help-menu-join-discord")).clicked() {
self.launch_website(ui, "https://discord.gg/ruffle");

View File

@ -1,4 +1,5 @@
use crate::{types::point::Coordinate as PointCoordinate, Point, Twips};
use std::fmt::{Display, Formatter};
pub trait Coordinate: PointCoordinate + Ord {
const INVALID: Self;
@ -127,3 +128,16 @@ impl<T: Coordinate> Default for Rectangle<T> {
Self::INVALID
}
}
impl<T> Display for Rectangle<T>
where
T: Display + Coordinate,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}, {} to {}, {}",
self.x_min, self.y_min, self.x_max, self.y_max
)
}
}