core: Add more avm2 debug info

This commit is contained in:
Nathan Adams 2023-06-07 23:31:44 +02:00
parent 5b429e3bf5
commit 647006b8d0
8 changed files with 359 additions and 81 deletions

1
Cargo.lock generated
View File

@ -3728,6 +3728,7 @@ dependencies = [
"num-derive",
"num-traits",
"percent-encoding",
"png",
"quick-xml",
"rand",
"realfft",

View File

@ -53,6 +53,7 @@ scopeguard = "1.1.0"
fluent-templates = "0.8.0"
egui = { version = "0.22.0", optional = true }
egui_extras = { version = "0.22.0", optional = true }
png = { version = "0.17.8", optional = true }
[target.'cfg(not(target_family = "wasm"))'.dependencies.futures]
version = "0.3.28"
@ -72,7 +73,7 @@ nellymoser = ["nellymoser-rs"]
audio = ["dasp"]
known_stubs = ["linkme"]
default_compatibility_rules = []
egui = ["dep:egui", "dep:egui_extras"]
egui = ["dep:egui", "dep:egui_extras", "png"]
[build-dependencies]
build_playerglobal = { path = "build_playerglobal" }

View File

@ -451,6 +451,21 @@ mod wrapper {
}
}
#[cfg(feature = "egui")]
pub fn debug_sync_status(&self) -> std::borrow::Cow<'static, str> {
match self.0.read().dirty_state {
DirtyState::Clean => std::borrow::Cow::Borrowed("Clean"),
DirtyState::CpuModified(area) => std::borrow::Cow::Owned(format!(
"CPU modified from {}, {} to {}, {}",
area.x_min, area.y_min, area.x_max, area.y_max
)),
DirtyState::GpuModified(_, area) => std::borrow::Cow::Owned(format!(
"GPU modified from {}, {} to {}, {}",
area.x_min, area.y_min, area.x_max, area.y_max
)),
}
}
pub fn is_point_in_bounds(&self, x: i32, y: i32) -> bool {
x >= 0 && x < self.width() as i32 && y >= 0 && y < self.height() as i32
}

View File

