Add profiling with Remotery
This commit is contained in:
parent
c37be6798f
commit
6331a2bf79
50 changed files with 16864 additions and 11 deletions
218
vis/Code/Console.js
Normal file
218
vis/Code/Console.js
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
|
||||
Console = (function()
|
||||
{
|
||||
var BORDER = 10;
|
||||
var HEIGHT = 200;
|
||||
|
||||
|
||||
function Console(wm, server)
|
||||
{
|
||||
// Create the window and its controls
|
||||
this.Window = wm.AddWindow("Console", 10, 10, 100, 100);
|
||||
this.PageContainer = this.Window.AddControlNew(new WM.Container(10, 10, 400, 160));
|
||||
DOM.Node.AddClass(this.PageContainer.Node, "ConsoleText");
|
||||
this.AppContainer = this.Window.AddControlNew(new WM.Container(10, 10, 400, 160));
|
||||
DOM.Node.AddClass(this.AppContainer.Node, "ConsoleText");
|
||||
this.UserInput = this.Window.AddControlNew(new WM.EditBox(10, 5, 400, 30, "Input", ""));
|
||||
this.UserInput.SetChangeHandler(Bind(ProcessInput, this));
|
||||
this.Window.ShowNoAnim();
|
||||
|
||||
// This accumulates log text as fast as is required
|
||||
this.PageTextBuffer = "";
|
||||
this.PageTextUpdatePending = false;
|
||||
this.AppTextBuffer = "";
|
||||
this.AppTextUpdatePending = false;
|
||||
|
||||
// Setup command history control
|
||||
this.CommandHistory = LocalStore.Get("App", "Global", "CommandHistory", [ ]);
|
||||
this.CommandIndex = 0;
|
||||
this.MaxNbCommands = 10000;
|
||||
DOM.Event.AddHandler(this.UserInput.EditNode, "keydown", Bind(OnKeyPress, this));
|
||||
DOM.Event.AddHandler(this.UserInput.EditNode, "focus", Bind(OnFocus, this));
|
||||
|
||||
// At a much lower frequency this will update the console window
|
||||
window.setInterval(Bind(UpdateHTML, this), 500);
|
||||
|
||||
// Setup log requests from the server
|
||||
this.Server = server;
|
||||
server.SetConsole(this);
|
||||
server.AddMessageHandler("LOGM", Bind(OnLog, this));
|
||||
|
||||
this.Window.SetOnResize(Bind(OnUserResize, this));
|
||||
}
|
||||
|
||||
|
||||
Console.prototype.Log = function(text)
|
||||
{
|
||||
this.PageTextBuffer = LogText(this.PageTextBuffer, text);
|
||||
this.PageTextUpdatePending = true;
|
||||
}
|
||||
|
||||
|
||||
Console.prototype.WindowResized = function(width, height)
|
||||
{
|
||||
// Place window
|
||||
this.Window.SetPosition(BORDER, height - BORDER - 200);
|
||||
this.Window.SetSize(width - 2 * BORDER, HEIGHT);
|
||||
|
||||
ResizeInternals(this);
|
||||
}
|
||||
|
||||
|
||||
Console.prototype.TriggerUpdate = function()
|
||||
{
|
||||
this.AppTextUpdatePending = true;
|
||||
}
|
||||
|
||||
|
||||
function OnLog(self, socket, data_view_reader)
|
||||
{
|
||||
var text = data_view_reader.GetString();
|
||||
self.AppTextBuffer = LogText(self.AppTextBuffer, text);
|
||||
|
||||
// Don't register text as updating if disconnected as this implies a trace is being loaded, which we want to speed up
|
||||
if (self.Server.Connected())
|
||||
{
|
||||
self.AppTextUpdatePending = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function LogText(existing_text, new_text)
|
||||
{
|
||||
// Filter the text a little to make it safer
|
||||
if (new_text == null)
|
||||
new_text = "NULL";
|
||||
|
||||
// Find and convert any HTML entities, ensuring the browser doesn't parse any embedded HTML code
|
||||
// This also allows the log to contain arbitrary C++ code (e.g. assert comparison operators)
|
||||
new_text = Convert.string_to_html_entities(new_text);
|
||||
|
||||
// Prefix date and end with new line
|
||||
var d = new Date();
|
||||
new_text = "[" + d.toLocaleTimeString() + "] " + new_text + "<br>";
|
||||
|
||||
// Append to local text buffer and ensure clip the oldest text to ensure a max size
|
||||
existing_text = existing_text + new_text;
|
||||
var max_len = 100 * 1024;
|
||||
var len = existing_text.length;
|
||||
if (len > max_len)
|
||||
existing_text = existing_text.substr(len - max_len, max_len);
|
||||
|
||||
return existing_text;
|
||||
}
|
||||
|
||||
function OnUserResize(self, evt)
|
||||
{
|
||||
ResizeInternals(self);
|
||||
}
|
||||
|
||||
function ResizeInternals(self)
|
||||
{
|
||||
// Place controls
|
||||
var parent_size = self.Window.Size;
|
||||
var mid_w = parent_size[0] / 3;
|
||||
self.UserInput.SetPosition(BORDER, parent_size[1] - 2 * BORDER - 30);
|
||||
self.UserInput.SetSize(parent_size[0] - 100, 18);
|
||||
var output_height = self.UserInput.Position[1] - 2 * BORDER;
|
||||
self.PageContainer.SetPosition(BORDER, BORDER);
|
||||
self.PageContainer.SetSize(mid_w - 2 * BORDER, output_height);
|
||||
self.AppContainer.SetPosition(mid_w, BORDER);
|
||||
self.AppContainer.SetSize(parent_size[0] - mid_w - BORDER, output_height);
|
||||
}
|
||||
|
||||
|
||||
function UpdateHTML(self)
|
||||
{
|
||||
// Reset the current text buffer as html
|
||||
|
||||
if (self.PageTextUpdatePending)
|
||||
{
|
||||
var page_node = self.PageContainer.Node;
|
||||
page_node.innerHTML = self.PageTextBuffer;
|
||||
page_node.scrollTop = page_node.scrollHeight;
|
||||
self.PageTextUpdatePending = false;
|
||||
}
|
||||
|
||||
if (self.AppTextUpdatePending)
|
||||
{
|
||||
var app_node = self.AppContainer.Node;
|
||||
app_node.innerHTML = self.AppTextBuffer;
|
||||
app_node.scrollTop = app_node.scrollHeight;
|
||||
self.AppTextUpdatePending = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function ProcessInput(self, node)
|
||||
{
|
||||
// Send the message exactly
|
||||
var msg = node.value;
|
||||
self.Server.Send("CONI" + msg);
|
||||
|
||||
// Emit to console and clear
|
||||
self.Log("> " + msg);
|
||||
self.UserInput.SetValue("");
|
||||
|
||||
// Keep track of recently issued commands, with an upper bound
|
||||
self.CommandHistory.push(msg);
|
||||
var extra_commands = self.CommandHistory.length - self.MaxNbCommands;
|
||||
if (extra_commands > 0)
|
||||
self.CommandHistory.splice(0, extra_commands);
|
||||
|
||||
// Set command history index to the most recent command
|
||||
self.CommandIndex = self.CommandHistory.length;
|
||||
|
||||
// Backup to local store
|
||||
LocalStore.Set("App", "Global", "CommandHistory", self.CommandHistory);
|
||||
|
||||
// Keep focus with the edit box
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
function OnKeyPress(self, evt)
|
||||
{
|
||||
evt = DOM.Event.Get(evt);
|
||||
|
||||
if (evt.keyCode == Keyboard.Codes.UP)
|
||||
{
|
||||
if (self.CommandHistory.length > 0)
|
||||
{
|
||||
// Cycle backwards through the command history
|
||||
self.CommandIndex--;
|
||||
if (self.CommandIndex < 0)
|
||||
self.CommandIndex = self.CommandHistory.length - 1;
|
||||
var command = self.CommandHistory[self.CommandIndex];
|
||||
self.UserInput.SetValue(command);
|
||||
}
|
||||
|
||||
// Stops default behaviour of moving cursor to the beginning
|
||||
DOM.Event.StopDefaultAction(evt);
|
||||
}
|
||||
|
||||
else if (evt.keyCode == Keyboard.Codes.DOWN)
|
||||
{
|
||||
if (self.CommandHistory.length > 0)
|
||||
{
|
||||
// Cycle fowards through the command history
|
||||
self.CommandIndex = (self.CommandIndex + 1) % self.CommandHistory.length;
|
||||
var command = self.CommandHistory[self.CommandIndex];
|
||||
self.UserInput.SetValue(command);
|
||||
}
|
||||
|
||||
// Stops default behaviour of moving cursor to the end
|
||||
DOM.Event.StopDefaultAction(evt);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function OnFocus(self)
|
||||
{
|
||||
// Reset command index on focus
|
||||
self.CommandIndex = self.CommandHistory.length;
|
||||
}
|
||||
|
||||
|
||||
return Console;
|
||||
})();
|
||||
52
vis/Code/DataViewReader.js
Normal file
52
vis/Code/DataViewReader.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
|
||||
//
|
||||
// Simple wrapper around DataView that auto-advances the read offset and provides
|
||||
// a few common data type conversions specific to this app
|
||||
//
|
||||
DataViewReader = (function ()
|
||||
{
|
||||
function DataViewReader(data_view, offset)
|
||||
{
|
||||
this.DataView = data_view;
|
||||
this.Offset = offset;
|
||||
}
|
||||
|
||||
DataViewReader.prototype.AtEnd = function()
|
||||
{
|
||||
return this.Offset >= this.DataView.byteLength;
|
||||
}
|
||||
|
||||
DataViewReader.prototype.GetUInt32 = function ()
|
||||
{
|
||||
var v = this.DataView.getUint32(this.Offset, true);
|
||||
this.Offset += 4;
|
||||
return v;
|
||||
}
|
||||
|
||||
DataViewReader.prototype.GetUInt64 = function ()
|
||||
{
|
||||
var v = this.DataView.getFloat64(this.Offset, true);
|
||||
this.Offset += 8;
|
||||
return v;
|
||||
}
|
||||
|
||||
DataViewReader.prototype.GetStringOfLength = function (string_length)
|
||||
{
|
||||
var string = "";
|
||||
for (var i = 0; i < string_length; i++)
|
||||
{
|
||||
string += String.fromCharCode(this.DataView.getInt8(this.Offset));
|
||||
this.Offset++;
|
||||
}
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
DataViewReader.prototype.GetString = function ()
|
||||
{
|
||||
var string_length = this.GetUInt32();
|
||||
return this.GetStringOfLength(string_length);
|
||||
}
|
||||
|
||||
return DataViewReader;
|
||||
})();
|
||||
53
vis/Code/NameMap.js
Normal file
53
vis/Code/NameMap.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
class NameMap
|
||||
{
|
||||
constructor(text_buffer)
|
||||
{
|
||||
this.names = { };
|
||||
this.textBuffer = text_buffer;
|
||||
}
|
||||
|
||||
Get(name_hash)
|
||||
{
|
||||
// Return immediately if it's in the hash
|
||||
let name = this.names[name_hash];
|
||||
if (name != undefined)
|
||||
{
|
||||
return [ true, name ];
|
||||
}
|
||||
|
||||
// Create a temporary name that uses the hash
|
||||
name = {
|
||||
string: name_hash.toString(),
|
||||
hash: name_hash
|
||||
};
|
||||
this.names[name_hash] = name;
|
||||
|
||||
// Add to the text buffer the first time this name is encountered
|
||||
name.textEntry = this.textBuffer.AddText(name.string);
|
||||
|
||||
return [ false, name ];
|
||||
}
|
||||
|
||||
Set(name_hash, name_string)
|
||||
{
|
||||
// Create the name on-demand if its hash doesn't exist
|
||||
let name = this.names[name_hash];
|
||||
if (name == undefined)
|
||||
{
|
||||
name = {
|
||||
string: name_string,
|
||||
hash: name_hash
|
||||
};
|
||||
this.names[name_hash] = name;
|
||||
}
|
||||
else
|
||||
{
|
||||
name.string = name_string;
|
||||
}
|
||||
|
||||
// Apply the updated text to the buffer
|
||||
name.textEntry = this.textBuffer.AddText(name_string);
|
||||
|
||||
return name;
|
||||
}
|
||||
}
|
||||
61
vis/Code/PixelTimeRange.js
Normal file
61
vis/Code/PixelTimeRange.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
|
||||
class PixelTimeRange
|
||||
{
|
||||
constructor(start_us, span_us, span_px)
|
||||
{
|
||||
this.Span_px = span_px;
|
||||
this.Set(start_us, span_us);
|
||||
}
|
||||
|
||||
Set(start_us, span_us)
|
||||
{
|
||||
this.Start_us = start_us;
|
||||
this.Span_us = span_us;
|
||||
this.End_us = this.Start_us + span_us;
|
||||
this.usPerPixel = this.Span_px / this.Span_us;
|
||||
}
|
||||
|
||||
SetStart(start_us)
|
||||
{
|
||||
this.Start_us = start_us;
|
||||
this.End_us = start_us + this.Span_us;
|
||||
}
|
||||
|
||||
SetEnd(end_us)
|
||||
{
|
||||
this.End_us = end_us;
|
||||
this.Start_us = end_us - this.Span_us;
|
||||
}
|
||||
|
||||
SetPixelSpan(span_px)
|
||||
{
|
||||
this.Span_px = span_px;
|
||||
this.usPerPixel = this.Span_px / this.Span_us;
|
||||
}
|
||||
|
||||
PixelOffset(time_us)
|
||||
{
|
||||
return Math.floor((time_us - this.Start_us) * this.usPerPixel);
|
||||
}
|
||||
|
||||
PixelSize(time_us)
|
||||
{
|
||||
return Math.floor(time_us * this.usPerPixel);
|
||||
}
|
||||
|
||||
TimeAtPosition(position)
|
||||
{
|
||||
return this.Start_us + position / this.usPerPixel;
|
||||
}
|
||||
|
||||
Clone()
|
||||
{
|
||||
return new PixelTimeRange(this.Start_us, this.Span_us, this.Span_px);
|
||||
}
|
||||
|
||||
SetAsUniform(gl, program)
|
||||
{
|
||||
glSetUniform(gl, program, "inTimeRange.usStart", this.Start_us);
|
||||
glSetUniform(gl, program, "inTimeRange.usPerPixel", this.usPerPixel);
|
||||
}
|
||||
}
|
||||
540
vis/Code/Remotery.js
Normal file
540
vis/Code/Remotery.js
Normal file
|
|
@ -0,0 +1,540 @@
|
|||
|
||||
//
|
||||
// TODO: Window resizing needs finer-grain control
|
||||
// TODO: Take into account where user has moved the windows
|
||||
// TODO: Controls need automatic resizing within their parent windows
|
||||
//
|
||||
|
||||
|
||||
Settings = (function()
|
||||
{
|
||||
function Settings()
|
||||
{
|
||||
this.IsPaused = false;
|
||||
this.SyncTimelines = true;
|
||||
}
|
||||
|
||||
return Settings;
|
||||
|
||||
})();
|
||||
|
||||
|
||||
Remotery = (function()
|
||||
{
|
||||
// crack the url and get the parameter we want
|
||||
var getUrlParameter = function getUrlParameter( search_param)
|
||||
{
|
||||
var page_url = decodeURIComponent( window.location.search.substring(1) ),
|
||||
url_vars = page_url.split('&'),
|
||||
param_name,
|
||||
i;
|
||||
|
||||
for (i = 0; i < url_vars.length; i++)
|
||||
{
|
||||
param_name = url_vars[i].split('=');
|
||||
|
||||
if (param_name[0] === search_param)
|
||||
{
|
||||
return param_name[1] === undefined ? true : param_name[1];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function Remotery()
|
||||
{
|
||||
this.WindowManager = new WM.WindowManager();
|
||||
this.Settings = new Settings();
|
||||
|
||||
// "addr" param is ip:port and will override the local store version if passed in the URL
|
||||
var addr = getUrlParameter( "addr" );
|
||||
if ( addr != null )
|
||||
this.ConnectionAddress = "ws://" + addr + "/rmt";
|
||||
else
|
||||
this.ConnectionAddress = LocalStore.Get("App", "Global", "ConnectionAddress", "ws://127.0.0.1:17815/rmt");
|
||||
|
||||
this.Server = new WebSocketConnection();
|
||||
this.Server.AddConnectHandler(Bind(OnConnect, this));
|
||||
this.Server.AddDisconnectHandler(Bind(OnDisconnect, this));
|
||||
|
||||
// Create the console up front as everything reports to it
|
||||
this.Console = new Console(this.WindowManager, this.Server);
|
||||
|
||||
// Create required windows
|
||||
this.TitleWindow = new TitleWindow(this.WindowManager, this.Settings, this.Server, this.ConnectionAddress);
|
||||
this.TitleWindow.SetConnectionAddressChanged(Bind(OnAddressChanged, this));
|
||||
this.SampleTimelineWindow = new TimelineWindow(this.WindowManager, "Sample Timeline", this.Settings, Bind(OnTimelineCheck, this));
|
||||
this.SampleTimelineWindow.SetOnHover(Bind(OnSampleHover, this));
|
||||
this.SampleTimelineWindow.SetOnSelected(Bind(OnSampleSelected, this));
|
||||
this.ProcessorTimelineWindow = new TimelineWindow(this.WindowManager, "Processor Timeline", this.Settings, null);
|
||||
|
||||
this.SampleTimelineWindow.SetOnMoved(Bind(OnTimelineMoved, this));
|
||||
this.ProcessorTimelineWindow.SetOnMoved(Bind(OnTimelineMoved, this));
|
||||
|
||||
this.TraceDrop = new TraceDrop(this);
|
||||
|
||||
this.NbSampleWindows = 0;
|
||||
this.SampleWindows = { };
|
||||
this.FrameHistory = { };
|
||||
this.ProcessorFrameHistory = { };
|
||||
this.SelectedFrames = { };
|
||||
this.sampleNames = new NameMap(this.SampleTimelineWindow.textBuffer);
|
||||
this.threadNames = new NameMap(this.ProcessorTimelineWindow.textBuffer);
|
||||
|
||||
this.Server.AddMessageHandler("SMPL", Bind(OnSamples, this));
|
||||
this.Server.AddMessageHandler("SSMP", Bind(OnSampleName, this));
|
||||
this.Server.AddMessageHandler("PRTH", Bind(OnProcessorThreads, this));
|
||||
this.Server.AddMessageHandler("THRN", Bind(OnThreadNames, this));
|
||||
|
||||
// Kick-off the auto-connect loop
|
||||
AutoConnect(this);
|
||||
|
||||
// Hook up resize event handler
|
||||
DOM.Event.AddHandler(window, "resize", Bind(OnResizeWindow, this));
|
||||
OnResizeWindow(this);
|
||||
|
||||
// Hook up browser-native canvas refresh
|
||||
this.DisplayFrame = 0;
|
||||
this.LastKnownPause = this.Settings.IsPaused;
|
||||
var self = this;
|
||||
(function display_loop()
|
||||
{
|
||||
window.requestAnimationFrame(display_loop);
|
||||
DrawTimeline(self);
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
Remotery.prototype.Clear = function()
|
||||
{
|
||||
// Clear timelines
|
||||
this.SampleTimelineWindow.Clear();
|
||||
this.ProcessorTimelineWindow.Clear();
|
||||
|
||||
// Close and clear all sample windows
|
||||
for (var i in this.SampleWindows)
|
||||
{
|
||||
var sample_window = this.SampleWindows[i];
|
||||
sample_window.Close();
|
||||
}
|
||||
this.NbSampleWindows = 0;
|
||||
this.SampleWindows = { };
|
||||
|
||||
// Clear runtime data
|
||||
this.FrameHistory = { };
|
||||
this.ProcessorFrameHistory = { };
|
||||
this.SelectedFrames = { };
|
||||
this.sampleNames = new NameMap(this.SampleTimelineWindow.textBuffer);
|
||||
this.threadNames = new NameMap(this.ProcessorTimelineWindow.textBuffer);
|
||||
|
||||
// Resize everything to fit new layout
|
||||
OnResizeWindow(this);
|
||||
}
|
||||
|
||||
|
||||
function AutoConnect(self)
|
||||
{
|
||||
// Only attempt to connect if there isn't already a connection or an attempt to connect
|
||||
if (!self.Server.Connected())
|
||||
self.Server.Connect(self.ConnectionAddress);
|
||||
|
||||
// Always schedule another check
|
||||
window.setTimeout(Bind(AutoConnect, self), 2000);
|
||||
}
|
||||
|
||||
|
||||
function OnConnect(self)
|
||||
{
|
||||
// Connection address has been validated
|
||||
LocalStore.Set("App", "Global", "ConnectionAddress", self.ConnectionAddress);
|
||||
|
||||
self.Clear();
|
||||
|
||||
// Ensure the viewer is ready for realtime updates
|
||||
self.TitleWindow.Unpause();
|
||||
}
|
||||
|
||||
function OnDisconnect(self)
|
||||
{
|
||||
// Pause so the user can inspect the trace
|
||||
self.TitleWindow.Pause();
|
||||
}
|
||||
|
||||
|
||||
function OnAddressChanged(self, node)
|
||||
{
|
||||
// Update and disconnect, relying on auto-connect to reconnect
|
||||
self.ConnectionAddress = node.value;
|
||||
self.Server.Disconnect();
|
||||
|
||||
// Give input focus away
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
function DrawTimeline(self)
|
||||
{
|
||||
// Has pause state changed?
|
||||
if (self.Settings.IsPaused != self.LastKnownPaused)
|
||||
{
|
||||
// When switching TO paused, draw one last frame to ensure the sample text gets drawn
|
||||
self.LastKnownPaused = self.Settings.IsPaused;
|
||||
self.SampleTimelineWindow.DrawAllRows();
|
||||
self.ProcessorTimelineWindow.DrawAllRows();
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't waste time drawing the timeline when paused
|
||||
if (self.Settings.IsPaused)
|
||||
return;
|
||||
|
||||
// requestAnimationFrame can run up to 60hz which is way too much for drawing the timeline
|
||||
// Assume it's running at 60hz and skip frames to achieve 10hz instead
|
||||
// Doing this instead of using setTimeout because it's better for browser rendering (or; will be once WebGL is in use)
|
||||
// TODO: Expose as config variable because high refresh rate is great when using a separate viewiing machine
|
||||
if ((self.DisplayFrame % 10) == 0)
|
||||
{
|
||||
self.SampleTimelineWindow.DrawAllRows();
|
||||
self.ProcessorTimelineWindow.DrawAllRows();
|
||||
}
|
||||
|
||||
self.DisplayFrame++;
|
||||
}
|
||||
|
||||
|
||||
function DecodeSample(self, data_view_reader)
|
||||
{
|
||||
var sample = {};
|
||||
|
||||
// Get name hash and lookup name it map
|
||||
sample.name_hash = data_view_reader.GetUInt32();
|
||||
let [ name_exists, name ] = self.sampleNames.Get(sample.name_hash);
|
||||
sample.name = name;
|
||||
|
||||
// If the name doesn't exist in the map yet, request it from the server
|
||||
if (!name_exists)
|
||||
{
|
||||
if (self.Server.Connected())
|
||||
{
|
||||
self.Server.Send("GSMP" + sample.name_hash);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the rest of the sample data
|
||||
sample.id = data_view_reader.GetUInt32();
|
||||
sample.colour = data_view_reader.GetStringOfLength(7);
|
||||
sample.us_start = data_view_reader.GetUInt64();
|
||||
sample.us_length = data_view_reader.GetUInt64();
|
||||
sample.us_self = data_view_reader.GetUInt64();
|
||||
sample.call_count = data_view_reader.GetUInt32();
|
||||
sample.recurse_depth = data_view_reader.GetUInt32();
|
||||
|
||||
// TODO(don): Get the profiler to pass these directly instead of hex colour
|
||||
const colour = parseInt(sample.colour.slice(1), 16);
|
||||
const r = (colour >> 16) & 255;
|
||||
const g = (colour >> 8) & 255;
|
||||
const b = colour & 255;
|
||||
sample.rgbColour = [ r, g, b ];
|
||||
|
||||
// Calculate dependent properties
|
||||
sample.ms_length = (sample.us_length / 1000.0).toFixed(3);
|
||||
sample.ms_self = (sample.us_self / 1000.0).toFixed(3);
|
||||
|
||||
// Recurse into children
|
||||
sample.children = [];
|
||||
DecodeSampleArray(self, data_view_reader, sample.children);
|
||||
|
||||
return sample;
|
||||
}
|
||||
|
||||
|
||||
function DecodeSampleArray(self, data_view_reader, samples)
|
||||
{
|
||||
var nb_samples = data_view_reader.GetUInt32();
|
||||
for (var i = 0; i < nb_samples; i++)
|
||||
{
|
||||
var sample = DecodeSample(self, data_view_reader);
|
||||
samples.push(sample)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function DecodeSamples(self, data_view_reader)
|
||||
{
|
||||
// Message-specific header
|
||||
let message = { };
|
||||
message.sample_tree_bytes = data_view_reader.GetUInt32();
|
||||
message.thread_name = data_view_reader.GetString();
|
||||
message.nb_samples = data_view_reader.GetUInt32();
|
||||
message.sample_digest = data_view_reader.GetUInt32();
|
||||
message.partial_tree = data_view_reader.GetUInt32();
|
||||
|
||||
// Read samples
|
||||
message.samples = [];
|
||||
message.samples.push(DecodeSample(self, data_view_reader));
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
|
||||
function OnSamples(self, socket, data_view_reader)
|
||||
{
|
||||
// Discard any new samples while paused and connected
|
||||
// Otherwise this stops a paused Remotery from loading new samples from disk
|
||||
if (self.Settings.IsPaused && self.Server.Connected())
|
||||
return;
|
||||
|
||||
// Binary decode incoming sample data
|
||||
var message = DecodeSamples(self, data_view_reader);
|
||||
var name = message.thread_name;
|
||||
|
||||
// Add to frame history for this thread
|
||||
var thread_frame = new ThreadFrame(message);
|
||||
if (!(name in self.FrameHistory))
|
||||
{
|
||||
self.FrameHistory[name] = [ ];
|
||||
}
|
||||
var frame_history = self.FrameHistory[name];
|
||||
if (frame_history.length > 0 && frame_history[frame_history.length - 1].PartialTree)
|
||||
{
|
||||
// Always overwrite partial trees with new information
|
||||
frame_history[frame_history.length - 1] = thread_frame;
|
||||
}
|
||||
else
|
||||
{
|
||||
frame_history.push(thread_frame);
|
||||
}
|
||||
|
||||
// Discard old frames to keep memory-use constant
|
||||
var max_nb_frames = 10000;
|
||||
var extra_frames = frame_history.length - max_nb_frames;
|
||||
if (extra_frames > 0)
|
||||
frame_history.splice(0, extra_frames);
|
||||
|
||||
// Create sample windows on-demand
|
||||
if (!(name in self.SampleWindows))
|
||||
{
|
||||
self.SampleWindows[name] = new SampleWindow(self.WindowManager, name, self.NbSampleWindows);
|
||||
self.SampleWindows[name].WindowResized(self.SampleTimelineWindow.Window, self.Console.Window);
|
||||
self.NbSampleWindows++;
|
||||
MoveSampleWindows(this);
|
||||
}
|
||||
|
||||
// Set on the window and timeline if connected as this implies a trace is being loaded, which we want to speed up
|
||||
if (self.Server.Connected())
|
||||
{
|
||||
self.SampleWindows[name].OnSamples(message.nb_samples, message.sample_digest, message.samples);
|
||||
self.SampleTimelineWindow.OnSamples(name, frame_history);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function OnSampleName(self, socket, data_view_reader)
|
||||
{
|
||||
// Add any names sent by the server to the local map
|
||||
let name_hash = data_view_reader.GetUInt32();
|
||||
let name_string = data_view_reader.GetString();
|
||||
self.sampleNames.Set(name_hash, name_string);
|
||||
}
|
||||
|
||||
|
||||
function OnProcessorThreads(self, socket, data_view_reader)
|
||||
{
|
||||
let nb_processors = data_view_reader.GetUInt32();
|
||||
let message_index = data_view_reader.GetUInt64();
|
||||
|
||||
// Decode each processor
|
||||
for (let i = 0; i < nb_processors; i++)
|
||||
{
|
||||
let thread_id = data_view_reader.GetUInt32();
|
||||
let thread_name_hash = data_view_reader.GetUInt32();
|
||||
let sample_time = data_view_reader.GetUInt64();
|
||||
|
||||
// Add frame history for this processor
|
||||
let processor_name = "Processor " + i.toString();
|
||||
if (!(processor_name in self.ProcessorFrameHistory))
|
||||
{
|
||||
self.ProcessorFrameHistory[processor_name] = [ ];
|
||||
}
|
||||
let frame_history = self.ProcessorFrameHistory[processor_name];
|
||||
|
||||
if (thread_id == 0xFFFFFFFF)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to merge this frame's samples with the previous frame if the are the same thread
|
||||
if (frame_history.length > 0)
|
||||
{
|
||||
let last_thread_frame = frame_history[frame_history.length - 1];
|
||||
if (last_thread_frame.threadId == thread_id && last_thread_frame.messageIndex == message_index - 1)
|
||||
{
|
||||
// Update last frame message index so that the next frame can check for continuity
|
||||
last_thread_frame.messageIndex = message_index;
|
||||
|
||||
// Sum time elapsed on the previous frame
|
||||
let us_length = sample_time - last_thread_frame.usLastStart;
|
||||
last_thread_frame.usLastStart = sample_time;
|
||||
last_thread_frame.EndTime_us += us_length;
|
||||
last_thread_frame.Samples[0].us_length += us_length;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Discard old frames to keep memory-use constant
|
||||
var max_nb_frames = 10000;
|
||||
var extra_frames = frame_history.length - max_nb_frames;
|
||||
if (extra_frames > 0)
|
||||
{
|
||||
frame_history.splice(0, extra_frames);
|
||||
}
|
||||
|
||||
// Lookup the thread name
|
||||
let [ _, thread_name ] = self.threadNames.Get(thread_name_hash);
|
||||
|
||||
// Make a pastel-y colour from the thread name hash
|
||||
let hash = thread_name.hash;
|
||||
let r = 127 + (hash & 255) / 2;
|
||||
let g = 127 + ((hash >> 4) & 255) / 2;
|
||||
let b = 127 + ((hash >> 8) & 255) / 2;
|
||||
|
||||
// We are co-opting the sample rendering functionality of the timeline window to display processor threads as
|
||||
// thread samples. Fabricate a thread frame message, packing the processor info into one root sample.
|
||||
// TODO(don): Abstract the timeline window for pure range display as this is quite inefficient.
|
||||
let thread_message = {
|
||||
nb_samples: 1,
|
||||
sample_digest: 0,
|
||||
samples : [
|
||||
{
|
||||
name_hash: thread_name_hash,
|
||||
name: thread_name,
|
||||
id: thread_id,
|
||||
colour: "#FFFFFF",
|
||||
us_start: sample_time,
|
||||
us_length: 250,
|
||||
rgbColour: [ r, g, b ],
|
||||
children: []
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Create a thread frame and annotate with data required to merge processor samples
|
||||
let thread_frame = new ThreadFrame(thread_message);
|
||||
thread_frame.threadId = thread_id;
|
||||
thread_frame.messageIndex = message_index;
|
||||
thread_frame.usLastStart = sample_time;
|
||||
frame_history.push(thread_frame);
|
||||
|
||||
if (self.Server.Connected())
|
||||
{
|
||||
self.ProcessorTimelineWindow.OnSamples(processor_name, frame_history);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function OnThreadNames(self, socket, data_view_reader)
|
||||
{
|
||||
let name_hash = data_view_reader.GetUInt32();
|
||||
let name_length = data_view_reader.GetUInt32();
|
||||
let name_string = data_view_reader.GetStringOfLength(name_length);
|
||||
self.threadNames.Set(name_hash, name_string);
|
||||
}
|
||||
|
||||
|
||||
function OnTimelineCheck(self, name, evt)
|
||||
{
|
||||
// Show/hide the equivalent sample window and move all the others to occupy any left-over space
|
||||
var target = DOM.Event.GetNode(evt);
|
||||
self.SampleWindows[name].SetVisible(target.checked);
|
||||
MoveSampleWindows(self);
|
||||
}
|
||||
|
||||
|
||||
function MoveSampleWindows(self)
|
||||
{
|
||||
// Stack all windows next to each other
|
||||
var xpos = 0;
|
||||
for (var i in self.SampleWindows)
|
||||
{
|
||||
var sample_window = self.SampleWindows[i];
|
||||
if (sample_window.Visible)
|
||||
{
|
||||
sample_window.SetXPos(xpos++, self.SampleTimelineWindow.Window, self.Console.Window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function OnSampleHover(self, thread_name, hover)
|
||||
{
|
||||
if (!self.Settings.IsPaused)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (let window_thread_name in self.SampleWindows)
|
||||
{
|
||||
let sample_window = self.SampleWindows[window_thread_name];
|
||||
|
||||
if (window_thread_name == thread_name && hover != null)
|
||||
{
|
||||
// Populate with sample under hover
|
||||
let frame = hover[0];
|
||||
sample_window.OnSamples(frame.NbSamples, frame.SampleDigest, frame.Samples);
|
||||
}
|
||||
else
|
||||
{
|
||||
// When there's no hover, go back to the selected frame
|
||||
if (self.SelectedFrames[window_thread_name])
|
||||
{
|
||||
const frame = self.SelectedFrames[window_thread_name];
|
||||
sample_window.OnSamples(frame.NbSamples, frame.SampleDigest, frame.Samples);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function OnSampleSelected(self, thread_name, select)
|
||||
{
|
||||
// Lookup sample window set the frame samples on it
|
||||
if (select && thread_name in self.SampleWindows)
|
||||
{
|
||||
var sample_window = self.SampleWindows[thread_name];
|
||||
var frame = select[0];
|
||||
self.SelectedFrames[thread_name] = frame;
|
||||
sample_window.OnSamples(frame.NbSamples, frame.SampleDigest, frame.Samples);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function OnResizeWindow(self)
|
||||
{
|
||||
// Resize windows
|
||||
var w = window.innerWidth;
|
||||
var h = window.innerHeight;
|
||||
self.Console.WindowResized(w, h);
|
||||
self.TitleWindow.WindowResized(w, h);
|
||||
self.SampleTimelineWindow.WindowResized(10, w / 2 - 5, self.TitleWindow.Window);
|
||||
self.ProcessorTimelineWindow.WindowResized(w / 2 + 5, w / 2 - 5, self.TitleWindow.Window);
|
||||
for (var i in self.SampleWindows)
|
||||
{
|
||||
self.SampleWindows[i].WindowResized(self.SampleTimelineWindow.Window, self.Console.Window);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function OnTimelineMoved(self, timeline)
|
||||
{
|
||||
if (self.Settings.SyncTimelines)
|
||||
{
|
||||
let other_timeline = timeline == self.ProcessorTimelineWindow ? self.SampleTimelineWindow : self.ProcessorTimelineWindow;
|
||||
other_timeline.SetTimeRange(timeline.TimeRange.Start_us, timeline.TimeRange.Span_us);
|
||||
other_timeline.DrawAllRows();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return Remotery;
|
||||
})();
|
||||
221
vis/Code/SampleWindow.js
Normal file
221
vis/Code/SampleWindow.js
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
|
||||
SampleWindow = (function()
|
||||
{
|
||||
function SampleWindow(wm, name, offset)
|
||||
{
|
||||
// Sample digest for checking if grid needs to be repopulated
|
||||
this.NbSamples = 0;
|
||||
this.SampleDigest = null;
|
||||
|
||||
// Source sample reference to reduce repopulation
|
||||
this.Samples = null;
|
||||
|
||||
this.XPos = 10 + offset * 410;
|
||||
this.Window = wm.AddWindow(name, 100, 100, 100, 100);
|
||||
this.Window.ShowNoAnim();
|
||||
this.Visible = true;
|
||||
|
||||
// Create a grid that's indexed by the unique sample ID
|
||||
this.Grid = this.Window.AddControlNew(new WM.Grid());
|
||||
var cell_data =
|
||||
{
|
||||
Name: "Samples",
|
||||
Length: "Time (ms)",
|
||||
Self: "Self (ms)",
|
||||
Calls: "Calls",
|
||||
Recurse: "Recurse",
|
||||
};
|
||||
var cell_classes =
|
||||
{
|
||||
Name: "SampleTitleNameCell",
|
||||
Length: "SampleTitleTimeCell",
|
||||
Self: "SampleTitleTimeCell",
|
||||
Calls: "SampleTitleCountCell",
|
||||
Recurse: "SampleTitleCountCell",
|
||||
};
|
||||
this.RootRow = this.Grid.Rows.Add(cell_data, "GridGroup", cell_classes);
|
||||
this.RootRow.Rows.AddIndex("_ID");
|
||||
}
|
||||
|
||||
|
||||
SampleWindow.prototype.Close = function()
|
||||
{
|
||||
this.Window.Close();
|
||||
}
|
||||
|
||||
|
||||
SampleWindow.prototype.SetXPos = function(xpos, top_window, bottom_window)
|
||||
{
|
||||
Anim.Animate(
|
||||
Bind(AnimatedMove, this, top_window, bottom_window),
|
||||
this.XPos, 10 + xpos * 410, 0.25);
|
||||
}
|
||||
|
||||
|
||||
function AnimatedMove(self, top_window, bottom_window, val)
|
||||
{
|
||||
self.XPos = val;
|
||||
self.WindowResized(top_window, bottom_window);
|
||||
}
|
||||
|
||||
|
||||
SampleWindow.prototype.SetVisible = function(visible)
|
||||
{
|
||||
if (visible != this.Visible)
|
||||
{
|
||||
if (visible == true)
|
||||
this.Window.ShowNoAnim();
|
||||
else
|
||||
this.Window.HideNoAnim();
|
||||
|
||||
this.Visible = visible;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
SampleWindow.prototype.WindowResized = function(top_window, bottom_window)
|
||||
{
|
||||
var top = top_window.Position[1] + top_window.Size[1] + 10;
|
||||
this.Window.SetPosition(this.XPos, top_window.Position[1] + top_window.Size[1] + 10);
|
||||
this.Window.SetSize(400, bottom_window.Position[1] - 10 - top);
|
||||
}
|
||||
|
||||
|
||||
SampleWindow.prototype.OnSamples = function(nb_samples, sample_digest, samples)
|
||||
{
|
||||
if (!this.Visible)
|
||||
return;
|
||||
|
||||
// If the source hasn't changed, don't repopulate
|
||||
if (this.Samples == samples)
|
||||
return;
|
||||
this.Samples = samples;
|
||||
|
||||
// Recreate all the HTML if the number of samples gets bigger
|
||||
if (nb_samples > this.NbSamples)
|
||||
{
|
||||
GrowGrid(this.RootRow, nb_samples);
|
||||
this.NbSamples = nb_samples;
|
||||
}
|
||||
|
||||
// If the content of the samples changes from previous update, update them all
|
||||
if (this.SampleDigest != sample_digest)
|
||||
{
|
||||
this.RootRow.Rows.ClearIndex("_ID");
|
||||
var index = UpdateAllSampleFields(this.RootRow, samples, 0, "");
|
||||
this.SampleDigest = sample_digest;
|
||||
|
||||
// Clear out any left-over rows
|
||||
for (var i = index; i < this.RootRow.Rows.Rows.length; i++)
|
||||
{
|
||||
var row = this.RootRow.Rows.Rows[i];
|
||||
DOM.Node.Hide(row.Node);
|
||||
}
|
||||
}
|
||||
|
||||
else if (this.Visible)
|
||||
{
|
||||
// Otherwise just update the existing sample fields
|
||||
UpdateChangedSampleFields(this.RootRow, samples, "");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function GrowGrid(parent_row, nb_samples)
|
||||
{
|
||||
parent_row.Rows.Clear();
|
||||
|
||||
for (var i = 0; i < nb_samples; i++)
|
||||
{
|
||||
var cell_data =
|
||||
{
|
||||
_ID: i,
|
||||
Name: "",
|
||||
Length: "",
|
||||
Self: "",
|
||||
Calls: "",
|
||||
Recurse: "",
|
||||
};
|
||||
|
||||
var cell_classes =
|
||||
{
|
||||
Name: "SampleNameCell",
|
||||
Length: "SampleTimeCell",
|
||||
Self: "SampleTimeCell",
|
||||
Calls: "SampleCountCell",
|
||||
Recurse: "SampleCountCell",
|
||||
};
|
||||
|
||||
parent_row.Rows.Add(cell_data, null, cell_classes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function UpdateAllSampleFields(parent_row, samples, index, indent)
|
||||
{
|
||||
for (var i in samples)
|
||||
{
|
||||
var sample = samples[i];
|
||||
|
||||
// Match row allocation in GrowGrid
|
||||
var row = parent_row.Rows.Rows[index++];
|
||||
|
||||
// Sample row may have been hidden previously
|
||||
DOM.Node.Show(row.Node);
|
||||
|
||||
// Assign unique ID so that the common fast path of updating sample times only
|
||||
// can lookup target samples in the grid
|
||||
row.CellData._ID = sample.id;
|
||||
parent_row.Rows.AddRowToIndex("_ID", sample.id, row);
|
||||
|
||||
// Record sample name for later comparison
|
||||
row.CellData.Name = sample.name.string;
|
||||
|
||||
// Set sample name and colour
|
||||
var name_node = row.CellNodes["Name"];
|
||||
name_node.innerHTML = indent + sample.name.string;
|
||||
DOM.Node.SetColour(name_node, sample.colour);
|
||||
|
||||
row.CellNodes["Length"].innerHTML = sample.ms_length;
|
||||
row.CellNodes["Self"].innerHTML = sample.ms_self;
|
||||
row.CellNodes["Calls"].innerHTML = sample.call_count;
|
||||
row.CellNodes["Recurse"].innerHTML = sample.recurse_depth;
|
||||
|
||||
index = UpdateAllSampleFields(parent_row, sample.children, index, indent + " ");
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
|
||||
function UpdateChangedSampleFields(parent_row, samples, indent)
|
||||
{
|
||||
for (var i in samples)
|
||||
{
|
||||
var sample = samples[i];
|
||||
|
||||
var row = parent_row.Rows.GetBy("_ID", sample.id);
|
||||
if (row)
|
||||
{
|
||||
row.CellNodes["Length"].innerHTML = sample.ms_length;
|
||||
row.CellNodes["Self"].innerHTML = sample.ms_self;
|
||||
row.CellNodes["Calls"].innerHTML = sample.call_count;
|
||||
row.CellNodes["Recurse"].innerHTML = sample.recurse_depth;
|
||||
|
||||
// Sample name will change when it switches from hash ID to network-retrieved
|
||||
// name. Quickly check that before re-applying the HTML for the name.
|
||||
if (row.CellData.Name != sample.name.string)
|
||||
{
|
||||
var name_node = row.CellNodes["Name"];
|
||||
row.CellData.Name = sample.name.string;
|
||||
name_node.innerHTML = indent + sample.name.string;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateChangedSampleFields(parent_row, sample.children, indent + " ");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return SampleWindow;
|
||||
})();
|
||||
275
vis/Code/Shaders.js
Normal file
275
vis/Code/Shaders.js
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
const TimelineVShader =`#version 300 es
|
||||
|
||||
#define CANVAS_BORDER 1.0
|
||||
#define SAMPLE_HEIGHT 16.0
|
||||
#define SAMPLE_BORDER 1.0
|
||||
#define SAMPLE_Y_SPACING (SAMPLE_HEIGHT + SAMPLE_BORDER * 2.0)
|
||||
#define SAMPLE_Y_OFFSET (CANVAS_BORDER + 1.0)
|
||||
|
||||
struct Viewport
|
||||
{
|
||||
float width;
|
||||
float height;
|
||||
};
|
||||
|
||||
struct TimeRange
|
||||
{
|
||||
float usStart;
|
||||
float usPerPixel;
|
||||
};
|
||||
|
||||
struct Row
|
||||
{
|
||||
float yOffset;
|
||||
};
|
||||
|
||||
uniform Viewport inViewport;
|
||||
uniform TimeRange inTimeRange;
|
||||
uniform Row inRow;
|
||||
|
||||
in vec4 inSample_TextOffset;
|
||||
in vec4 inColour_TextLength;
|
||||
|
||||
out vec4 varColour_TimeMs;
|
||||
out vec4 varPosInBoxPx_TextEntry;
|
||||
out float varTimeChars;
|
||||
|
||||
//#define PIXEL_ROUNDED_OFFSETS
|
||||
|
||||
float PixelOffset(float time_us)
|
||||
{
|
||||
float offset = (time_us - inTimeRange.usStart) * inTimeRange.usPerPixel;
|
||||
#ifdef PIXEL_ROUNDED_OFFSETS
|
||||
return floor(offset);
|
||||
#else
|
||||
return offset;
|
||||
#endif
|
||||
}
|
||||
|
||||
float PixelSize(float time_us)
|
||||
{
|
||||
float size = time_us * inTimeRange.usPerPixel;
|
||||
#ifdef PIXEL_ROUNDED_OFFSETS
|
||||
return floor(size);
|
||||
#else
|
||||
return size;
|
||||
#endif
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
// Unpack input data
|
||||
float us_start = inSample_TextOffset.x;
|
||||
float us_length = inSample_TextOffset.y;
|
||||
float depth = inSample_TextOffset.z;
|
||||
float text_buffer_offset = inSample_TextOffset.w;
|
||||
vec3 box_colour = inColour_TextLength.rgb;
|
||||
float text_length_chars = inColour_TextLength.w;
|
||||
|
||||
// Determine pixel range of the sample
|
||||
float x0 = PixelOffset(us_start);
|
||||
float x1 = x0 + PixelSize(us_length);
|
||||
|
||||
// Calculate box to render
|
||||
float offset_x = x0;
|
||||
float offset_y = inRow.yOffset + SAMPLE_Y_OFFSET + (depth - 1.0) * SAMPLE_Y_SPACING;
|
||||
float size_x = max(x1 - x0, 1.0);
|
||||
float size_y = SAMPLE_HEIGHT;
|
||||
|
||||
// Box range
|
||||
float min_x = offset_x;
|
||||
float min_y = offset_y;
|
||||
float max_x = offset_x + size_x;
|
||||
float max_y = offset_y + size_y;
|
||||
|
||||
// Quad indices are:
|
||||
//
|
||||
// 2 3
|
||||
// +----+
|
||||
// | |
|
||||
// +----+
|
||||
// 0 1
|
||||
//
|
||||
vec2 position;
|
||||
position.x = (gl_VertexID & 1) == 0 ? min_x : max_x;
|
||||
position.y = (gl_VertexID & 2) == 0 ? min_y : max_y;
|
||||
|
||||
//
|
||||
// NDC is:
|
||||
// -1 to 1, left to right
|
||||
// -1 to 1, bottom to top
|
||||
//
|
||||
vec4 ndc_pos;
|
||||
ndc_pos.x = (position.x / inViewport.width) * 2.0 - 1.0;
|
||||
ndc_pos.y = 1.0 - (position.y / inViewport.height) * 2.0;
|
||||
ndc_pos.z = 0.0;
|
||||
ndc_pos.w = 1.0;
|
||||
|
||||
// Calculate number of characters required to display the millisecond time
|
||||
float time_ms = us_length / 1000.0;
|
||||
float time_ms_int = floor(time_ms);
|
||||
float time_chars = time_ms_int == 0.0 ? 1.0 : floor(log(time_ms_int) / 2.302585092994046) + 1.0;
|
||||
|
||||
gl_Position = ndc_pos;
|
||||
|
||||
varColour_TimeMs = vec4(box_colour / 255.0, time_ms);
|
||||
varPosInBoxPx_TextEntry = vec4(position.x - offset_x, position.y - offset_y, text_buffer_offset, text_length_chars);
|
||||
varTimeChars = time_chars;
|
||||
}
|
||||
`;
|
||||
|
||||
const TimelineFShader = `#version 300 es
|
||||
|
||||
precision mediump float;
|
||||
|
||||
#define SAMPLE_HEIGHT 16.0
|
||||
|
||||
struct TextBufferDesc
|
||||
{
|
||||
float fontWidth;
|
||||
float fontHeight;
|
||||
float textBufferLength;
|
||||
};
|
||||
|
||||
uniform sampler2D inFontAtlasTextre;
|
||||
uniform sampler2D inTextBuffer;
|
||||
uniform TextBufferDesc inTextBufferDesc;
|
||||
|
||||
in vec4 varColour_TimeMs;
|
||||
in vec4 varPosInBoxPx_TextEntry;
|
||||
in float varTimeChars;
|
||||
|
||||
out vec4 outColour;
|
||||
|
||||
vec4 LookupCharacter(float char_ascii, float pos_x, float pos_y, float font_width_px, float font_height_px)
|
||||
{
|
||||
// 2D index of the ASCII character in the font atlas
|
||||
float char_index_y = floor(char_ascii / 16.0);
|
||||
float char_index_x = char_ascii - char_index_y * 16.0;
|
||||
|
||||
// Start UV of the character in the font atlas
|
||||
float char_base_uv_x = char_index_x / 16.0;
|
||||
float char_base_uv_y = char_index_y / 16.0;
|
||||
|
||||
// UV within the character itself, scaled to the font atlas
|
||||
float char_uv_x = pos_x / (font_width_px * 16.0);
|
||||
float char_uv_y = pos_y / (font_height_px * 16.0);
|
||||
|
||||
vec2 uv;
|
||||
uv.x = char_base_uv_x + char_uv_x;
|
||||
uv.y = char_base_uv_y + char_uv_y;
|
||||
|
||||
// Apply colour to the text in premultiplied alpha space
|
||||
vec4 t = texture(inFontAtlasTextre, uv);
|
||||
vec3 colour = vec3(1.0, 1.0, 1.0) * 0.25;
|
||||
return vec4(colour * t.a, t.a);
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
// Font description
|
||||
float font_width_px = inTextBufferDesc.fontWidth;
|
||||
float font_height_px = inTextBufferDesc.fontHeight;
|
||||
float text_buffer_length = inTextBufferDesc.textBufferLength;
|
||||
|
||||
// Text range in the text buffer
|
||||
vec2 pos_in_box_px = varPosInBoxPx_TextEntry.xy;
|
||||
float text_buffer_offset = varPosInBoxPx_TextEntry.z;
|
||||
float text_length_chars = varPosInBoxPx_TextEntry.w;
|
||||
float text_length_px = text_length_chars * font_width_px;
|
||||
|
||||
// Text placement offset within the box
|
||||
const vec2 text_offset_px = vec2(4.0, 3.0);
|
||||
|
||||
vec4 box_colour = vec4(varColour_TimeMs.rgb, 1.0);
|
||||
|
||||
// Add a subtle border to the box so that you can visually separate samples when they are next to each other
|
||||
vec2 top_left = min(pos_in_box_px.xy, 2.0);
|
||||
float both = min(top_left.x, top_left.y);
|
||||
box_colour.rgb *= (0.8 + both * 0.1);
|
||||
|
||||
vec4 text_colour = vec4(0.0);
|
||||
|
||||
float text_end_px = text_length_px + text_offset_px.x + font_width_px;
|
||||
float time_length_px = (varTimeChars + 4.0) * font_width_px;
|
||||
if (pos_in_box_px.x > text_end_px && pos_in_box_px.x < text_end_px + time_length_px)
|
||||
{
|
||||
float time_ms = varColour_TimeMs.w;
|
||||
|
||||
vec2 time_pixel_pos;
|
||||
time_pixel_pos.x = max(min(pos_in_box_px.x - text_end_px, time_length_px), 0.0);
|
||||
time_pixel_pos.y = max(min(pos_in_box_px.y - text_offset_px.y, font_height_px - 1.0), 0.0);
|
||||
|
||||
float time_index = floor(time_pixel_pos.x / font_width_px);
|
||||
if (time_index < varTimeChars)
|
||||
{
|
||||
// Use base-10 integer digit counting to calculate the divisor needed to bring this digit below 10
|
||||
float time_divisor = 1.0;
|
||||
for (int i = 0; i < int(varTimeChars - time_index - 1.0); i++)
|
||||
{
|
||||
time_divisor *= 10.0;
|
||||
}
|
||||
|
||||
// Calculate digit
|
||||
float time_shifted_int = floor(time_ms / time_divisor);
|
||||
float time_digit = floor(mod(time_shifted_int, 10.0));
|
||||
|
||||
text_colour = LookupCharacter(48.0 + time_digit,
|
||||
time_pixel_pos.x - time_index * font_width_px,
|
||||
time_pixel_pos.y,
|
||||
font_width_px, font_height_px);
|
||||
}
|
||||
else if (time_index == varTimeChars)
|
||||
{
|
||||
text_colour = LookupCharacter(46.0,
|
||||
time_pixel_pos.x - time_index * font_width_px,
|
||||
time_pixel_pos.y,
|
||||
font_width_px, font_height_px);
|
||||
}
|
||||
else if (time_index == varTimeChars + 1.0)
|
||||
{
|
||||
float time_digit = floor(mod(time_ms * 10.0, 10.0));
|
||||
text_colour = LookupCharacter(48.0 + time_digit,
|
||||
time_pixel_pos.x - time_index * font_width_px,
|
||||
time_pixel_pos.y,
|
||||
font_width_px, font_height_px);
|
||||
}
|
||||
else if (time_index == varTimeChars + 2.0)
|
||||
{
|
||||
float time_digit = floor(mod(time_ms * 10.0, 10.0));
|
||||
text_colour = LookupCharacter(109.0,
|
||||
time_pixel_pos.x - time_index * font_width_px,
|
||||
time_pixel_pos.y,
|
||||
font_width_px, font_height_px);
|
||||
}
|
||||
else if (time_index == varTimeChars + 3.0)
|
||||
{
|
||||
float time_digit = floor(mod(time_ms * 10.0, 10.0));
|
||||
text_colour = LookupCharacter(115.0, time_pixel_pos.x - time_index * font_width_px, time_pixel_pos.y, font_width_px, font_height_px);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Text pixel position clamped to the bounds of the full word, allowing leakage to neighbouring NULL characters to pad zeroes
|
||||
vec2 text_pixel_pos;
|
||||
text_pixel_pos.x = max(min(pos_in_box_px.x - text_offset_px.x, text_length_px), -1.0);
|
||||
text_pixel_pos.y = max(min(pos_in_box_px.y - text_offset_px.y, font_height_px - 1.0), 0.0);
|
||||
|
||||
// Index of the current character in the text buffer
|
||||
float text_index = text_buffer_offset + floor(text_pixel_pos.x / font_width_px);
|
||||
|
||||
// Sample the 1D text buffer to get the ASCII character index
|
||||
vec2 char_uv = vec2((text_index + 0.5) / text_buffer_length, 0.5);
|
||||
float char_ascii = texture(inTextBuffer, char_uv).a * 255.0;
|
||||
|
||||
text_colour = LookupCharacter(char_ascii,
|
||||
text_pixel_pos.x - (text_index - text_buffer_offset) * font_width_px,
|
||||
text_pixel_pos.y,
|
||||
font_width_px, font_height_px);
|
||||
}
|
||||
|
||||
// Bring out of premultiplied alpha space and lerp with the box colour
|
||||
float inv_alpha = text_colour.a == 0.0 ? 1.0 : 1.0 / text_colour.a;
|
||||
outColour = mix(box_colour, vec4(text_colour.rgb * inv_alpha, 1.0), text_colour.a);
|
||||
}
|
||||
`;
|
||||
29
vis/Code/ThreadFrame.js
Normal file
29
vis/Code/ThreadFrame.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
|
||||
|
||||
ThreadFrame = (function()
|
||||
{
|
||||
function ThreadFrame(message)
|
||||
{
|
||||
// Persist the required message data
|
||||
this.NbSamples = message.nb_samples;
|
||||
this.SampleDigest = message.sample_digest;
|
||||
this.Samples = message.samples;
|
||||
this.PartialTree = message.partial_tree > 0 ? true : false;
|
||||
|
||||
// Calculate the frame start/end times
|
||||
this.StartTime_us = 0;
|
||||
this.EndTime_us = 0;
|
||||
var nb_root_samples = this.Samples.length;
|
||||
if (nb_root_samples > 0)
|
||||
{
|
||||
var last_sample = this.Samples[nb_root_samples - 1];
|
||||
this.StartTime_us = this.Samples[0].us_start;
|
||||
this.EndTime_us = last_sample.us_start + last_sample.us_length;
|
||||
}
|
||||
|
||||
this.Length_us = this.EndTime_us - this.StartTime_us;
|
||||
}
|
||||
|
||||
|
||||
return ThreadFrame;
|
||||
})();
|
||||
186
vis/Code/TimelineMarkers.js
Normal file
186
vis/Code/TimelineMarkers.js
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
|
||||
function GetTimeText(seconds)
|
||||
{
|
||||
if (seconds < 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
var text = "";
|
||||
|
||||
// Add any contributing hours
|
||||
var h = Math.floor(seconds / 3600);
|
||||
seconds -= h * 3600;
|
||||
if (h)
|
||||
{
|
||||
text += h + "h ";
|
||||
}
|
||||
|
||||
// Add any contributing minutes
|
||||
var m = Math.floor(seconds / 60);
|
||||
seconds -= m * 60;
|
||||
if (m)
|
||||
{
|
||||
text += m + "m ";
|
||||
}
|
||||
|
||||
// Add any contributing seconds or always add seconds when hours or minutes have no contribution
|
||||
// This ensures the 0s marker displays
|
||||
var s = Math.floor(seconds);
|
||||
seconds -= s;
|
||||
if (s || text == "")
|
||||
{
|
||||
text += s + "s ";
|
||||
}
|
||||
|
||||
// Add remaining milliseconds
|
||||
var ms = Math.floor(seconds * 1000);
|
||||
if (ms)
|
||||
{
|
||||
text += ms + "ms";
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
|
||||
class TimelineMarkers
|
||||
{
|
||||
constructor(timeline)
|
||||
{
|
||||
this.timeline = timeline;
|
||||
|
||||
// Need a 2D drawing context
|
||||
this.markerContainer = timeline.Window.AddControlNew(new WM.Container(10, 10, 10, 10));
|
||||
this.markerCanvas = document.createElement("canvas");
|
||||
this.markerContainer.Node.appendChild(this.markerCanvas);
|
||||
this.markerContext = this.markerCanvas.getContext("2d");
|
||||
}
|
||||
|
||||
Draw(time_range)
|
||||
{
|
||||
let ctx = this.markerContext;
|
||||
|
||||
ctx.clearRect(0, 0, this.markerCanvas.width, this.markerCanvas.height);
|
||||
|
||||
// Setup render state for the time line markers
|
||||
ctx.strokeStyle = "#BBB";
|
||||
ctx.fillStyle = "#BBB";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.font = "9px LocalFiraCode";
|
||||
|
||||
// A list of all supported units of time (measured in seconds) that require markers
|
||||
let units = [ 0.001, 0.01, 0.1, 1, 10, 60, 60 * 5, 60 * 60, 60 * 60 * 24 ];
|
||||
|
||||
// Given the current pixel size of a second, calculate the spacing for each unit marker
|
||||
let second_pixel_size = time_range.PixelSize(1000 * 1000);
|
||||
let sizeof_units = [ ];
|
||||
for (let unit of units)
|
||||
{
|
||||
sizeof_units.push(unit * second_pixel_size);
|
||||
}
|
||||
|
||||
// Calculate whether each unit marker is visible at the current zoom level
|
||||
var show_unit = [ ];
|
||||
for (let sizeof_unit of sizeof_units)
|
||||
{
|
||||
show_unit.push(Math.max(Math.min((sizeof_unit - 4) * 0.25, 1), 0));
|
||||
}
|
||||
|
||||
// Find the first visible unit
|
||||
for (let i = 0; i < units.length; i++)
|
||||
{
|
||||
if (show_unit[i] > 0)
|
||||
{
|
||||
// Cut out unit information for the first set of units not visible
|
||||
units = units.slice(i);
|
||||
sizeof_units = sizeof_units.slice(i);
|
||||
show_unit = show_unit.slice(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let timeline_end = this.markerCanvas.width;
|
||||
for (let i = 0; i < 3; i++)
|
||||
{
|
||||
// Round the start time up to the next visible unit
|
||||
let time_start = time_range.Start_us / (1000 * 1000);
|
||||
let unit_time_start = Math.ceil(time_start / units[i]) * units[i];
|
||||
|
||||
// Calculate the canvas offset required to step to the first visible unit
|
||||
let pre_step_x = time_range.PixelOffset(unit_time_start * (1000 * 1000));
|
||||
|
||||
// Draw lines for every unit at this level, keeping tracking of the seconds
|
||||
var seconds = unit_time_start;
|
||||
for (let x = pre_step_x; x <= timeline_end; x += sizeof_units[i])
|
||||
{
|
||||
// For the first two units, don't draw the units above it to prevent
|
||||
// overdraw and the visual errors that causes
|
||||
// The last unit always draws
|
||||
if (i > 1 || (seconds % units[i + 1]))
|
||||
{
|
||||
// Only the first two units scale with unit visibility
|
||||
// The last unit maintains its size
|
||||
let height = Math.min(i * 4 + 4 * show_unit[i], 16);
|
||||
|
||||
// Draw the line on an integer boundary, shifted by 0.5 to get an un-anti-aliased 1px line
|
||||
let ix = Math.floor(x);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(ix + 0.5, 1);
|
||||
ctx.lineTo(ix + 0.5, 1 + height);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
seconds += units[i];
|
||||
}
|
||||
|
||||
if (i == 1)
|
||||
{
|
||||
// Draw text labels for the second unit, fading them out as they slowly
|
||||
// become the first unit
|
||||
ctx.globalAlpha = show_unit[0];
|
||||
var seconds = unit_time_start;
|
||||
for (let x = pre_step_x; x <= timeline_end; x += sizeof_units[i])
|
||||
{
|
||||
if (seconds % units[2])
|
||||
{
|
||||
this.DrawTimeText(seconds, x, 16);
|
||||
}
|
||||
seconds += units[i];
|
||||
}
|
||||
|
||||
// Restore alpha
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
else if (i == 2)
|
||||
{
|
||||
// Draw text labels for the third unit with no fade
|
||||
var seconds = unit_time_start;
|
||||
for (let x = pre_step_x; x <= timeline_end; x += sizeof_units[i])
|
||||
{
|
||||
this.DrawTimeText(seconds, x, 16);
|
||||
seconds += units[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DrawTimeText(seconds, x, y)
|
||||
{
|
||||
// Use text measuring to centre the text horizontally on the input x
|
||||
var text = GetTimeText(seconds);
|
||||
var width = this.markerContext.measureText(text).width;
|
||||
this.markerContext.fillText(text, Math.floor(x) - width / 2, y);
|
||||
}
|
||||
|
||||
Resize(x, y, w, h)
|
||||
{
|
||||
this.markerContainer.SetPosition(x, y);
|
||||
this.markerContainer.SetSize(w, h);
|
||||
|
||||
// Match canvas size to container
|
||||
this.markerCanvas.width = this.markerContainer.Node.clientWidth;
|
||||
this.markerCanvas.height = this.markerContainer.Node.clientHeight;
|
||||
}
|
||||
}
|
||||
389
vis/Code/TimelineRow.js
Normal file
389
vis/Code/TimelineRow.js
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
|
||||
|
||||
TimelineRow = (function()
|
||||
{
|
||||
const RowLabelTemplate = `
|
||||
<div class='TimelineRow'>
|
||||
<div class='TimelineRowCheck TimelineBox'>
|
||||
<input class='TimelineRowCheckbox' type='checkbox' />
|
||||
</div>
|
||||
<div class='TimelineRowExpand TimelineBox NoSelect'>
|
||||
<div class='TimelineRowExpandButton'>+</div>
|
||||
</div>
|
||||
<div class='TimelineRowExpand TimelineBox NoSelect'>
|
||||
<div class='TimelineRowExpandButton'>-</div>
|
||||
</div>
|
||||
<div class='TimelineRowLabel TimelineBox'></div>
|
||||
<div style="clear:left"></div>
|
||||
</div>`
|
||||
|
||||
|
||||
var CANVAS_BORDER = 1;
|
||||
var SAMPLE_HEIGHT = 16;
|
||||
var SAMPLE_BORDER = 1;
|
||||
var SAMPLE_Y_SPACING = SAMPLE_HEIGHT + SAMPLE_BORDER * 2;
|
||||
|
||||
|
||||
function TimelineRow(gl, name, timeline, frame_history, check_handler)
|
||||
{
|
||||
this.Name = name;
|
||||
this.timeline = timeline;
|
||||
|
||||
// Create the row HTML and add to the parent
|
||||
this.LabelContainerNode = DOM.Node.CreateHTML(RowLabelTemplate);
|
||||
const label_node = DOM.Node.FindWithClass(this.LabelContainerNode, "TimelineRowLabel");
|
||||
label_node.innerHTML = name;
|
||||
timeline.TimelineLabels.Node.appendChild(this.LabelContainerNode);
|
||||
|
||||
// All sample view windows visible by default
|
||||
const checkbox_node = DOM.Node.FindWithClass(this.LabelContainerNode, "TimelineRowCheckbox");
|
||||
checkbox_node.checked = true;
|
||||
checkbox_node.addEventListener("change", (e) => check_handler(name, e));
|
||||
|
||||
// Manually hook-up events to simulate div:active
|
||||
// I can't get the equivalent CSS to work in Firefox, so...
|
||||
const expand_node_0 = DOM.Node.FindWithClass(this.LabelContainerNode, "TimelineRowExpand", 0);
|
||||
const expand_node_1 = DOM.Node.FindWithClass(this.LabelContainerNode, "TimelineRowExpand", 1);
|
||||
const inc_node = DOM.Node.FindWithClass(expand_node_0, "TimelineRowExpandButton");
|
||||
const dec_node = DOM.Node.FindWithClass(expand_node_1, "TimelineRowExpandButton");
|
||||
inc_node.addEventListener("mousedown", ExpandButtonDown);
|
||||
inc_node.addEventListener("mouseup", ExpandButtonUp);
|
||||
inc_node.addEventListener("mouseleave", ExpandButtonUp);
|
||||
dec_node.addEventListener("mousedown", ExpandButtonDown);
|
||||
dec_node.addEventListener("mouseup", ExpandButtonUp);
|
||||
dec_node.addEventListener("mouseleave", ExpandButtonUp);
|
||||
|
||||
// Pressing +/i increases/decreases depth
|
||||
inc_node.addEventListener("click", () => this.IncDepth());
|
||||
dec_node.addEventListener("click", () => this.DecDepth());
|
||||
|
||||
// Frame index to start at when looking for first visible sample
|
||||
this.StartFrameIndex = 0;
|
||||
|
||||
this.FrameHistory = frame_history;
|
||||
this.VisibleFrames = [ ];
|
||||
this.VisibleTimeRange = null;
|
||||
this.Depth = 1;
|
||||
|
||||
// Currently selected sample
|
||||
this.SelectSampleInfo = null;
|
||||
|
||||
// Create WebGL sample buffers
|
||||
this.sampleBuffer = new glDynamicBuffer(gl, gl.FLOAT, 4, 8);
|
||||
this.colourBuffer = new glDynamicBuffer(gl, gl.FLOAT, 4, 8);
|
||||
|
||||
// Create a vertex array for these buffers
|
||||
this.vertexArrayObject = gl.createVertexArray();
|
||||
gl.bindVertexArray(this.vertexArrayObject);
|
||||
this.sampleBuffer.BindAsInstanceAttribute(timeline.Program, "inSample_TextOffset");
|
||||
this.colourBuffer.BindAsInstanceAttribute(timeline.Program, "inColour_TextLength");
|
||||
|
||||
// An initial SetSize call to restore containers to their original size after traces were loaded prior to this
|
||||
this.SetSize();
|
||||
}
|
||||
|
||||
|
||||
TimelineRow.prototype.SetSize = function()
|
||||
{
|
||||
this.LabelContainerNode.style.height = CANVAS_BORDER + SAMPLE_BORDER + SAMPLE_Y_SPACING * this.Depth;
|
||||
}
|
||||
|
||||
|
||||
TimelineRow.prototype.SetVisibleFrames = function(time_range)
|
||||
{
|
||||
// Clear previous visible list
|
||||
this.VisibleFrames = [ ];
|
||||
if (this.FrameHistory.length == 0)
|
||||
return;
|
||||
|
||||
// Store a copy of the visible time range rather than referencing it
|
||||
// This prevents external modifications to the time range from affecting rendering/selection
|
||||
time_range = time_range.Clone();
|
||||
this.VisibleTimeRange = time_range;
|
||||
|
||||
// The frame history can be reset outside this class
|
||||
// This also catches the overflow to the end of the frame list below when a thread stops sending samples
|
||||
var max_frame = Math.max(this.FrameHistory.length - 1, 0);
|
||||
var start_frame_index = Math.min(this.StartFrameIndex, max_frame);
|
||||
|
||||
// First do a back-track in case the time range moves negatively
|
||||
while (start_frame_index > 0)
|
||||
{
|
||||
var frame = this.FrameHistory[start_frame_index];
|
||||
if (time_range.Start_us > frame.StartTime_us)
|
||||
break;
|
||||
start_frame_index--;
|
||||
}
|
||||
|
||||
// Then search from this point for the first visible frame
|
||||
while (start_frame_index < this.FrameHistory.length)
|
||||
{
|
||||
var frame = this.FrameHistory[start_frame_index];
|
||||
if (frame.EndTime_us > time_range.Start_us)
|
||||
break;
|
||||
start_frame_index++;
|
||||
}
|
||||
|
||||
// Gather all frames up to the end point
|
||||
this.StartFrameIndex = start_frame_index;
|
||||
for (var i = start_frame_index; i < this.FrameHistory.length; i++)
|
||||
{
|
||||
var frame = this.FrameHistory[i];
|
||||
if (frame.StartTime_us > time_range.End_us)
|
||||
break;
|
||||
this.VisibleFrames.push(frame);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TimelineRow.prototype.DrawSampleHighlight = function(sample, depth, colour, y_scroll_offset)
|
||||
{
|
||||
if (depth <= this.Depth)
|
||||
{
|
||||
// Determine pixel range of the sample
|
||||
var x0 = this.VisibleTimeRange.PixelOffset(sample.us_start);
|
||||
var x1 = x0 + this.VisibleTimeRange.PixelSize(sample.us_length);
|
||||
|
||||
var offset_x = x0;
|
||||
var offset_y = this.LabelContainerNode.offsetTop + 2 + (depth - 1) * SAMPLE_Y_SPACING + y_scroll_offset;
|
||||
var size_x = x1 - x0;
|
||||
var size_y = SAMPLE_HEIGHT;
|
||||
|
||||
// Normal rendering
|
||||
var ctx = this.timeline.drawContext;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeStyle = colour;
|
||||
ctx.strokeRect(offset_x + 2.5, offset_y - 0.5, size_x - 3, size_y + 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TimelineRow.prototype.DisplayHeight = function()
|
||||
{
|
||||
return this.LabelContainerNode.clientHeight;
|
||||
}
|
||||
|
||||
|
||||
TimelineRow.prototype.YOffset = function()
|
||||
{
|
||||
return this.LabelContainerNode.offsetTop;
|
||||
}
|
||||
|
||||
|
||||
TimelineRow.prototype.DrawBackground = function(hover_sample_info, y_scroll_offset)
|
||||
{
|
||||
// Fill box that shows the boundary between thread rows
|
||||
this.timeline.drawContext.fillStyle = "#444"
|
||||
var b = CANVAS_BORDER;
|
||||
this.timeline.drawContext.fillRect(b, this.YOffset() + y_scroll_offset + b, this.timeline.drawCanvas.width - b * 2, this.DisplayHeight() - b * 2);
|
||||
|
||||
// Draw the selected sample for this row
|
||||
if (this.SelectSampleInfo != null)
|
||||
{
|
||||
const sample = this.SelectSampleInfo[1];
|
||||
const depth = this.SelectSampleInfo[2];
|
||||
this.DrawSampleHighlight(sample, depth, "#FF0000", y_scroll_offset);
|
||||
}
|
||||
|
||||
// Draw the current hover sample if it's over this row
|
||||
if (hover_sample_info != null && hover_sample_info[3] == this)
|
||||
{
|
||||
const sample = hover_sample_info[1];
|
||||
const depth = hover_sample_info[2];
|
||||
const thread_row = hover_sample_info[3];
|
||||
this.DrawSampleHighlight(sample, depth, "#FFFFFF", y_scroll_offset);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TimelineRow.prototype.Draw = function(gl, draw_text, y_scroll_offset)
|
||||
{
|
||||
let samples_per_depth = [];
|
||||
|
||||
// Gather all root samples in the visible frame set
|
||||
for (var i in this.VisibleFrames)
|
||||
{
|
||||
var frame = this.VisibleFrames[i];
|
||||
GatherSamples(this, frame.Samples, 1, draw_text, samples_per_depth);
|
||||
}
|
||||
|
||||
// Count number of samples required
|
||||
let nb_samples = 0;
|
||||
for (const samples_this_depth of samples_per_depth)
|
||||
{
|
||||
nb_samples += samples_this_depth.length;
|
||||
}
|
||||
|
||||
// Resize buffers to match any new count of samples
|
||||
if (nb_samples > this.sampleBuffer.nbEntries)
|
||||
{
|
||||
this.sampleBuffer.ResizeToFitNextPow2(nb_samples);
|
||||
this.colourBuffer.ResizeToFitNextPow2(nb_samples);
|
||||
|
||||
// Have to create a new VAO for these buffers
|
||||
this.vertexArrayObject = gl.createVertexArray();
|
||||
gl.bindVertexArray(this.vertexArrayObject);
|
||||
this.sampleBuffer.BindAsInstanceAttribute(this.timeline.Program, "inSample_TextOffset");
|
||||
this.colourBuffer.BindAsInstanceAttribute(this.timeline.Program, "inColour_TextLength");
|
||||
}
|
||||
|
||||
// CPU write destination for samples
|
||||
let cpu_samples = this.sampleBuffer.cpuArray;
|
||||
let cpu_colours = this.colourBuffer.cpuArray;
|
||||
let sample_pos = 0;
|
||||
|
||||
const empty_text_entry = {
|
||||
offset: 0,
|
||||
length: 1,
|
||||
};
|
||||
|
||||
// Copy samples to the CPU buffer
|
||||
// TODO(don): Use a ring buffer instead and take advantage of timeline scrolling adding new samples at the beginning/end
|
||||
for (let depth = 0; depth < samples_per_depth.length; depth++)
|
||||
{
|
||||
let samples_this_depth = samples_per_depth[depth];
|
||||
for (const sample of samples_this_depth)
|
||||
{
|
||||
const text_entry = sample.name.textEntry != null ? sample.name.textEntry : empty_text_entry;
|
||||
|
||||
cpu_samples[sample_pos + 0] = sample.us_start;
|
||||
cpu_samples[sample_pos + 1] = sample.us_length;
|
||||
cpu_samples[sample_pos + 2] = depth;
|
||||
cpu_samples[sample_pos + 3] = text_entry.offset;
|
||||
|
||||
cpu_colours[sample_pos + 0] = sample.rgbColour[0];
|
||||
cpu_colours[sample_pos + 1] = sample.rgbColour[1];
|
||||
cpu_colours[sample_pos + 2] = sample.rgbColour[2];
|
||||
cpu_colours[sample_pos + 3] = text_entry.length;
|
||||
|
||||
sample_pos += 4;
|
||||
}
|
||||
}
|
||||
|
||||
// Upload to GPU
|
||||
this.sampleBuffer.UploadData();
|
||||
this.colourBuffer.UploadData();
|
||||
this.timeline.textBuffer.UploadData();
|
||||
|
||||
// Set row parameters
|
||||
glSetUniform(gl, this.timeline.Program, "inRow.yOffset", this.YOffset() + y_scroll_offset);
|
||||
|
||||
gl.bindVertexArray(this.vertexArrayObject);
|
||||
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, nb_samples);
|
||||
}
|
||||
|
||||
|
||||
function GatherSamples(self, samples, depth, draw_text, samples_per_depth)
|
||||
{
|
||||
// Ensure there's enough entries for each depth
|
||||
while (depth >= samples_per_depth.length)
|
||||
{
|
||||
samples_per_depth.push([]);
|
||||
}
|
||||
let samples_this_depth = samples_per_depth[depth];
|
||||
|
||||
for (var i in samples)
|
||||
{
|
||||
var sample = samples[i];
|
||||
samples_this_depth.push(sample);
|
||||
|
||||
if (depth < self.Depth && sample.children != null)
|
||||
GatherSamples(self, sample.children, depth + 1, draw_text, samples_per_depth);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TimelineRow.prototype.SetSelectSample = function(sample_info)
|
||||
{
|
||||
this.SelectSampleInfo = sample_info;
|
||||
}
|
||||
|
||||
|
||||
function ExpandButtonDown(evt)
|
||||
{
|
||||
var node = DOM.Event.GetNode(evt);
|
||||
DOM.Node.AddClass(node, "TimelineRowExpandButtonActive");
|
||||
}
|
||||
|
||||
|
||||
function ExpandButtonUp(evt)
|
||||
{
|
||||
var node = DOM.Event.GetNode(evt);
|
||||
DOM.Node.RemoveClass(node, "TimelineRowExpandButtonActive");
|
||||
}
|
||||
|
||||
|
||||
TimelineRow.prototype.IncDepth = function()
|
||||
{
|
||||
this.Depth++;
|
||||
this.SetSize();
|
||||
this.timeline.DrawAllRows();
|
||||
}
|
||||
|
||||
|
||||
TimelineRow.prototype.DecDepth = function()
|
||||
{
|
||||
if (this.Depth > 1)
|
||||
{
|
||||
this.Depth--;
|
||||
this.SetSize();
|
||||
|
||||
// Trigger scroll handling to ensure reducing the depth reduces the display height
|
||||
this.timeline.ScrollVertically(0);
|
||||
|
||||
this.timeline.DrawAllRows();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TimelineRow.prototype.GetSampleAtPosition = function(time_us, mouse_y)
|
||||
{
|
||||
// Calculate depth of the mouse cursor
|
||||
var depth = Math.min(Math.floor(mouse_y / SAMPLE_Y_SPACING) + 1, this.Depth);
|
||||
|
||||
// Search for the first frame to intersect this time
|
||||
for (var i in this.VisibleFrames)
|
||||
{
|
||||
// Use the sample's closed interval to detect hits.
|
||||
// Rendering of samples ensures a sample is never smaller than one pixel so that all samples always draw, irrespective
|
||||
// of zoom level. If a half-open interval is used then some visible samples will be unselectable due to them being
|
||||
// smaller than a pixel. This feels pretty odd and the closed interval fixes this feeling well.
|
||||
// TODO(don): There are still inconsistencies, need to shift to pixel range checking to match exactly.
|
||||
var frame = this.VisibleFrames[i];
|
||||
if (time_us >= frame.StartTime_us && time_us <= frame.EndTime_us)
|
||||
{
|
||||
var found_sample = FindSample(this, frame.Samples, time_us, depth, 1);
|
||||
if (found_sample != null)
|
||||
return [ frame, found_sample[0], found_sample[1], this ];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
function FindSample(self, samples, time_us, target_depth, depth)
|
||||
{
|
||||
for (var i in samples)
|
||||
{
|
||||
var sample = samples[i];
|
||||
if (depth == target_depth)
|
||||
{
|
||||
if (time_us >= sample.us_start && time_us < sample.us_start + sample.us_length)
|
||||
return [ sample, depth ];
|
||||
}
|
||||
|
||||
else if (depth < target_depth && sample.children != null)
|
||||
{
|
||||
var found_sample = FindSample(self, sample.children, time_us, target_depth, depth + 1);
|
||||
if (found_sample != null)
|
||||
return found_sample;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return TimelineRow;
|
||||
})();
|
||||
494
vis/Code/TimelineWindow.js
Normal file
494
vis/Code/TimelineWindow.js
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
|
||||
// TODO(don): Separate all knowledge of threads from this timeline
|
||||
|
||||
TimelineWindow = (function()
|
||||
{
|
||||
var BORDER = 10;
|
||||
|
||||
function TimelineWindow(wm, name, settings, check_handler)
|
||||
{
|
||||
this.Settings = settings;
|
||||
|
||||
// Create timeline window
|
||||
this.Window = wm.AddWindow("Timeline", 10, 20, 100, 100);
|
||||
this.Window.SetTitle(name);
|
||||
this.Window.ShowNoAnim();
|
||||
|
||||
this.timelineMarkers = new TimelineMarkers(this);
|
||||
|
||||
// DO THESE need to be containers... can they just be divs?
|
||||
// divs need a retrieval function
|
||||
this.TimelineLabelScrollClipper = this.Window.AddControlNew(new WM.Container(10, 10, 10, 10));
|
||||
DOM.Node.AddClass(this.TimelineLabelScrollClipper.Node, "TimelineLabelScrollClipper");
|
||||
this.TimelineLabels = this.TimelineLabelScrollClipper.AddControlNew(new WM.Container(0, 0, 10, 10));
|
||||
DOM.Node.AddClass(this.TimelineLabels.Node, "TimelineLabels");
|
||||
|
||||
// Ordered list of thread rows on the timeline
|
||||
this.ThreadRows = [ ];
|
||||
|
||||
// Create timeline container
|
||||
this.TimelineContainer = this.Window.AddControlNew(new WM.Container(10, 10, 800, 160));
|
||||
DOM.Node.AddClass(this.TimelineContainer.Node, "TimelineContainer");
|
||||
|
||||
var mouse_wheel_event = (/Firefox/i.test(navigator.userAgent)) ? "DOMMouseScroll" : "mousewheel";
|
||||
DOM.Event.AddHandler(this.TimelineContainer.Node, mouse_wheel_event, Bind(OnMouseScroll, this));
|
||||
|
||||
// Setup timeline manipulation
|
||||
this.MouseDown = false;
|
||||
this.LastMouseState = null;
|
||||
this.TimelineMoved = false;
|
||||
DOM.Event.AddHandler(this.TimelineContainer.Node, "mousedown", Bind(OnMouseDown, this));
|
||||
DOM.Event.AddHandler(this.TimelineContainer.Node, "mouseup", Bind(OnMouseUp, this));
|
||||
DOM.Event.AddHandler(this.TimelineContainer.Node, "mousemove", Bind(OnMouseMove, this));
|
||||
DOM.Event.AddHandler(this.TimelineContainer.Node, "mouseleave", Bind(OnMouseLeave, this));
|
||||
|
||||
// Create a canvas for timeline 2D rendering
|
||||
// TODO(don): Port this to shaders
|
||||
this.drawCanvas = document.createElement("canvas");
|
||||
this.drawCanvas.width = this.TimelineContainer.Node.clientWidth;
|
||||
this.drawCanvas.height = this.TimelineContainer.Node.clientHeight;
|
||||
this.TimelineContainer.Node.appendChild(this.drawCanvas);
|
||||
this.drawContext = this.drawCanvas.getContext("2d");
|
||||
|
||||
// Create a canvas for timeline 3D accelerated rendering
|
||||
this.glCanvas = document.createElement("canvas");
|
||||
this.glCanvas.width = this.TimelineContainer.Node.clientWidth;
|
||||
this.glCanvas.height = this.TimelineContainer.Node.clientHeight;
|
||||
this.TimelineContainer.Node.appendChild(this.glCanvas);
|
||||
|
||||
// OVERLAY - add to CSS
|
||||
this.glCanvas.style.position = "absolute";
|
||||
this.glCanvas.style.top = 0;
|
||||
this.glCanvas.style.left = 0;
|
||||
|
||||
// For now a gl context per timeline
|
||||
let gl = this.glCanvas.getContext("webgl2");
|
||||
this.gl = gl;
|
||||
|
||||
const vshader = glCompileShader(gl, gl.VERTEX_SHADER, "TimelineVShader", TimelineVShader);
|
||||
const fshader = glCompileShader(gl, gl.FRAGMENT_SHADER, "TimelineFShader", TimelineFShader);
|
||||
this.Program = glCreateProgram(gl, vshader, fshader);
|
||||
|
||||
this.font = new glFont(gl);
|
||||
this.textBuffer = new glTextBuffer(gl, this.font);
|
||||
|
||||
this.Window.SetOnResize(Bind(OnUserResize, this));
|
||||
|
||||
this.Clear();
|
||||
|
||||
this.OnHoverHandler = null;
|
||||
this.OnSelectedHandler = null;
|
||||
this.OnMovedHandler = null;
|
||||
this.CheckHandler = check_handler;
|
||||
|
||||
this.yScrollOffset = 0;
|
||||
|
||||
this.HoverSampleInfo = null;
|
||||
}
|
||||
|
||||
|
||||
TimelineWindow.prototype.Clear = function()
|
||||
{
|
||||
// Clear out labels
|
||||
this.TimelineLabels.ClearControls();
|
||||
|
||||
this.ThreadRows = [ ];
|
||||
this.TimeRange = new PixelTimeRange(0, 200 * 1000, this.TimelineContainer.Node.clientWidth);
|
||||
}
|
||||
|
||||
|
||||
TimelineWindow.prototype.SetOnHover = function(handler)
|
||||
{
|
||||
this.OnHoverHandler = handler;
|
||||
}
|
||||
|
||||
|
||||
TimelineWindow.prototype.SetOnSelected = function(handler)
|
||||
{
|
||||
this.OnSelectedHandler = handler;
|
||||
}
|
||||
|
||||
|
||||
TimelineWindow.prototype.SetOnMoved = function(handler)
|
||||
{
|
||||
this.OnMovedHandler = handler;
|
||||
}
|
||||
|
||||
|
||||
TimelineWindow.prototype.WindowResized = function(x, width, top_window)
|
||||
{
|
||||
// Resize window
|
||||
var top = top_window.Position[1] + top_window.Size[1] + 10;
|
||||
this.Window.SetPosition(x, top);
|
||||
this.Window.SetSize(width - 2 * 10, 260);
|
||||
|
||||
ResizeInternals(this);
|
||||
}
|
||||
|
||||
|
||||
TimelineWindow.prototype.OnSamples = function(thread_name, frame_history)
|
||||
{
|
||||
// Shift the timeline to the last entry on this thread
|
||||
// As multiple threads come through here with different end frames, only do this for the latest
|
||||
var last_frame = frame_history[frame_history.length - 1];
|
||||
if (last_frame.EndTime_us > this.TimeRange.End_us)
|
||||
this.TimeRange.SetEnd(last_frame.EndTime_us);
|
||||
|
||||
// Search for the index of this thread
|
||||
var thread_index = -1;
|
||||
for (var i in this.ThreadRows)
|
||||
{
|
||||
if (this.ThreadRows[i].Name == thread_name)
|
||||
{
|
||||
thread_index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If this thread has not been seen before, add a new row to the list and re-sort
|
||||
if (thread_index == -1)
|
||||
{
|
||||
var row = new TimelineRow(this.gl, thread_name, this, frame_history, this.CheckHandler);
|
||||
this.ThreadRows.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TimelineWindow.prototype.DrawBackground = function()
|
||||
{
|
||||
// TODO(don): Port all this lot to shader, maybe... it's not performance sensitive
|
||||
|
||||
this.drawContext.clearRect(0, 0, this.drawCanvas.width, this.drawCanvas.height);
|
||||
|
||||
// Draw thread row backgrounds
|
||||
for (let thread_row of this.ThreadRows)
|
||||
{
|
||||
thread_row.DrawBackground(this.HoverSampleInfo, this.yScrollOffset);
|
||||
}
|
||||
|
||||
this.timelineMarkers.Draw(this.TimeRange);
|
||||
}
|
||||
|
||||
|
||||
TimelineWindow.prototype.DrawAllRows = function()
|
||||
{
|
||||
let gl = this.gl;
|
||||
|
||||
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||
|
||||
gl.useProgram(this.Program);
|
||||
|
||||
// Set viewport parameters
|
||||
glSetUniform(gl, this.Program, "inViewport.width", gl.canvas.width);
|
||||
glSetUniform(gl, this.Program, "inViewport.height", gl.canvas.height);
|
||||
|
||||
// Set time range parameters
|
||||
const time_range = this.TimeRange;
|
||||
time_range.SetAsUniform(gl, this.Program);
|
||||
|
||||
// Set text rendering resources
|
||||
// Note it might not be loaded yet so we need the null check
|
||||
if (this.font.atlasTexture != null)
|
||||
{
|
||||
glSetUniform(gl, this.Program, "inFontAtlasTextre", this.font.atlasTexture, 0);
|
||||
this.textBuffer.SetAsUniform(gl, this.Program, "inTextBuffer", 1);
|
||||
}
|
||||
|
||||
const draw_text = this.Settings.IsPaused;
|
||||
for (let i in this.ThreadRows)
|
||||
{
|
||||
var thread_row = this.ThreadRows[i];
|
||||
thread_row.SetVisibleFrames(time_range);
|
||||
thread_row.Draw(gl, draw_text, this.yScrollOffset);
|
||||
}
|
||||
|
||||
// Render last so that each thread row uses any new time ranges
|
||||
this.DrawBackground();
|
||||
}
|
||||
|
||||
|
||||
function OnUserResize(self, evt)
|
||||
{
|
||||
ResizeInternals(self);
|
||||
}
|
||||
|
||||
function ResizeInternals(self)
|
||||
{
|
||||
// .TimelineRowLabel
|
||||
// .TimelineRowExpand
|
||||
// .TimelineRowExpand
|
||||
// .TimelineRowCheck
|
||||
// Window padding
|
||||
let offset_x = 145+19+19+19+10;
|
||||
|
||||
let MarkersHeight = 18;
|
||||
|
||||
var parent_size = self.Window.Size;
|
||||
|
||||
self.timelineMarkers.Resize(BORDER + offset_x, 10, parent_size[0] - 2* BORDER - offset_x, MarkersHeight);
|
||||
|
||||
// Resize controls
|
||||
self.TimelineContainer.SetPosition(BORDER + offset_x, 10 + MarkersHeight);
|
||||
self.TimelineContainer.SetSize(parent_size[0] - 2 * BORDER - offset_x, parent_size[1] - MarkersHeight - 40);
|
||||
|
||||
self.TimelineLabelScrollClipper.SetPosition(10, 10 + MarkersHeight);
|
||||
self.TimelineLabelScrollClipper.SetSize(offset_x, parent_size[1] - MarkersHeight - 40);
|
||||
self.TimelineLabels.SetSize(offset_x, parent_size[1] - MarkersHeight - 40);
|
||||
|
||||
// Match canvas size to container
|
||||
const width = self.TimelineContainer.Node.clientWidth;
|
||||
const height = self.TimelineContainer.Node.clientHeight;
|
||||
self.drawCanvas.width = width;
|
||||
self.drawCanvas.height = height;
|
||||
self.glCanvas.width = width;
|
||||
self.glCanvas.height = height;
|
||||
|
||||
// Adjust time range to new width
|
||||
self.TimeRange.SetPixelSpan(width);
|
||||
self.DrawAllRows();
|
||||
}
|
||||
|
||||
|
||||
function OnMouseScroll(self, evt)
|
||||
{
|
||||
let mouse_state = new Mouse.State(evt);
|
||||
let scale = 1.11;
|
||||
if (mouse_state.WheelDelta > 0)
|
||||
scale = 1 / scale;
|
||||
|
||||
// What time is the mouse hovering over?
|
||||
let mouse_pos = self.TimelineMousePosition(mouse_state);
|
||||
let time_us = self.TimeRange.TimeAtPosition(mouse_pos[0]);
|
||||
|
||||
// Calculate start time relative to the mouse hover position
|
||||
var time_start_us = self.TimeRange.Start_us - time_us;
|
||||
|
||||
// Scale and offset back to the hover time
|
||||
self.TimeRange.Set(time_start_us * scale + time_us, self.TimeRange.Span_us * scale);
|
||||
self.DrawAllRows();
|
||||
|
||||
if (self.OnMovedHandler)
|
||||
{
|
||||
self.OnMovedHandler(self);
|
||||
}
|
||||
|
||||
// Prevent vertical scrolling on mouse-wheel
|
||||
DOM.Event.StopDefaultAction(evt);
|
||||
}
|
||||
|
||||
|
||||
TimelineWindow.prototype.SetTimeRange = function(start_us, span_us)
|
||||
{
|
||||
this.TimeRange.Set(start_us, span_us);
|
||||
}
|
||||
|
||||
|
||||
TimelineWindow.prototype.DisplayHeight = function()
|
||||
{
|
||||
// Sum height of each thread row
|
||||
let height = 0;
|
||||
for (thread_row of this.ThreadRows)
|
||||
{
|
||||
height += thread_row.DisplayHeight();
|
||||
}
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
|
||||
TimelineWindow.prototype.ScrollVertically = function(y_scroll)
|
||||
{
|
||||
// Calculate the minimum negative value the position of the labels can be to account for scrolling to the bottom
|
||||
// of the label/depth list
|
||||
let display_height = this.DisplayHeight();
|
||||
let container_height = this.TimelineLabelScrollClipper.Node.clientHeight;
|
||||
let minimum_y = Math.min(container_height - display_height, 0.0);
|
||||
|
||||
// Resize the label container to match the display height
|
||||
this.TimelineLabels.Node.style.height = Math.max(display_height, container_height);
|
||||
|
||||
// Increment the y-scroll using just-calculated limits
|
||||
let old_y_scroll_offset = this.yScrollOffset;
|
||||
this.yScrollOffset = Math.min(Math.max(this.yScrollOffset + y_scroll, minimum_y), 0);
|
||||
|
||||
// Calculate how much the labels should actually scroll after limiting and apply
|
||||
let y_scroll_px = this.yScrollOffset - old_y_scroll_offset;
|
||||
this.TimelineLabels.Node.style.top = this.TimelineLabels.Node.offsetTop + y_scroll_px;
|
||||
}
|
||||
|
||||
|
||||
TimelineWindow.prototype.TimelineMousePosition = function(mouse_state)
|
||||
{
|
||||
// Position of the mouse relative to the timeline container
|
||||
let node_offset = DOM.Node.GetPosition(this.TimelineContainer.Node);
|
||||
let mouse_x = mouse_state.Position[0] - node_offset[0];
|
||||
let mouse_y = mouse_state.Position[1] - node_offset[1];
|
||||
|
||||
// Offset by the amount of scroll
|
||||
mouse_y -= this.yScrollOffset;
|
||||
|
||||
return [ mouse_x, mouse_y ];
|
||||
}
|
||||
|
||||
|
||||
TimelineWindow.prototype.GetHoverThreadRow = function(mouse_pos)
|
||||
{
|
||||
// Search for the thread row the mouse intersects
|
||||
let height = 0;
|
||||
for (let thread_row of this.ThreadRows)
|
||||
{
|
||||
let row_height = thread_row.DisplayHeight();
|
||||
if (mouse_pos[1] >= height && mouse_pos[1] < height + row_height)
|
||||
{
|
||||
// Mouse y relative to row start
|
||||
mouse_pos[1] -= height;
|
||||
return thread_row;
|
||||
}
|
||||
height += row_height;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
function OnMouseDown(self, evt)
|
||||
{
|
||||
// Only manipulate the timeline when paused
|
||||
if (!self.Settings.IsPaused)
|
||||
return;
|
||||
|
||||
self.MouseDown = true;
|
||||
self.LastMouseState = new Mouse.State(evt);
|
||||
self.TimelineMoved = false;
|
||||
DOM.Event.StopDefaultAction(evt);
|
||||
}
|
||||
|
||||
|
||||
function OnMouseUp(self, evt)
|
||||
{
|
||||
// Only manipulate the timeline when paused
|
||||
if (!self.Settings.IsPaused)
|
||||
return;
|
||||
|
||||
var mouse_state = new Mouse.State(evt);
|
||||
|
||||
self.MouseDown = false;
|
||||
|
||||
if (!self.TimelineMoved)
|
||||
{
|
||||
// Are we hovering over a thread row?
|
||||
let mouse_pos = self.TimelineMousePosition(mouse_state);
|
||||
let hover_thread_row = self.GetHoverThreadRow(mouse_pos);
|
||||
if (hover_thread_row != null)
|
||||
{
|
||||
// Are we hovering over a sample?
|
||||
let time_us = self.TimeRange.TimeAtPosition(mouse_pos[0]);
|
||||
let sample_info = hover_thread_row.GetSampleAtPosition(time_us, mouse_pos[1]);
|
||||
if (sample_info != null)
|
||||
{
|
||||
// Redraw with new select sample
|
||||
hover_thread_row.SetSelectSample(sample_info);
|
||||
self.DrawBackground();
|
||||
|
||||
// Call any selection handlers
|
||||
if (self.OnSelectedHandler)
|
||||
{
|
||||
self.OnSelectedHandler(hover_thread_row.Name, sample_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function OnMouseMove(self, evt)
|
||||
{
|
||||
// Only manipulate the timeline when paused
|
||||
if (!self.Settings.IsPaused)
|
||||
return;
|
||||
|
||||
var mouse_state = new Mouse.State(evt);
|
||||
|
||||
if (self.MouseDown)
|
||||
{
|
||||
let movement = false;
|
||||
|
||||
// Shift the visible time range with mouse movement
|
||||
let time_offset_us = (mouse_state.Position[0] - self.LastMouseState.Position[0]) / self.TimeRange.usPerPixel;
|
||||
if (time_offset_us != 0)
|
||||
{
|
||||
self.TimeRange.SetStart(self.TimeRange.Start_us - time_offset_us);
|
||||
movement = true;
|
||||
}
|
||||
|
||||
// Control vertical movement
|
||||
let y_offset_px = mouse_state.Position[1] - self.LastMouseState.Position[1];
|
||||
if (y_offset_px != 0)
|
||||
{
|
||||
self.ScrollVertically(y_offset_px);
|
||||
movement = true;
|
||||
}
|
||||
|
||||
// Redraw everything if there is movement
|
||||
if (movement)
|
||||
{
|
||||
self.DrawAllRows();
|
||||
self.TimelineMoved = true;
|
||||
|
||||
if (self.OnMovedHandler)
|
||||
{
|
||||
self.OnMovedHandler(self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
// Are we hovering over a thread row?
|
||||
let mouse_pos = self.TimelineMousePosition(mouse_state);
|
||||
let hover_thread_row = self.GetHoverThreadRow(mouse_pos);
|
||||
if (hover_thread_row != null)
|
||||
{
|
||||
// Are we hovering over a sample?
|
||||
let time_us = self.TimeRange.TimeAtPosition(mouse_pos[0]);
|
||||
self.HoverSampleInfo = hover_thread_row.GetSampleAtPosition(time_us, mouse_pos[1]);
|
||||
|
||||
// Tell listeners which sample we're hovering over
|
||||
if (self.OnHoverHandler != null)
|
||||
{
|
||||
self.OnHoverHandler(hover_thread_row.Name, self.HoverSampleInfo);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
self.HoverSampleInfo = null;
|
||||
}
|
||||
|
||||
// Redraw to update highlights
|
||||
self.DrawBackground();
|
||||
}
|
||||
|
||||
self.LastMouseState = mouse_state;
|
||||
}
|
||||
|
||||
|
||||
function OnMouseLeave(self, evt)
|
||||
{
|
||||
// Only manipulate the timeline when paused
|
||||
if (!self.Settings.IsPaused)
|
||||
return;
|
||||
|
||||
// Cancel scrolling
|
||||
self.MouseDown = false;
|
||||
|
||||
// Cancel hovering
|
||||
if (self.OnHoverHandler != null)
|
||||
{
|
||||
self.OnHoverHandler(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return TimelineWindow;
|
||||
})();
|
||||
|
||||
105
vis/Code/TitleWindow.js
Normal file
105
vis/Code/TitleWindow.js
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
|
||||
TitleWindow = (function()
|
||||
{
|
||||
function TitleWindow(wm, settings, server, connection_address)
|
||||
{
|
||||
this.Settings = settings;
|
||||
|
||||
this.Window = wm.AddWindow(" Remotery", 10, 10, 100, 100);
|
||||
this.Window.ShowNoAnim();
|
||||
|
||||
this.PingContainer = this.Window.AddControlNew(new WM.Container(4, -13, 10, 10));
|
||||
DOM.Node.AddClass(this.PingContainer.Node, "PingContainer");
|
||||
|
||||
this.EditBox = this.Window.AddControlNew(new WM.EditBox(10, 5, 300, 18, "Connection Address", connection_address));
|
||||
|
||||
// Setup pause button
|
||||
this.PauseButton = this.Window.AddControlNew(new WM.Button("Pause", 5, 5, { toggle: true }));
|
||||
this.PauseButton.SetOnClick(Bind(OnPausePressed, this));
|
||||
|
||||
this.SyncButton = this.Window.AddControlNew(new WM.Button("Sync Timelines", 5, 5, { toggle: true}));
|
||||
this.SyncButton.SetOnClick(Bind(OnSyncPressed, this));
|
||||
this.SyncButton.SetState(this.Settings.SyncTimelines);
|
||||
|
||||
server.AddMessageHandler("PING", Bind(OnPing, this));
|
||||
|
||||
this.Window.SetOnResize(Bind(OnUserResize, this));
|
||||
}
|
||||
|
||||
|
||||
TitleWindow.prototype.SetConnectionAddressChanged = function(handler)
|
||||
{
|
||||
this.EditBox.SetChangeHandler(handler);
|
||||
}
|
||||
|
||||
|
||||
TitleWindow.prototype.WindowResized = function(width, height)
|
||||
{
|
||||
this.Window.SetSize(width - 2 * 10, 50);
|
||||
ResizeInternals(this);
|
||||
}
|
||||
|
||||
TitleWindow.prototype.Pause = function()
|
||||
{
|
||||
if (!this.Settings.IsPaused)
|
||||
{
|
||||
this.PauseButton.SetText("Paused");
|
||||
this.PauseButton.SetState(true);
|
||||
this.Settings.IsPaused = true;
|
||||
}
|
||||
}
|
||||
|
||||
TitleWindow.prototype.Unpause = function()
|
||||
{
|
||||
if (this.Settings.IsPaused)
|
||||
{
|
||||
this.PauseButton.SetText("Pause");
|
||||
this.PauseButton.SetState(false);
|
||||
this.Settings.IsPaused = false;
|
||||
}
|
||||
}
|
||||
|
||||
function OnUserResize(self, evt)
|
||||
{
|
||||
ResizeInternals(self);
|
||||
}
|
||||
|
||||
function ResizeInternals(self)
|
||||
{
|
||||
self.PauseButton.SetPosition(self.Window.Size[0] - 60, 5);
|
||||
self.SyncButton.SetPosition(self.Window.Size[0] - 155, 5);
|
||||
}
|
||||
|
||||
|
||||
function OnPausePressed(self)
|
||||
{
|
||||
if (self.PauseButton.IsPressed())
|
||||
{
|
||||
self.Pause();
|
||||
}
|
||||
else
|
||||
{
|
||||
self.Unpause();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function OnSyncPressed(self)
|
||||
{
|
||||
self.Settings.SyncTimelines = self.SyncButton.IsPressed();
|
||||
}
|
||||
|
||||
|
||||
function OnPing(self, server)
|
||||
{
|
||||
// Set the ping container as active and take it off half a second later
|
||||
DOM.Node.AddClass(self.PingContainer.Node, "PingContainerActive");
|
||||
window.setTimeout(Bind(function(self)
|
||||
{
|
||||
DOM.Node.RemoveClass(self.PingContainer.Node, "PingContainerActive");
|
||||
}, self), 500);
|
||||
}
|
||||
|
||||
|
||||
return TitleWindow;
|
||||
})();
|
||||
134
vis/Code/TraceDrop.js
Normal file
134
vis/Code/TraceDrop.js
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
|
||||
class TraceDrop
|
||||
{
|
||||
constructor(remotery)
|
||||
{
|
||||
this.Remotery = remotery;
|
||||
|
||||
// Create a full-page overlay div for dropping files onto
|
||||
this.DropNode = DOM.Node.CreateHTML("<div id='DropZone' class='DropZone'>Load Remotery Trace</div>");
|
||||
document.body.appendChild(this.DropNode);
|
||||
|
||||
// Attach drop handlers
|
||||
window.addEventListener("dragenter", () => this.ShowDropZone());
|
||||
this.DropNode.addEventListener("dragenter", (e) => this.AllowDrag(e));
|
||||
this.DropNode.addEventListener("dragover", (e) => this.AllowDrag(e));
|
||||
this.DropNode.addEventListener("dragleave", () => this.HideDropZone());
|
||||
this.DropNode.addEventListener("drop", (e) => this.OnDrop(e));
|
||||
}
|
||||
|
||||
ShowDropZone()
|
||||
{
|
||||
this.DropNode.style.display = "flex";
|
||||
}
|
||||
|
||||
HideDropZone()
|
||||
{
|
||||
this.DropNode.style.display = "none";
|
||||
}
|
||||
|
||||
AllowDrag(evt)
|
||||
{
|
||||
// Prevent the default drag handler kicking in
|
||||
evt.preventDefault();
|
||||
|
||||
evt.dataTransfer.dropEffect = "copy";
|
||||
}
|
||||
|
||||
OnDrop(evt)
|
||||
{
|
||||
// Prevent the default drop handler kicking in
|
||||
evt.preventDefault();
|
||||
|
||||
this.HideDropZone(evt);
|
||||
|
||||
// Get the file that was dropped
|
||||
let files = DOM.Event.GetDropFiles(evt);
|
||||
if (files.length == 0)
|
||||
{
|
||||
alert("No files dropped");
|
||||
return;
|
||||
}
|
||||
if (files.length > 1)
|
||||
{
|
||||
alert("Too many files dropped");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check file type
|
||||
let file = files[0];
|
||||
if (!file.name.endsWith(".rbin"))
|
||||
{
|
||||
alert("Not the correct .rbin file type");
|
||||
return;
|
||||
}
|
||||
|
||||
// Background-load the file
|
||||
var remotery = this.Remotery;
|
||||
let file_reader = new FileReader();
|
||||
file_reader.onload = function()
|
||||
{
|
||||
// Create the data reader and verify the header
|
||||
let data_view = new DataView(this.result);
|
||||
let data_view_reader = new DataViewReader(data_view, 0);
|
||||
let header = data_view_reader.GetStringOfLength(8);
|
||||
if (header != "RMTBLOGF")
|
||||
{
|
||||
alert("Not a valid Remotery Log File");
|
||||
return;
|
||||
}
|
||||
|
||||
remotery.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
// Forward all recorded events to message handlers
|
||||
while (!data_view_reader.AtEnd())
|
||||
{
|
||||
remotery.Server.CallMessageHandlers(data_view_reader);
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
// The last message may be partially written due to process exit
|
||||
// Catch this safely as it's a valid state for the file to be in
|
||||
if (e instanceof RangeError)
|
||||
{
|
||||
console.log("Aborted reading last message");
|
||||
}
|
||||
}
|
||||
|
||||
// After loading completes, populate the UI which wasn't updated during loading
|
||||
|
||||
remotery.Console.TriggerUpdate();
|
||||
|
||||
// Set frame history for each timeline thread
|
||||
for (let name in remotery.FrameHistory)
|
||||
{
|
||||
let frame_history = remotery.FrameHistory[name];
|
||||
remotery.SampleTimelineWindow.OnSamples(name, frame_history);
|
||||
}
|
||||
remotery.SampleTimelineWindow.DrawAllRows();
|
||||
|
||||
for (let name in remotery.ProcessorFrameHistory)
|
||||
{
|
||||
let frame_history = remotery.ProcessorFrameHistory[name];
|
||||
remotery.ProcessorTimelineWindow.OnSamples(name, frame_history);
|
||||
}
|
||||
remotery.ProcessorTimelineWindow.DrawAllRows();
|
||||
|
||||
// Set the last frame of each thread sample history on the sample windows
|
||||
for (let name in remotery.SampleWindows)
|
||||
{
|
||||
let sample_window = remotery.SampleWindows[name];
|
||||
let frame_history = remotery.FrameHistory[name];
|
||||
let frame = frame_history[frame_history.length - 1];
|
||||
sample_window.OnSamples(frame.NbSamples, frame.SampleDigest, frame.Samples);
|
||||
}
|
||||
|
||||
// Pause for viewing
|
||||
remotery.TitleWindow.Pause();
|
||||
};
|
||||
file_reader.readAsArrayBuffer(file);
|
||||
}
|
||||
}
|
||||
238
vis/Code/WebGL.js
Normal file
238
vis/Code/WebGL.js
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
|
||||
function assert(condition, message)
|
||||
{
|
||||
if (!condition)
|
||||
{
|
||||
throw new Error(message || "Assertion failed");
|
||||
}
|
||||
}
|
||||
|
||||
function glCompileShader(gl, type, name, source)
|
||||
{
|
||||
console.log("Compiling " + name);
|
||||
|
||||
// Compile the shader
|
||||
let shader = gl.createShader(type);
|
||||
gl.shaderSource(shader, source);
|
||||
gl.compileShader(shader);
|
||||
|
||||
// Report any errors
|
||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS))
|
||||
{
|
||||
console.log("Error compiling " + name);
|
||||
console.log(gl.getShaderInfoLog(shader));
|
||||
console.trace();
|
||||
}
|
||||
|
||||
return shader;
|
||||
}
|
||||
|
||||
function glCreateProgram(gl, vshader, fshader)
|
||||
{
|
||||
// Attach shaders and link
|
||||
let program = gl.createProgram();
|
||||
gl.attachShader(program, vshader);
|
||||
gl.attachShader(program, fshader);
|
||||
gl.linkProgram(program);
|
||||
|
||||
// Report any errors
|
||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS))
|
||||
{
|
||||
console.log("Failed to link program");
|
||||
console.trace();
|
||||
}
|
||||
|
||||
return program;
|
||||
}
|
||||
|
||||
function glSetUniform(gl, program, name, value, index)
|
||||
{
|
||||
// Get location
|
||||
const location = gl.getUniformLocation(program, name);
|
||||
assert(location != null, "Can't find uniform " + name);
|
||||
|
||||
// Dispatch to uniform function by type
|
||||
assert(value != null, "Value is null");
|
||||
const type = Object.prototype.toString.call(value).slice(8, -1);
|
||||
switch (type)
|
||||
{
|
||||
case "Number":
|
||||
gl.uniform1f(location, value);
|
||||
break;
|
||||
|
||||
case "WebGLTexture":
|
||||
gl.activeTexture(gl.TEXTURE0 + index);
|
||||
gl.bindTexture(gl.TEXTURE_2D, value);
|
||||
gl.uniform1i(location, index);
|
||||
break;
|
||||
|
||||
default:
|
||||
assert(false, "Unhandled type " + type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function glCreateTexture(gl, width, height, data)
|
||||
{
|
||||
const texture = gl.createTexture();
|
||||
|
||||
// Set filtering/wrapping to nearest/clamp
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
const glDynamicBufferType = Object.freeze({
|
||||
Buffer: 1,
|
||||
Texture: 2
|
||||
});
|
||||
|
||||
class glDynamicBuffer
|
||||
{
|
||||
constructor(gl, element_type, nb_elements, nb_entries, buffer_type)
|
||||
{
|
||||
this.gl = gl;
|
||||
this.elementType = element_type;
|
||||
this.nbElements = nb_elements;
|
||||
this.bufferType = buffer_type == undefined ? glDynamicBufferType.Buffer : buffer_type;
|
||||
this.dirty = false;
|
||||
|
||||
this.Resize(nb_entries);
|
||||
}
|
||||
|
||||
BindAsInstanceAttribute(program, attrib_name)
|
||||
{
|
||||
assert(this.bufferType == glDynamicBufferType.Buffer, "Can only call BindAsInstanceAttribute with Buffer types");
|
||||
|
||||
let gl = this.gl;
|
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
||||
|
||||
// The attribute referenced in the program
|
||||
const attrib_location = gl.getAttribLocation(program, attrib_name);
|
||||
|
||||
gl.enableVertexAttribArray(attrib_location);
|
||||
gl.vertexAttribPointer(attrib_location, this.nbElements, this.elementType, false, 0, 0);
|
||||
|
||||
// One per instance
|
||||
gl.vertexAttribDivisor(attrib_location, 1);
|
||||
}
|
||||
|
||||
UploadData()
|
||||
{
|
||||
let gl = this.gl;
|
||||
|
||||
switch (this.bufferType)
|
||||
{
|
||||
case glDynamicBufferType.Buffer:
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
||||
gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.cpuArray);
|
||||
break;
|
||||
|
||||
case glDynamicBufferType.Texture:
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, this.nbEntries, 1, 0, gl.ALPHA, gl.UNSIGNED_BYTE, this.cpuArray);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
UploadDirtyData()
|
||||
{
|
||||
if (this.dirty)
|
||||
{
|
||||
this.UploadData();
|
||||
this.dirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
ResizeToFitNextPow2(target_count)
|
||||
{
|
||||
let nb_entries = this.nbEntries;
|
||||
while (target_count > nb_entries)
|
||||
{
|
||||
nb_entries <<= 1;
|
||||
}
|
||||
|
||||
if (nb_entries > this.nbEntries)
|
||||
{
|
||||
this.Resize(nb_entries);
|
||||
}
|
||||
}
|
||||
|
||||
Resize(nb_entries)
|
||||
{
|
||||
let gl = this.gl;
|
||||
|
||||
this.nbEntries = nb_entries;
|
||||
|
||||
// Create the CPU array
|
||||
const old_array = this.cpuArray;
|
||||
switch (this.elementType)
|
||||
{
|
||||
case gl.FLOAT:
|
||||
this.nbElementBytes = 4;
|
||||
this.cpuArray = new Float32Array(this.nbElements * this.nbEntries);
|
||||
break;
|
||||
|
||||
case gl.BYTE:
|
||||
this.nbElementBytes = 1;
|
||||
this.cpuArray = new Uint8Array(this.nbElements * this.nbEntries);
|
||||
break;
|
||||
|
||||
default:
|
||||
assert(false, "Unsupported dynamic buffer element type");
|
||||
}
|
||||
|
||||
// Calculate byte size of the buffer
|
||||
this.nbBytes = this.nbElementBytes * this.nbElements * this.nbEntries;
|
||||
|
||||
if (old_array != undefined)
|
||||
{
|
||||
// Copy the values of the previous array over
|
||||
this.cpuArray.set(old_array);
|
||||
|
||||
console.log(`glDynamicBuffer: Resizing to ${nb_entries} entries, ${this.nbElements} elements per entry, ${this.nbElementBytes} bytes per element, ${this.nbBytes} bytes total.`);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log(`glDynamicBuffer: Creating ${nb_entries} entries, ${this.nbElements} elements per entry, ${this.nbElementBytes} bytes per element, ${this.nbBytes} bytes total.`);
|
||||
}
|
||||
|
||||
// Create the GPU buffer
|
||||
switch (this.bufferType)
|
||||
{
|
||||
case glDynamicBufferType.Buffer:
|
||||
this.buffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, this.nbBytes, gl.DYNAMIC_DRAW);
|
||||
break;
|
||||
|
||||
case glDynamicBufferType.Texture:
|
||||
this.texture = gl.createTexture();
|
||||
|
||||
// Point sampling with clamp for indexing
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
|
||||
// Fixed-format for now
|
||||
assert(this.elementType == gl.BYTE);
|
||||
assert(this.nbElements == 1);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, this.nbEntries, 1, 0, gl.ALPHA, gl.UNSIGNED_BYTE, this.cpuArray);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
assert(false, "Unsupported dynamic buffer type");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
119
vis/Code/WebGLFont.js
Normal file
119
vis/Code/WebGLFont.js
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
|
||||
class glFont
|
||||
{
|
||||
constructor(gl)
|
||||
{
|
||||
// Offscreen canvas for rendering individual characters
|
||||
this.charCanvas = document.createElement("canvas");
|
||||
this.charContext = this.charCanvas.getContext("2d");
|
||||
|
||||
// Describe the font
|
||||
const font_size = 9;
|
||||
this.fontWidth = 5;
|
||||
this.fontHeight = 12;
|
||||
const font_face = "LocalFiraCode";
|
||||
const font_desc = font_size + "px " + font_face;
|
||||
|
||||
// Ensure the CSS font is loaded before we do any work with it
|
||||
const self = this;
|
||||
document.fonts.load(font_desc).then(function (){
|
||||
|
||||
// Create a canvas atlas for all characters in the font
|
||||
const atlas_canvas = document.createElement("canvas");
|
||||
const atlas_context = atlas_canvas.getContext("2d");
|
||||
atlas_canvas.width = 16 * self.fontWidth;
|
||||
atlas_canvas.height = 16 * self.fontHeight;
|
||||
|
||||
// Add each character to the atlas
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-+=[]{};\'~#,./<>?!\"£$%%^&*()";
|
||||
for (let char of chars)
|
||||
{
|
||||
// Render this character to the canvas on its own
|
||||
self.RenderTextToCanvas(char, font_desc, self.fontWidth, self.fontHeight);
|
||||
|
||||
// Calculate a location for it in the atlas using its ASCII code
|
||||
const ascii_code = char.charCodeAt(0);
|
||||
assert(ascii_code < 256);
|
||||
const y_index = Math.floor(ascii_code / 16);
|
||||
const x_index = ascii_code - y_index * 16
|
||||
assert(x_index < 16);
|
||||
assert(y_index < 16);
|
||||
|
||||
// Copy into the atlas
|
||||
atlas_context.drawImage(self.charCanvas, x_index * self.fontWidth, y_index * self.fontHeight);
|
||||
}
|
||||
|
||||
// Create the atlas texture and store it in the destination object
|
||||
self.atlasTexture = glCreateTexture(gl, atlas_canvas.width, atlas_canvas.height, atlas_canvas);
|
||||
});
|
||||
}
|
||||
|
||||
RenderTextToCanvas(text, font, width, height)
|
||||
{
|
||||
// Resize canvas to match
|
||||
this.charCanvas.width = width;
|
||||
this.charCanvas.height = height;
|
||||
|
||||
// Clear the background
|
||||
this.charContext.fillStyle = "black";
|
||||
this.charContext.clearRect(0, 0, width, height);
|
||||
|
||||
// Render the text
|
||||
this.charContext.font = font;
|
||||
this.charContext.textAlign = "left";
|
||||
this.charContext.textBaseline = "top";
|
||||
this.charContext.fillText(text, 0, 1.5);
|
||||
}
|
||||
}
|
||||
|
||||
class glTextBuffer
|
||||
{
|
||||
constructor(gl, font)
|
||||
{
|
||||
this.font = font;
|
||||
this.textMap = {};
|
||||
this.textBuffer = new glDynamicBuffer(gl, gl.BYTE, 1, 8, glDynamicBufferType.Texture);
|
||||
this.textBufferPos = 0;
|
||||
this.textEncoder = new TextEncoder();
|
||||
}
|
||||
|
||||
AddText(text)
|
||||
{
|
||||
// Return if it already exists
|
||||
const existing_entry = this.textMap[text];
|
||||
if (existing_entry != undefined)
|
||||
{
|
||||
return existing_entry;
|
||||
}
|
||||
|
||||
// Add to the map
|
||||
// Note we're leaving an extra NULL character before every piece of text so that the shader can sample into it on text
|
||||
// boundaries and sample a zero colour for clamp.
|
||||
let entry = {
|
||||
offset: this.textBufferPos + 1,
|
||||
length: text.length,
|
||||
};
|
||||
this.textMap[text] = entry;
|
||||
|
||||
// Ensure there's always enough space in the text buffer before adding
|
||||
this.textBuffer.ResizeToFitNextPow2(entry.offset + entry.length + 1);
|
||||
this.textBuffer.cpuArray.set(this.textEncoder.encode(text), entry.offset, entry.length);
|
||||
this.textBuffer.dirty = true;
|
||||
this.textBufferPos = entry.offset + entry.length;
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
UploadData()
|
||||
{
|
||||
this.textBuffer.UploadDirtyData();
|
||||
}
|
||||
|
||||
SetAsUniform(gl, program, name, index)
|
||||
{
|
||||
glSetUniform(gl, program, name, this.textBuffer.texture, index);
|
||||
glSetUniform(gl, program, "inTextBufferDesc.fontWidth", this.font.fontWidth);
|
||||
glSetUniform(gl, program, "inTextBufferDesc.fontHeight", this.font.fontHeight);
|
||||
glSetUniform(gl, program, "inTextBufferDesc.textBufferLength", this.textBuffer.nbEntries);
|
||||
}
|
||||
}
|
||||
137
vis/Code/WebSocketConnection.js
Normal file
137
vis/Code/WebSocketConnection.js
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
|
||||
WebSocketConnection = (function()
|
||||
{
|
||||
function WebSocketConnection()
|
||||
{
|
||||
this.MessageHandlers = { };
|
||||
this.Socket = null;
|
||||
this.Console = null;
|
||||
}
|
||||
|
||||
|
||||
WebSocketConnection.prototype.SetConsole = function(console)
|
||||
{
|
||||
this.Console = console;
|
||||
}
|
||||
|
||||
|
||||
WebSocketConnection.prototype.Connected = function()
|
||||
{
|
||||
// Will return true if the socket is also in the process of connecting
|
||||
return this.Socket != null;
|
||||
}
|
||||
|
||||
|
||||
WebSocketConnection.prototype.AddConnectHandler = function(handler)
|
||||
{
|
||||
this.AddMessageHandler("__OnConnect__", handler);
|
||||
}
|
||||
|
||||
|
||||
WebSocketConnection.prototype.AddDisconnectHandler = function(handler)
|
||||
{
|
||||
this.AddMessageHandler("__OnDisconnect__", handler);
|
||||
}
|
||||
|
||||
|
||||
WebSocketConnection.prototype.AddMessageHandler = function(message_name, handler)
|
||||
{
|
||||
// Create the message handler array on-demand
|
||||
if (!(message_name in this.MessageHandlers))
|
||||
this.MessageHandlers[message_name] = [ ];
|
||||
this.MessageHandlers[message_name].push(handler);
|
||||
}
|
||||
|
||||
|
||||
WebSocketConnection.prototype.Connect = function(address)
|
||||
{
|
||||
// Disconnect if already connected
|
||||
if (this.Connected())
|
||||
this.Disconnect();
|
||||
|
||||
Log(this, "Connecting to " + address);
|
||||
|
||||
this.Socket = new WebSocket(address);
|
||||
this.Socket.binaryType = "arraybuffer";
|
||||
this.Socket.onopen = Bind(OnOpen, this);
|
||||
this.Socket.onmessage = Bind(OnMessage, this);
|
||||
this.Socket.onclose = Bind(OnClose, this);
|
||||
this.Socket.onerror = Bind(OnError, this);
|
||||
}
|
||||
|
||||
|
||||
WebSocketConnection.prototype.Disconnect = function()
|
||||
{
|
||||
Log(this, "Disconnecting");
|
||||
if (this.Connected())
|
||||
this.Socket.close();
|
||||
}
|
||||
|
||||
|
||||
WebSocketConnection.prototype.Send = function(msg)
|
||||
{
|
||||
if (this.Connected())
|
||||
this.Socket.send(msg);
|
||||
}
|
||||
|
||||
|
||||
function Log(self, message)
|
||||
{
|
||||
self.Console.Log(message);
|
||||
}
|
||||
|
||||
|
||||
function CallMessageHandlers(self, message_name, data_view)
|
||||
{
|
||||
if (message_name in self.MessageHandlers)
|
||||
{
|
||||
var handlers = self.MessageHandlers[message_name];
|
||||
for (var i in handlers)
|
||||
handlers[i](self, data_view);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function OnOpen(self, event)
|
||||
{
|
||||
Log(self, "Connected");
|
||||
CallMessageHandlers(self, "__OnConnect__");
|
||||
}
|
||||
|
||||
|
||||
function OnClose(self, event)
|
||||
{
|
||||
// Clear all references
|
||||
self.Socket.onopen = null;
|
||||
self.Socket.onmessage = null;
|
||||
self.Socket.onclose = null;
|
||||
self.Socket.onerror = null;
|
||||
self.Socket = null;
|
||||
|
||||
Log(self, "Disconnected");
|
||||
CallMessageHandlers(self, "__OnDisconnect__");
|
||||
}
|
||||
|
||||
|
||||
function OnError(self, event)
|
||||
{
|
||||
Log(self, "Connection Error ");
|
||||
}
|
||||
|
||||
|
||||
function OnMessage(self, event)
|
||||
{
|
||||
let data_view = new DataView(event.data);
|
||||
let data_view_reader = new DataViewReader(data_view, 0);
|
||||
self.CallMessageHandlers(data_view_reader);
|
||||
}
|
||||
|
||||
WebSocketConnection.prototype.CallMessageHandlers = function(data_view_reader)
|
||||
{
|
||||
let id = data_view_reader.GetStringOfLength(4);
|
||||
CallMessageHandlers(this, id, data_view_reader);
|
||||
}
|
||||
|
||||
|
||||
return WebSocketConnection;
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue