xous-core/services/gam/src/lib.rs
bunnie aaf16dcf5e light refactor to the GAM to solve a deadlock issue
This is a "no good choice" situation but there is a problem with
progress bars.

You want progress bars to update quickly, and you don't want
to have to context switch out for the entire graphics stack to
come back to you to approve the drawing of your progress bar update.

On the other hand, you don't want to short-circuit all the checks
on drawing stuff to the screen for the sake of progress bars --
right now, there are a number of checks in place to make sure you
can't draw stuff on parts of the screen you shouldn't, to avoid
fake messages and dialog boxes.

I had managed to work out a way to get a dialog box to redraw without
violating the security rules -- each redraw is checked by the defacing
routine -- however, due to the way I was computing the height of
the modal I was causing the dialog box to be resized to its largest
size and then shrunk again to fit the text every iteration. This
in essence also caused the defacement routine to run repeatedly,
which caused the GAM to also issue repeated redraw requests to
the main loop.

These redraw requests can't be serviced until the task being tracked
by the progress bar is completed. As the progress bar gets redrawn
almost a hundred times as part of the task, roughly a hundred messages
arrive in the server's queue before it can handle a single one.

This results in a deadlock situation on completion of the progress
bar, because we want to fire off a message to ourselves to go to
the next state in the Ux routine.

There are a few possible solutions to this:

1. Multi thread the heck out of the routine doing the progress bar,
so the dialog box and the slider are each in their own threads
with their own responders and then the thing doing the computation
is doing some Mutex-guarded-Arc thing.

2. Do some very unsafe bodge to allow sliders to do direct draws
that are unchecked

3. Add a check so that if the size of the dialog box doesn't change,
defacement isn't recomputed and thus redraw messages aren't sent back.

(1) doesn't work for a number of reasons -- in a general case, it's
probably OK, but it does cause a lot more memory usage (and we are
lean on memory); but more importantly, for this use case, our routine
is interacting with CSRs and those don't implement Send so you can't
pass them into a thread via a closure. You could maybe put the dialog
boxes themselves into an Arc Mutex thing but because this routine
was heavily coded to re-use the heavyweight Ux resources this becomes
a bit cumersome to code.

(2) is terrible

(3) ended up working and what is in this commit. I think it's OK
because a progress bar generally doesn't require changing the
size of a window to update its state; therefore the redraw is
deface-safe (since you aren't changing the bounds of what you were
already approved to draw in). Of course, if you have some sort
of a progress bar implementation that changes the size of the messages
reported up top so that the dialog box has to change size over and
over again, you still end up in the deadlock situation!
2021-08-12 01:25:35 +08:00

392 lines
No EOL
17 KiB
Rust