@ -16,6 +16,7 @@ use gc_arena::DynamicRootSet;
use hashbrown::HashMap;
use ruffle_render::commands::CommandHandler;
use ruffle_render::matrix::Matrix;
use std::fmt::{Debug, Formatter};
use std::sync::{Arc, Weak};
use swf::{Color, Rectangle, Twips};
use weak_table::PtrWeakKeyHashMap;
@ -27,6 +28,7 @@ pub struct DebugUi {
avm1_objects: HashMap<AVM1ObjectHandle, Avm1ObjectWindow>,
avm2_objects: HashMap<AVM2ObjectHandle, Avm2ObjectWindow>,
queued_messages: Vec<Message>,
items_to_save: Vec<ItemToSave>,
}
#[derive(Debug)]
@ -37,10 +39,11 @@ pub enum Message {
TrackAVM2Object(AVM2ObjectHandle),
TrackStage,
TrackTopLevelMovie,
SaveFile(ItemToSave),
}
impl DebugUi {
pub fn show(&mut self, egui_ctx: &egui::Context, context: &mut UpdateContext) {
pub(crate) 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| {
@ -59,13 +62,15 @@ impl DebugUi {
});
self.movies
.retain(|movie, window| window.show(egui_ctx, context, movie));
.retain(|movie, window| window.show(egui_ctx, context, movie, &mut messages));
for message in messages {
match message {
Message::TrackDisplayObject(object) => self.track_display_object(object),
Message::TrackDisplayObject(object) => {
self.track_display_object(object);
}
Message::TrackStage => {
self.track_display_object(DisplayObjectHandle::new(context, context.stage))
self.track_display_object(DisplayObjectHandle::new(context, context.stage));
}
Message::TrackMovie(movie) => {
self.movies.insert(movie, Default::default());
@ -79,9 +84,16 @@ impl DebugUi {
Message::TrackAVM2Object(object) => {
self.avm2_objects.insert(object, Default::default());
}
Message::SaveFile(file) => {
self.items_to_save.push(file);
}
}
}
}
pub fn items_to_save(&mut self) -> Vec<ItemToSave> {
std::mem::take(&mut self.items_to_save)
}
pub fn queue_message(&mut self, message: Message) {
self.queued_messages.push(message);
@ -134,6 +146,20 @@ impl DebugUi {
}
}
pub struct ItemToSave {
pub suggested_name: String,
pub data: Vec<u8>,
}
impl Debug for ItemToSave {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ItemToSave")
.field("suggested_name", &self.suggested_name)
.field("data", &self.data.len())
.finish()
}
}
fn draw_debug_rect(
context: &mut RenderContext,
color: Color,

View File

@ -1,14 +1,23 @@
use crate::avm2::property::Property;
use crate::avm2::{Activation, Error, Namespace, Object, TObject, Value};
use crate::avm2::{Activation, ClassObject, Error, Namespace, Object, TObject, Value};
use crate::context::UpdateContext;
use crate::debug_ui::display_object::open_display_object_button;
use crate::debug_ui::handle::{AVM2ObjectHandle, DisplayObjectHandle};
use crate::debug_ui::Message;
use egui::{Align, Id, Layout, TextEdit, Ui, Widget, Window};
use egui_extras::{Column, TableBuilder};
use crate::debug_ui::{ItemToSave, Message};
use egui::{Align, Checkbox, Grid, Id, Layout, TextEdit, Ui, Window};
use egui_extras::{Column, TableBody, TableBuilder, TableRow};
use fnv::FnvHashMap;
use gc_arena::MutationContext;
use std::borrow::Cow;
#[derive(Debug, Eq, PartialEq, Hash, Default, Copy, Clone)]
enum Panel {
Information,
#[default]
Properties,
Class,
}
#[derive(Debug, Default)]
pub struct Avm2ObjectWindow {
hovered_debug_rect: Option<DisplayObjectHandle>,
@ -16,6 +25,7 @@ pub struct Avm2ObjectWindow {
call_getters: bool,
getter_values: FnvHashMap<(String, String), Option<ValueWidget>>,
search: String,
open_panel: Panel,
}
impl Avm2ObjectWindow {
@ -36,13 +46,178 @@ impl Avm2ObjectWindow {
Window::new(object_name(activation.context.gc_context, object))
.id(Id::new(object.as_ptr()))
.open(&mut keep_open)
.scroll2([false, false]) // Table will provide its own scrolling
.scroll2([true, true])
.show(egui_ctx, |ui| {
self.show_properties(object, messages, &mut activation, ui);
ui.horizontal(|ui| {
ui.selectable_value(&mut self.open_panel, Panel::Information, "Information");
ui.selectable_value(&mut self.open_panel, Panel::Properties, "Properties");
if object.as_class_object().is_some() {
ui.selectable_value(&mut self.open_panel, Panel::Class, "Class Info");
}
});
ui.separator();
match self.open_panel {
Panel::Information => {
self.show_information(object, messages, &mut activation, ui)
}
Panel::Properties => {
self.show_properties(object, messages, &mut activation, ui)
}
Panel::Class => {
if let Some(class) = object.as_class_object() {
self.show_class(class, messages, &mut activation, ui)
}
}
}
});
keep_open
}
fn show_information<'gc>(
&mut self,
object: Object<'gc>,
messages: &mut Vec<Message>,
activation: &mut Activation<'_, 'gc>,
ui: &mut Ui,
) {
Grid::new(ui.id().with("info"))
.num_columns(2)
.striped(true)
.show(ui, |ui| {
if let Some(class) = object.instance_of() {
ui.label("Instance Of");
ValueWidget::new(activation, Ok(class.into())).show(ui, messages);
ui.end_row();
}
if let Some(object) = object.as_display_object() {
ui.label("Display Object");
open_display_object_button(
ui,
&mut activation.context,
messages,
object,
&mut self.hovered_debug_rect,
);
ui.end_row();
}
if let Some(bmd) = object.as_bitmap_data() {
ui.label("Bitmap Data Size");
ui.label(format!("{} x {}", bmd.width(), bmd.height()));
ui.end_row();
ui.label("Bitmap Data Status");
if bmd.disposed() {
ui.label("Disposed");
} else {
ui.horizontal(|ui| {
ui.label("Alive");
ui.add_space(10.0);
if ui.button("Save File...").clicked() {
let mut data = Vec::new();
let mut encoder =
png::Encoder::new(&mut data, bmd.width(), bmd.height());
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
if let Err(e) = encoder.write_header().and_then(|mut w| {
w.write_image_data(&bmd.sync().read().pixels_rgba())
}) {
tracing::error!("Couldn't create png: {e}");
} else {
messages.push(Message::SaveFile(ItemToSave {
suggested_name: format!("{:p}.png", object.as_ptr()),
data,
}));
}
}
});
}
ui.end_row();
ui.label("Bitmap Data Transparency");
ui.add_enabled(false, Checkbox::new(&mut bmd.transparency(), "Transparent"));
ui.end_row();
ui.label("Bitmap Data Sync");
ui.label(bmd.debug_sync_status());
ui.end_row();
}
if let Some(ba) = object.as_bytearray() {
ui.label("Byte Array");
if ba.len() > 0 {
ui.horizontal(|ui| {
ui.label(format!("{} bytes", ba.len()));
ui.add_space(10.0);
if ui.button("Sync & Save PNG...").clicked() {
messages.push(Message::SaveFile(ItemToSave {
suggested_name: format!("{:p}.png", object.as_ptr()),
data: ba.bytes().to_vec(),
}));
}
});
} else {
ui.label("0 bytes");
}
ui.end_row();
}
});
}
fn show_class<'gc>(
&mut self,
class: ClassObject<'gc>,
messages: &mut Vec<Message>,
activation: &mut Activation<'_, 'gc>,
ui: &mut Ui,
) {
Grid::new(ui.id().with("class"))
.num_columns(2)
.striped(true)
.spacing([8.0, 8.0])
.show(ui, |ui| {
let definition = class.inner_class_definition();
let name = definition.read().name();
ui.label("Namespace");
ui.text_edit_singleline(&mut name.namespace().as_uri().to_string().as_str());
ui.end_row();
ui.label("Name");
ui.text_edit_singleline(&mut name.local_name().to_string().as_str());
ui.end_row();
ui.label("Super Chain");
ui.vertical(|ui| {
let mut superclass = Some(class);
while let Some(class) = superclass {
ValueWidget::new(activation, Ok(class.into())).show(ui, messages);
superclass = class.superclass_object();
}
});
ui.end_row();
ui.label("Interfaces");
ui.vertical(|ui| {
for interface in class.interfaces() {
ui.text_edit_singleline(
&mut interface
.read()
.name()
.to_qualified_name_err_message(activation.context.gc_context)
.to_string()
.as_str(),
);
}
});
ui.end_row();
});
}
fn show_properties<'gc>(
&mut self,
object: Object<'gc>,
@ -59,13 +234,15 @@ impl Avm2ObjectWindow {
}
entries.sort_by(|a, b| a.0.cmp(&b.0));
// [NA] Adding these on the same line seems to break the width of the table :(
TextEdit::singleline(&mut self.search)
.hint_text("Search...")
.ui(ui);
ui.horizontal(|ui| {
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
ui.checkbox(&mut self.show_private_items, "Show Private Items");
ui.checkbox(&mut self.call_getters, "Call Getters");
ui.add_sized(
ui.available_size(),
TextEdit::singleline(&mut self.search).hint_text("Search..."),
);
});
});
let search = self.search.to_ascii_lowercase();
@ -91,17 +268,39 @@ impl Avm2ObjectWindow {
})
.body(|mut body| {
for (name, ns, prop) in entries {
if (ns.is_public() || self.show_private_items)
if (self.show_private_items || ns.is_public())
&& name.to_ascii_lowercase().contains(&search)
{
match prop {
Property::Slot { slot_id } | Property::ConstSlot { slot_id } => {
body.row(18.0, |mut row| {
self.show_property(
object, messages, activation, &mut body, &name, ns, prop,
);
}
}
});
}
#[allow(clippy::too_many_arguments)]
fn show_property<'gc>(
&mut self,
object: Object<'gc>,
messages: &mut Vec<Message>,
activation: &mut Activation<'_, 'gc>,
body: &mut TableBody,
name: &str,
ns: Namespace<'gc>,
prop: Property,
) {
let label_col = |row: &mut TableRow| {
row.col(|ui| {
ui.label(&name).on_hover_ui(|ui| {
ui.label(name).on_hover_ui(|ui| {
ui.label(format!("{ns:?}"));
});
});
};
match prop {
Property::Slot { slot_id } | Property::ConstSlot { slot_id } => {
body.row(18.0, |mut row| {
label_col(&mut row);
row.col(|ui| {
let value = object.get_slot(slot_id);
ValueWidget::new(activation, value).show(ui, messages);
@ -110,13 +309,9 @@ impl Avm2ObjectWindow {
});
}
Property::Virtual { get: Some(get), .. } => {
let key = (ns.as_uri().to_string(), name.clone());
let key = (ns.as_uri().to_string(), name.to_string());
body.row(18.0, |mut row| {
row.col(|ui| {
ui.label(&name).on_hover_ui(|ui| {
ui.label(format!("{ns:?}"));
});
});
label_col(&mut row);
row.col(|ui| {
if self.call_getters {
let value = object.call_method(get, &[], activation);
@ -127,8 +322,7 @@ impl Avm2ObjectWindow {
// Empty entry means we want to refresh it,
// so let's do that now
let widget = value.get_or_insert_with(|| {
let value =
object.call_method(get, &[], activation);
let value = object.call_method(get, &[], activation);
ValueWidget::new(activation, value)
});
widget.show(ui, messages);
@ -145,9 +339,6 @@ impl Avm2ObjectWindow {
_ => {}
}
}
}
});
}
}
#[derive(Debug, Clone)]
@ -174,7 +365,7 @@ impl ValueWidget {
AVM2ObjectHandle::new(&mut activation.context, value),
object_name(activation.context.gc_context, value),
),
Err(e) => ValueWidget::Error(e.to_string()),
Err(e) => ValueWidget::Error(format!("{e:?}")),
}
}
@ -200,9 +391,18 @@ impl ValueWidget {
}
fn object_name<'gc>(mc: MutationContext<'gc, '_>, object: Object<'gc>) -> String {
if let Some(class) = object.as_class_object() {
class
.inner_class_definition()
.read()
.name()
.to_qualified_name_err_message(mc)
.to_string()
} else {
let name = object
.instance_of_class_definition()
.map(|r| Cow::Owned(r.read().name().to_qualified_name(mc).to_string()))
.map(|r| Cow::Owned(r.read().name().local_name().to_string()))
.unwrap_or(Cow::Borrowed("Object"));
format!("{} {:p}", name, object.as_ptr())
}
}

View File

@ -1,6 +1,6 @@
use crate::character::Character;
use crate::context::UpdateContext;
use crate::debug_ui::Message;
use crate::debug_ui::{ItemToSave, Message};
use crate::tag_utils::SwfMovie;
use egui::{CollapsingHeader, Grid, Id, TextEdit, Ui, Window};
use std::sync::Arc;
@ -25,6 +25,7 @@ impl MovieWindow {
egui_ctx: &egui::Context,
context: &mut UpdateContext,
movie: Arc<SwfMovie>,
messages: &mut Vec<Message>,
) -> bool {
let mut keep_open = true;
@ -49,7 +50,7 @@ impl MovieWindow {
ui.separator();
match self.open_panel {
Panel::Information => self.show_information(ui, &movie),
Panel::Information => self.show_information(ui, &movie, messages),
Panel::Characters => self.show_characters(ui, context, &movie),
}
});
@ -101,7 +102,24 @@ impl MovieWindow {
});
}
fn show_information(&mut self, ui: &mut Ui, movie: &Arc<SwfMovie>) {
fn show_information(
&mut self,
ui: &mut Ui,
movie: &Arc<SwfMovie>,
messages: &mut Vec<Message>,
) {
if !movie.data().is_empty() && ui.button("Save File...").clicked() {
let suggested_name = movie
.url()
.rsplit_once('.')
.map(|(_left, right)| right.to_string())
.unwrap_or_else(|| format!("{:p}.swf", Arc::as_ptr(movie)));
messages.push(Message::SaveFile(ItemToSave {
suggested_name,
data: movie.data().to_vec(),
}));
}
Grid::new(ui.id().with("information"))
.num_columns(2)
.show(ui, |ui| {

View File

@ -1870,8 +1870,8 @@ impl Player {
}
#[cfg(feature = "egui")]
pub fn debug_ui_message(&mut self, message: crate::debug_ui::Message) {
self.debug_ui.borrow_mut().queue_message(message)
pub fn debug_ui(&mut self) -> core::cell::RefMut<'_, crate::debug_ui::DebugUi> {
self.debug_ui.borrow_mut()
}
/// Update the current state of the player.

View File

@ -14,10 +14,12 @@ use chrono::DateTime;
use egui::*;
use fluent_templates::fluent_bundle::FluentValue;
use fluent_templates::{static_loader, Loader};
use rfd::FileDialog;
use ruffle_core::backend::ui::US_ENGLISH;
use ruffle_core::debug_ui::Message as DebugMessage;
use ruffle_core::Player;
use std::collections::HashMap;
use std::fs;
use sys_locale::get_locale;
use unic_langid::LanguageIdentifier;
use winit::event_loop::EventLoopProxy;
@ -123,6 +125,21 @@ impl RuffleGui {
if let Some(player) = player {
player.show_debug_ui(egui_ctx);
for item in player.debug_ui().items_to_save() {
std::thread::spawn(move || {
if let Some(path) = FileDialog::new()
.set_file_name(&item.suggested_name)
.save_file()
{
if let Err(e) = fs::write(&path, item.data) {
tracing::error!(
"Couldn't save {} to {path:?}: {e}",
item.suggested_name,
);
}
}
});
}
}
if !self.context_menu.is_empty() {
@ -240,13 +257,13 @@ impl RuffleGui {
if Button::new(text(&self.locale, "debug-menu-open-stage")).ui(ui).clicked() {
ui.close_menu();
if let Some(player) = &mut player {
player.debug_ui_message(DebugMessage::TrackStage);
player.debug_ui().queue_message(DebugMessage::TrackStage);
}
}
if Button::new(text(&self.locale, "debug-menu-open-movie")).ui(ui).clicked() {
ui.close_menu();
if let Some(player) = &mut player {
player.debug_ui_message(DebugMessage::TrackTopLevelMovie);
player.debug_ui().queue_message(DebugMessage::TrackTopLevelMovie);
}
}
});