Skip to content

Commit

Permalink
Render frames out of order on frontend
Browse files Browse the repository at this point in the history
  • Loading branch information
aidanhs committed Sep 13, 2021
1 parent afee6d4 commit 59950bb
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 37 deletions.
37 changes: 28 additions & 9 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ impl Default for RenderStatus {
#[derive(Debug)]
enum ClientState {
NeedsConfig,
NeedsFrameMeta(usize),
NeedsFrame(usize),
NeedsGifMeta,
NeedsGif,
Complete,
}
Expand Down Expand Up @@ -121,7 +123,13 @@ struct MyWs {
}

enum MyMsg {
Frame(Vec<u8>),
Meta(MetaMsg),
Binary(Vec<u8>),
}

enum MetaMsg {
Frame { index: usize },
Gif,
Reset(RenderJob),
}

Expand All @@ -134,12 +142,20 @@ impl Handler<MyMsg> for MyWs {

fn handle(&mut self, msg: MyMsg, ctx: &mut Self::Context) {
match msg {
MyMsg::Frame(d) => ctx.binary(d),
MyMsg::Reset(job) =>
MyMsg::Binary(d) => ctx.binary(d),
MyMsg::Meta(MetaMsg::Reset(job)) =>
ctx.text(serde_json::json!({
"job": job,
"job_fields": render_job_fields(),
}).to_string()),
MyMsg::Meta(MetaMsg::Frame { index }) =>
ctx.text(serde_json::json!({
"frame": index,
}).to_string()),
MyMsg::Meta(MetaMsg::Gif) =>
ctx.text(serde_json::json!({
"gif": null,
}).to_string()),
}
}
}
Expand Down Expand Up @@ -430,24 +446,27 @@ fn update_client(addr: &Addr<MyWs>, cs: &mut ClientState, render: &RenderStatus)
loop {
let (msg, next_cs) = match *cs {
// Send the config
ClientState::NeedsConfig => (MyMsg::Reset(render.job.clone()), ClientState::NeedsFrame(0)),
ClientState::NeedsConfig => (MyMsg::Meta(MetaMsg::Reset(render.job.clone())), ClientState::NeedsFrameMeta(0)),
// Wants more frames, but the frames are finished - move onto the gif
ClientState::NeedsFrame(i) if i == render.job.total_frames => {
*cs = ClientState::NeedsGif;
ClientState::NeedsFrameMeta(i) if i == render.job.total_frames => {
*cs = ClientState::NeedsGifMeta;
continue
},
// Wants more frames, but nothing to send yet
ClientState::NeedsFrame(i) if i == render.frames.len() => break,
ClientState::NeedsFrameMeta(i) if i == render.frames.len() => break,
// Send a frame
ClientState::NeedsFrame(i) => (MyMsg::Frame(render.frames[i].1.png.clone()), ClientState::NeedsFrame(i+1)),
ClientState::NeedsFrame(i) => (MyMsg::Binary(render.frames[i].1.png.clone()), ClientState::NeedsFrameMeta(i+1)),
ClientState::NeedsGif => {
match render.gif.as_ref() {
// Send the gif
Some(gif) => (MyMsg::Frame(gif.clone()), ClientState::Complete),
Some(gif) => (MyMsg::Binary(gif.clone()), ClientState::Complete),
// No gif available yet
None => break,
}
},
// If needs some meta, send it and move to the actual data
ClientState::NeedsFrameMeta(i) => (MyMsg::Meta(MetaMsg::Frame { index: render.frames[i].0 }), ClientState::NeedsFrame(i)),
ClientState::NeedsGifMeta => (MyMsg::Meta(MetaMsg::Gif), ClientState::NeedsGif),
// Client is up to date
ClientState::Complete => break,
};
Expand Down
87 changes: 59 additions & 28 deletions static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,15 @@
a.readAsDataURL(blob);
}

function numRenderedFrames(frames) {
return frames.reduce((count, f) => count + (f === null ? 0 : 1), 0);
}

function handleWSMessage(msg) {
if (typeof msg.data === 'string') {
// Strings on the websocket are JSON-formatted config messages that reset rendering
// Strings on the websocket are JSON-formatted config messages of one of the following variants
//
// VARIANT 1: resets rendering:
// {
// "job": { "field1": <value1>, ... },
// "job_fields": [
Expand All @@ -114,41 +120,66 @@
// ],
// }
// NOTE: must contain at least 'width', 'height' and 'total_frames'
//
// VARIANT 2: indicates the next binary message will be frame <index>
// {
// "frame": <index>,
// }
//
// VARIANT 3: indicates the next binary message will be the gif
// {
// "gif": null,
// }

let config = JSON.parse(msg.data);
let job_entry = {};
config.job_fields.forEach(([field, _type]) => {
job_entry[field] = this.state.job_entry[field] || '';
});
this.setState({
config,
nextFrame: 0,
frames: Array(config.job.total_frames).fill(null),
gif: null,
job_entry,
});
let metaMsg = JSON.parse(msg.data);
if (metaMsg.hasOwnProperty('job')) {
let job_entry = {};
metaMsg.job_fields.forEach(([field, _type]) => {
job_entry[field] = this.state.job_entry[field] || '';
});
this.setState({
config: metaMsg,
nextBinary: null,
frames: Array(metaMsg.job.total_frames).fill(null),
gif: null,
job_entry,
});
} else if (metaMsg.hasOwnProperty('frame')) {
this.setState({
nextBinary: { type: 'frame', index: metaMsg['frame'] },
});
} else if (metaMsg.hasOwnProperty('gif')) {
this.setState({
nextBinary: { type: 'gif' },
});
} else {
console.log('unknown meta msg:', metaMsg);
}

} else if (msg.data instanceof Blob) {
// Blobs are either a rendered frame or a gif

if (this.state.nextFrame === this.state.config.job.total_frames) {
// If we've already got all frames it must be the gif
blobToDataURL(msg.data, (gif) => this.setState({ gif }));

} else {
// Otherwise it's a frame

// Reserve a slot for this frame
let idx = this.state.nextFrame;
this.setState({ nextFrame: this.state.nextFrame + 1 });

// Convert to a data url and actually add to the slot when it's ready
if (typeof this.state.nextBinary !== 'object') {
console.log('not yet expecting a binary message');
} else if (this.state.nextBinary.type === 'frame') {
let idx = this.state.nextBinary.index;
// Convert to a data url and add to the slot when it's ready
blobToDataURL(msg.data, (frame) => {
let frames = this.state.frames.slice();
frames[idx] = frame;
this.setState({ frames });
});
} else if (this.state.nextBinary.type === 'gif') {
let renderedFrames = numRenderedFrames(this.state.frames);
if (renderedFrames !== this.state.config.job.total_frames) {
console.log('got a gif before we expected', renderedFrames, this.state.config.job.total_frames);
}
// If we've already got all frames it must be the gif
blobToDataURL(msg.data, (gif) => this.setState({ gif }));
} else {
console.log('unknown binary message type');
}
this.setState({ nextBinary: null });

} else {
console.log('unknown websocket msg', msg);
Expand All @@ -161,7 +192,7 @@
ws.onmessage = handleWSMessage.bind(this);
this.state = {
config: { job: { 'width': 1, 'height': 1, 'total_frames': 0 }, job_fields: [] },
nextFrame: 0,
nextBinary: null,
frames: [],
gif: null,
job_entry: {},
Expand Down Expand Up @@ -198,7 +229,7 @@
}

render() {
let { config, nextFrame, frames, gif, job_entry } = this.state;
let { config, frames, gif, job_entry } = this.state;

let params_display = <div>{config.job_fields.map(([field, type]) => {
let inner;
Expand Down Expand Up @@ -233,7 +264,7 @@
<h1>Hadean Renderer</h1>
{params_display}
<button onClick={this.handleClick.bind(this)}>Re-render</button>
<div>Rendered {nextFrame} of {config.job.total_frames} frames for {JSON.stringify(config.job)}</div>
<div>Rendered {numRenderedFrames(frames)} of {config.job.total_frames} frames for {JSON.stringify(config.job)}</div>
</div>
<div id="main"><img width={config.job.width} height={config.job.height} src={gif === null ? BLACK_PIXEL : gif}></img></div>
<div id="thumbs">{frames_display}</div>
Expand Down

0 comments on commit 59950bb

Please sign in to comment.