#![cfg_attr(target_os = "none", no_std)]
//! Detailed docs are parked under Structs/Gam down below
pub mod api;
pub use api::*;
pub mod modal;
pub use modal::*;
pub mod menu;
pub use menu::*;
use graphics_server::api::{TextOp, TextView};
use graphics_server::api::{Point, Gid, Line, Rectangle, Circle, RoundedRectangle, TokenClaim};
pub use graphics_server::GlyphStyle;
use api::Opcode; // if you prefer to map the api into your local namespace
use xous::{send_message, CID, Message};
use xous_ipc::{String, Buffer};
use num_traits::*;
use ime_plugin_api::ImefCallback;
#[doc = include_str!("../README.md")]
#[derive(Debug)]
pub struct Gam {
/// The Gam structure exists on the client-side. This is the connection ID to the GAM server, local to this client.
conn: xous::CID,
/// A SID for callbacks from the GAM (e.g. redraw requests)
callback_sid: Option<xous::SID>,
}
impl Gam {
pub fn new(xns: &xous_names::XousNames) -> Result<Self, xous::Error> {
REFCOUNT.store(REFCOUNT.load(Ordering::Relaxed) + 1, Ordering::Relaxed);
let conn = xns.request_connection_blocking(api::SERVER_NAME_GAM).expect("Can't connect to GAM");
Ok(Gam {
conn,
callback_sid: None,
})
}
pub fn conn(&self) -> CID { self.conn }
pub fn getop_revert_focus(&self) -> u32 { // non-blocking version is handed out to the menu handler
Opcode::RevertFocusNb.to_u32().unwrap()
}
pub fn redraw(&self) -> Result<(), xous::Error> {
send_message(self.conn,
Message::new_scalar(Opcode::Redraw.to_usize().unwrap(), 0, 0, 0, 0)
).map(|_|())
}
pub fn powerdown_request(&self) -> Result<bool, xous::Error> {
let response = send_message(self.conn,
Message::new_blocking_scalar(Opcode::PowerDownRequest.to_usize().unwrap(), 0, 0, 0, 0))?;
if let xous::Result::Scalar1(confirmed) = response {
if confirmed != 0 {
Ok(true)
} else {
Ok(false)
}
} else {
panic!("GAM_API: unexpected return value: {:#?}", response);
}
}
pub fn shipmode_blank_request(&self) -> Result<bool, xous::Error> {
let response = send_message(self.conn,
Message::new_blocking_scalar(Opcode::ShipModeBlankRequest.to_usize().unwrap(), 0, 0, 0, 0))?;
if let xous::Result::Scalar1(confirmed) = response {
if confirmed != 0 {
Ok(true)
} else {
Ok(false)
}
} else {
panic!("GAM_API: unexpected return value: {:#?}", response);
}
}
/// this "posts" a textview -- it's not a "draw" as the update is neither guaranteed nor instantaneous
/// the GAM first has to check that the textview is allowed to be updated, and then it will decide when
/// the actual screen update is allowed
pub fn post_textview(&self, tv: &mut TextView) -> Result<(), xous::Error> {
tv.set_op(TextOp::Render);
// force the clip_rect to none, in case a stale value from a previous bounds computation was hanging out
// the bounds should /always/ come from the GAM canvas when doing a "live fire" redraw
tv.clip_rect = None;
let mut buf = Buffer::into_buf(tv.clone()).or(Err(xous::Error::InternalError))?;
buf.lend_mut(self.conn, Opcode::RenderTextView.to_u32().unwrap()).or(Err(xous::Error::InternalError))?;
match buf.to_original().unwrap() {
api::Return::RenderReturn(tvr) => {
tv.bounds_computed = tvr.bounds_computed;
tv.cursor = tvr.cursor;
}
api::Return::NotCurrentlyDrawable => {
tv.bounds_computed = None;
}
_ => panic!("GAM_API: post_textview got a return value from the server that isn't expected or handled")
}
tv.set_op(TextOp::Nop);
Ok(())
}
/// Bounds computation does no checks on security since it's a non-drawing operation. While normal drawing always
/// takes the bounds from the canvas, the caller can specify a clip_rect in this tv, instead of drawing the
/// clip_rect from the Canvas associated with the tv.
pub fn bounds_compute_textview(&self, tv: &mut TextView) -> Result<(), xous::Error> {
tv.set_op(TextOp::ComputeBounds);
let mut buf = Buffer::into_buf(tv.clone()).or(Err(xous::Error::InternalError))?;
buf.lend_mut(self.conn, Opcode::RenderTextView.to_u32().unwrap()).or(Err(xous::Error::InternalError))?;
match buf.to_original().unwrap() {
api::Return::RenderReturn(tvr) => {
tv.bounds_computed = tvr.bounds_computed;
tv.cursor = tvr.cursor;
}
_ => panic!("GAM_API: bounds_compute_textview got a return value from the server that isn't expected or handled")
}
tv.set_op(TextOp::Nop);
Ok(())
}
pub fn draw_line(&self, gid: Gid, line: Line) -> Result<(), xous::Error> {
let go = GamObject {
canvas: gid,
obj: GamObjectType::Line(line),
};
let buf = Buffer::into_buf(go).or(Err(xous::Error::InternalError))?;
buf.lend(self.conn, Opcode::RenderObject.to_u32().unwrap()).map(|_|())
}
pub fn draw_rectangle(&self, gid: Gid, rect: Rectangle) -> Result<(), xous::Error> {
let go = GamObject {
canvas: gid,
obj: GamObjectType::Rect(rect),
};
log::trace!("draw_rectangle: {:?}, conn: {}", go, self.conn);
let buf = Buffer::into_buf(go).or(Err(xous::Error::InternalError))?;
buf.lend(self.conn, Opcode::RenderObject.to_u32().unwrap()).map(|_|())
}
pub fn draw_rounded_rectangle(&self, gid: Gid, rr: RoundedRectangle) -> Result<(), xous::Error> {
let go = GamObject {
canvas: gid,
obj: GamObjectType::RoundRect(rr),
};
let buf = Buffer::into_buf(go).or(Err(xous::Error::InternalError))?;
buf.lend(self.conn, Opcode::RenderObject.to_u32().unwrap()).map(|_|())
}
pub fn draw_circle(&self, gid: Gid, circ: Circle) -> Result<(), xous::Error> {
let go = GamObject {
canvas: gid,
obj: GamObjectType::Circ(circ),
};
let buf = Buffer::into_buf(go).or(Err(xous::Error::InternalError))?;
buf.lend(self.conn, Opcode::RenderObject.to_u32().unwrap()).map(|_|())
}
pub fn get_canvas_bounds(&self, gid: Gid) -> Result<Point, xous::Error> {
log::trace!("GAM_API: get_canvas_bounds");
let response = send_message(self.conn,
Message::new_blocking_scalar(Opcode::GetCanvasBounds.to_usize().unwrap(),
gid.gid()[0] as _, gid.gid()[1] as _, gid.gid()[2] as _, gid.gid()[3] as _))
.expect("GAM_API: can't get canvas bounds from GAM");
if let xous::Result::Scalar2(tl, br) = response {
// note that the result should always be normalized so the rectangle's "tl" should be (0,0)
log::trace!("GAM_API: tl:{:?}, br:{:?}", Point::from(tl), Point::from(br));
assert!(tl == 0, "GAM_API: api call returned non-zero top left for canvas bounds");
Ok(br.into())
} else {
panic!("GAM_API: can't get canvas bounds")
}
}
pub fn set_canvas_bounds_request(&self, req: &mut SetCanvasBoundsRequest) -> Result<(), xous::Error> {
let mut buf = Buffer::into_buf(req.clone()).or(Err(xous::Error::InternalError))?;
buf.lend_mut(self.conn, Opcode::SetCanvasBounds.to_u32().unwrap()).or(Err(xous::Error::InternalError))?;
match buf.to_original().unwrap() {
api::Return::SetCanvasBoundsReturn(ret) => {
req.granted = ret.granted;
}
_ => panic!("GAM_API: set_canvas_bounds_request view got a return value from the server that isn't expected or handled")
}
Ok(())
}
pub fn request_content_canvas(&self, token: [u32; 4]) -> Result<Gid, xous::Error> {
let mut buf = Buffer::into_buf(token).or(Err(xous::Error::InternalError))?;
buf.lend_mut(self.conn, Opcode::RequestContentCanvas.to_u32().unwrap()).or(Err(xous::Error::InternalError))?;
match buf.to_original().unwrap() {
api::Return::ContentCanvasReturn(ret) => {
if let Some(gid) = ret {
Ok(gid)
} else {
Err(xous::Error::InternalError)
}
}
_ => {
log::error!("GAM_API: request_content_canvas got a return value from the server that isn't expected or handled");
Err(xous::Error::InternalError)
}
}
}
pub fn claim_token(&self, name: &str) -> Result<Option<[u32; 4]>, xous::Error> {
let tokenclaim = TokenClaim {
token: None,
name: String::<128>::from_str(name),
};
let mut buf = Buffer::into_buf(tokenclaim).or(Err(xous::Error::InternalError))?;
buf.lend_mut(self.conn, Opcode::ClaimToken.to_u32().unwrap()).or(Err(xous::Error::InternalError))?;
let returned_claim = buf.to_original::<TokenClaim, _>().unwrap();
Ok(returned_claim.token)
}
pub fn trusted_init_done(&self) -> Result<bool, xous::Error> {
let response = send_message(self.conn,
Message::new_blocking_scalar(Opcode::TrustedInitDone.to_usize().unwrap(), 0, 0, 0, 0)
).expect("couldn't run allow trusted code check");
if let xous::Result::Scalar1(result) = response {
if result == 1 {
Ok(true)
} else {
Ok(false)
}
} else {
Err(xous::Error::InternalError)
}
}
pub fn register_ux(&self, registration: UxRegistration) -> Result<Option<[u32; 4]>, xous::Error> {
let mut buf = Buffer::into_buf(registration).or(Err(xous::Error::InternalError))?;
buf.lend_mut(self.conn, Opcode::RegisterUx.to_u32().unwrap()).or(Err(xous::Error::InternalError))?;
match buf.to_original().unwrap() {
api::Return::UxToken(token) => {
Ok(token)
}
_ => {
Err(xous::Error::InternalError)
}
}
}
pub fn set_audio_opcode(&self, opcode: u32, token: [u32; 4]) -> Result<(), xous::Error> {
let audio_op = SetAudioOpcode {
token,
opcode,
};
let buf = Buffer::into_buf(audio_op).or(Err(xous::Error::InternalError))?;
buf.lend(self.conn, Opcode::SetAudioOpcode.to_u32().unwrap()).or(Err(xous::Error::InternalError)).map(|_| ())
}
pub fn set_vibe(&self, enable: bool) -> Result<(), xous::Error> {
let ena =
if enable { 1 }
else { 0 };
send_message(self.conn,
Message::new_scalar(Opcode::Vibe.to_usize().unwrap(),
ena, 0, 0, 0,)
).map(|_| ())
}
/// this indicates to the GAM that the currently running app no longer wants to be the focus of attention
/// we might respect that. or maybe not. depends on the GAM's policies.
pub fn relinquish_focus(&self) -> Result<(), xous::Error> {
send_message(self.conn,
Message::new_blocking_scalar(Opcode::RevertFocus.to_usize().unwrap(),
0, 0, 0, 0,)
).map(|_| ())
}
pub fn request_focus(&self, token: [u32; 4]) -> Result<(), xous::Error> {
send_message(self.conn,
Message::new_blocking_scalar(Opcode::RequestFocus.to_usize().unwrap(),
token[0] as usize, token[1] as usize, token[2] as usize, token[3] as usize,)
).map(|_| ())
}
pub fn glyph_height_hint(&self, glyph: GlyphStyle) -> Result<usize, xous::Error> {
let response = send_message(self.conn,
Message::new_blocking_scalar(Opcode::QueryGlyphProps.to_usize().unwrap(),
glyph as usize, 0, 0, 0,)
).expect("QueryGlyphProps failed");
if let xous::Result::Scalar1(h) = response {
Ok(h)
} else {
panic!("unexpected return value: {:#?}", response);
}
}
pub fn request_ime_redraw(&self) -> Result<(), xous::Error> {
send_message(self.conn,
Message::new_scalar(Opcode::RedrawIme.to_usize().unwrap(),
0, 0, 0, 0,)
).map(|_| ())
}
/// requests a context switch to a new app. Doesn't have to be obeyed, depending
/// upon the GAM's policy. Only the certain sources can switch apps, hence the token.
pub fn switch_to_app(&self, app_name: &str, token: [u32; 4]) -> Result<(), xous::Error> {
let switchapp = SwitchToApp {
token,
app_name: String::<128>::from_str(app_name),
};
let buf = Buffer::into_buf(switchapp).or(Err(xous::Error::InternalError))?;
buf.send(self.conn, Opcode::SwitchToApp.to_u32().unwrap()).or(Err(xous::Error::InternalError)).map(|_|())
}
pub fn raise_menu(&self, menu_name: &str) -> Result<(), xous::Error> {
let menu_name = String::<128>::from_str(menu_name);
let buf = Buffer::into_buf(menu_name).or(Err(xous::Error::InternalError))?;
buf.send(self.conn, Opcode::RaiseMenu.to_u32().unwrap()).or(Err(xous::Error::InternalError)).map(|_|())
}
pub fn raise_modal(&self, modal_name: &str) -> Result<(), xous::Error> {
self.raise_menu(modal_name)
}
/// this is a one-way door, once you've set it, you can't unset it.
pub fn set_devboot(&self, enable: bool) -> Result<(), xous::Error> {
let ena =
if enable { 1 }
else { 0 };
send_message(self.conn,
Message::new_scalar(Opcode::Devboot.to_usize().unwrap(),
ena, 0, 0, 0,)
).map(|_| ())
}
}
use core::sync::atomic::{AtomicU32, Ordering};
static REFCOUNT: AtomicU32 = AtomicU32::new(0);
impl Drop for Gam {
fn drop(&mut self) {
if let Some(sid) = self.callback_sid.take() {
// no need to tell the pstream server we're quitting: the next time a callback processes,
// it will automatically remove my entry as it will receive a ServerNotFound error.
// tell my handler thread to quit
let cid = xous::connect(sid).unwrap();
xous::send_message(cid,
Message::new_blocking_scalar(ImefCallback::Drop.to_usize().unwrap(), 0, 0, 0, 0)).unwrap();
unsafe{xous::disconnect(cid).unwrap();}
xous::destroy_server(sid).unwrap();
}
if REFCOUNT.load(Ordering::Relaxed) == 0 {
unsafe{xous::disconnect(self.conn).unwrap();}
}
}
}
// common message forwarding infrastructure used by Menus, Modals, etc...
#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
struct MsgForwarder {
pub public_sid: [u32; 4],
pub private_sid: [u32; 4],
pub redraw_op: u32,
pub rawkeys_op: u32,
pub drop_op: u32,
}
/// this is a simple server that forwards incoming messages from a generic
/// "modal" interface to the internal private server. It keeps the GAM from being
/// able to tinker with the internal mechanics of the larger server that owns the
/// dialog box.
fn forwarding_thread(addr: usize, size: usize, offset: usize) {
let buf = unsafe{Buffer::from_raw_parts(addr, size, offset)};
let forwarding_config = buf.to_original::<MsgForwarder, _>().unwrap();
let private_conn = xous::connect(xous::SID::from_array(forwarding_config.private_sid)).expect("couldn't connect to the private server");
log::trace!("modal forwarding server started");
loop {
let msg = xous::receive_message(xous::SID::from_array(forwarding_config.public_sid)).unwrap();
log::trace!("modal forwarding server got msg: {:?}", msg);
match FromPrimitive::from_usize(msg.body.id()) {
Some(ModalOpcode::Redraw) => {
xous::send_message(private_conn,
Message::new_scalar(forwarding_config.redraw_op as usize, 0, 0, 0, 0)
).expect("couldn't forward redraw message");
},
Some(ModalOpcode::Rawkeys) => xous::msg_scalar_unpack!(msg, k1, k2, k3, k4, {
xous::send_message(private_conn,
Message::new_scalar(forwarding_config.rawkeys_op as usize, k1, k2, k3, k4)
).expect("couldn't forard rawkeys message");
}),
Some(ModalOpcode::Quit) => {
xous::send_message(private_conn,
Message::new_scalar(forwarding_config.drop_op as usize, 0, 0, 0, 0)
).expect("couldn't forward drop message");
break;
},
None => {
log::error!("unknown opcode {:?}", msg.body.id());
}
}
}
log::trace!("modal forwarding server exiting");
xous::destroy_server(xous::SID::from_array(forwarding_config.public_sid)).expect("can't destroy my server on exit!");
}