issie
issie copied to clipboard
Better SVG creation
This is motivated by #442
Currently, whenever SVG radix or zoom is changed, waveform SVGs are recreated. We recreate (typically) 1000 cycles of the SVG even if we are only looking at 2 cycles.
The proposal is to recreate a custom segment of SVG waveform with a width that exactly fits the viewer. This will typically be only 20 cycles and at most around 300 cycles - however in that case there will be no text printed on waveforms which will consist of two polylines each only.
Advantages:
- Fixes #442
- Faster, more responsive, waveform generation
- Numeric values can be printed, when there is room, in the correct places - at either side of a waveform change or at the edge of the viewer. this will make the waveforms more readable.
Implementation
- Waveform generation happens in
generateWaveform
. There is some code above this that calculates when waveforms need to be regenerated - this may need changing as well (to regenerate more often) but that will be quite easy to fix. - Inside generateWaveform there are 3 match cases according to waveform width: binary, <= 32 bits, > 32 bits. The logic is different for binary (waveform shows value) and other (waveform shows when value changes - value is written on waveform as text).
- In all cases waveforms are generated first by extracting a transitions array
transitions
from the simulator, then by generating polylines (for waveform) and if required text labels from this. - the transitions array is generated by
calculateBinaryTransitionsUnInt32
orcalculateNonBinaryTransitions
. Currently this is always 1000 cycles long. These functions must be changed to calculate only the clock cycles currently in the viewer:ws.ShownCycles
,ws.StartCycle
- The functions that add text:
displayUnint32Onwave
,displayBigIntOnwave
should be changed:- to display only 0, 1 (in middle) , or 2 (at either end) copies of number according to width of gap rather than displaying many repetitions.
- To use a newly written fast function upperBoundOfNumericTextWidth (t: Text) (r: radix) (txt: string). This function is curried and can be written so that all the necessary calls to textMetrics are done just once when it is called with a constant first parameter.
Context for SVG creation:
- Maybe we will want to display in-integral-clock-cycle SVGs when scrolling to have better UI. In that case we will still (I think) have a snap-to-integral thing at the end of a scrollbar drag.
- We do not know how fast creation will be. it is plausible to think it will *always be fast - but only if we replace oscillatory bits of SVG with too many points to see with grey polygons. this would make waveforms look better as well.
- SVG text - this needs to be written in the correct place - which depends on what you can see on screen. Best would be as 5 above.
- See #449 - I tried implementing some of this, it was not much faster.
Long Term Plan (for ideal display).
- Create shorter SVGs
- Optimise text placement and grey polygon placement on SVG
- Create scroll bar - use an svg polygon to slide. 1. work out where to put it - add a dummy svg there 2. work out how to display a scrollbar as an svg. 3. work out how to capture clicks and mouse moves on the svg (see draw block, also see context menus which capture right-clicks on any issue html component. 4. Work out how should scrollbar work when scrolling much longer distances than can be seen. 5. Make it work 7. Check worst case timing - of a problem instrument this. 8. Implement "fluid" scroll.
Stage 1: Shorter SVG
Summary
- Shorter SVG generation was achieved by truncating the
transitions
array, and shifting displayed numbers. - Solution's performance was analysed, see results below.
- Ability to generate full SVG was left in.
Performance Analysis
The function being timed was the makeWaveformsWithTimeOut
function. A quick experiment was done run on 107 waves with 38 binary and 69 short non-binary waves. Waveform Simulator was set to generate 8 cycles from the start (0th cycle). We note that, with the original solution, multiple runs of the function was used and added up to roughly 1000ms for total generation time (See Fig. 1). With shorter SVG generation, only one run of the function was used and only took 16ms (See Fig. 2.).
Figure 1. Perf Logs with Complete SVG Generation
Figure 2. Perf Logs with Shorter SVG Generation
An improvement of generation time of around 40x to 50x is expected.
One could replicate these experiments by toggling WaveSim.Constants.generateVisibleOnly
to control the generation range of the SVG, and toggling WaveSim.Constants.showPerfLogs
to see the performance monitoring output.
Nice. This looks good enough to implement on demand waveform generation while scrolling.
Stage 2: Waveform Values Display Method
Summary
- Changes were made to
displayUInt32OnWave
anddisplayBigIntOnWave
to implement the updated view method. - Numbers were displayed in the following format:
- No space for 1 copy: grey polygons, drawn manually, styling can be found in
wavePolyfillStyle
. - Space for 1 copy: a copy is displayed in the center of the waveform.
- Space for 2 or more copies: two copies are displayed at either end of the waveform, padding between edge of waveform is automatically set to padding of a centered single-cycle waveform, but set to a constant if a single-cycle waveform can fit more than 2 copies.
- No space for 1 copy: grey polygons, drawn manually, styling can be found in
- Ability to display values in the old method was left out.
Quick Demo
New value display methods were tested on a simple counter circuit, see Fig. 1.
Figure 1. Counter Circuit
Figures 2 to 4 offers different zoomed views of the updated value display method.
Figure 2. Low Zoom (18 cycles)
Figure 3. Mid Zoom (12 cycles)
Figure 4. High Zoom (3 cycles)
Stage 3: Scrollbar v1
Summary
- Working scrollbar was achieved.
- Changes to enable static scrollbar representation:
- Fields were added to
WaveSimModel
to track scrollbar information. - Function
generateScrollbarInfo
was added torefreshWaveSim
to updateWaveSimModel
when it is changed. - Function
makeScrollbar
was added toviewWaveSim
to make React elements that will be inserted into the DOM.
- Fields were added to
- Changes to enable dynamic scrollbar control:
- New message type --
ScrollbarMouseMsg
-- was added. - Function
setClkCycleWithCounter
was added to shift viewed cycles by a non-integer amount. - Function
updateScrollbar
updates theWaveSimModel
after the scrollbar mouse message is issued.
- New message type --
Scrollbar Components
The scrollbar consisted of 4 major components, as shown in Fig. 1 below. The components are labeled in the figure:
- Side buttons: buttons with similar function like those on the top, one on each side.
- Scrollbar background: grey SVG rectangle that serves as the backdrop to the control thumb.
- Scrollbar thumb: white SVG rectangle that represents the clock cycle range we are currently viewing and also serves as the main control of the scrollbar.
- Thumb buffer zone: invisible SVG rectangles spanning a width of 32px before and after the thumb, used to catch mouse movements allowing the scrollbar to feel more smooth.
Uses and behaviours of these components will be discussed in later sections.
Figure 1. Anatomy of a Scrollbar
Representation Mechanics
See the representation mechanics in action below in Movie 1. The representation mechanics can be described in the following three main points
- The scrollbar background initially represents 100 cycles.
- The scrollbar background will later represent "0 to the furthermost cycle the user has visited", and this can be achieved by dragging the thumb at the end or using the buttons.
- The scrollbar thumb approximates the location and percentage of the viewed range; and need not be too small such that it is impossible to drag. Therefore:
- If the thumb width to represent the viewed range is calculated to be LESS THAN 5px, the width is set to 5px, and the centre of the thumb is set to the location of the current cycle within the represented range.
- If the thumb width is calculated to be MORE THAN 5px, the width is used as such, and the centre of the thumb is set to the position of the centre of the viewed range within the represented range. I.e. it does not move if you go between cycles within the viewed range.
The function generateScrollbarInfo
is just a template for translating viewed range information stored in WaveSimModel
into the characteristics of the scrollbar. This can be later swapped out to accommodate more complicated representations.
Movie 1. Scrollbar Scaling Demo
https://github.com/user-attachments/assets/61ca2394-609c-47a2-8476-6d4c8bce5375
Control Mechanics
Control of the scrollbar operates under the these assumption and goals:
- The length of the viewed range will not change when you are scrolling.
- The thumb should follow the cursor as closely as possible when it is being dragged. We note that true drag is not achievable through
MouseMove
events, as if true drag is implemented, the mouse will not be moving in relation to the thumb.
The following mechanics was therefore devised:
-
MouseMove
events will give off the distance the mouse moved in pixels, which can then be translated into the number of cycles the thumb and the viewed range should move by. - The number of cycles to move by will not be an integer, so instead the viewed range is moved as close to the given non-integer cycles as possible, the remaining cycles to be scrolled is stored in the
ScrollbarCounter
variable inWaveSimModel
. - Since the
ScrollbarCounter
is not cleared upon mouse release, and we have no way of knowing (specifically, if the mouse is NOT released upon the scrollbar SVG element): the remainder cycles, or "the difference between where the thumb should be if scrolling was fluid and where the thumb is currently at", may add up, and we will see the thumb slow down behind the cursor. This is problematic because if the cursor is sufficiently fast, it may run over the thumb and scrolling would stop, which is just bad user experience. - To fix this, two buffer zones to the left and right of the thumb was added. If the cursor moved off the thumb and into the buffer zones, the viewed range will move forward/backwards faster, which allows the thumb to "catch up" to be under the cursor. If the mouse is dragged over the buffer zone directly, the viewed range will still move but the thumb will quickly catch up to the cursor and scrolling at a normal rate will be restored. We note that the scrolling in the buffer zone is uni-directional, i.e. you can only go right in the right buffer zone and left in the left buffer zone.
The combination of these mechanics has allowed the scrollbar to achieve a sufficiently comfortable scrolling experience. See Movie 2 for a demo of fast scrolling.
We further note that the function linearMouseToCycleTranslator
translates mouse movement to number of cycles to move by, which can be swapped out to provide more complicated control mechanics.
Movie 2. Scrollbar Controlling Demo
https://github.com/user-attachments/assets/4957668c-e3b3-4e27-8fe5-a83b9ef06226
OK - feedback
- This is a vast improvement on what we had before. great work.
- The mouse-move events need to be detected throughout the RHS - so that if cursor moves off the scrollbar dragging continues.
- This can easily be implemented, but you need to use absolute mouse coordinates to control the drag, storing the offset between the mouse X position and the puck on mouse-down, calculating from this and the current mouse X coordinate the correct exact puck (and waveform) position.
- Locking puck and waveforms to a cycle edge works fine as is and can easily still be done.
- Since the puck can be exactly positioned (modulo clock cycle quantisation) under the mouse the buffer zones etc are not needed, (Strictly speaking the puck position muts have the ame offset from the mouse it had on mouse-down, otehrwise you get an unpleasant puck movement on mouse down.
- All the above requires a float option in Model to determine whether a scroll drag is happenning, and what was the offset of teh puck from the cursor on button-down
Stage 4: Scrollbar v2
NB: Development branch moved from samuelpswang/dev-#448
to samuelpswang/dev-#448-tmp
, in order to use the rebasing work done by @tomcl.
Summary
-
Representation mechanics updates: when thumb size is calculated to be below 5px limit,
WaveSimModel.StartCycle
is used determine thumb position instead ofWaveSimModel.CurrClkCycle
as in v1. [14ec821] -
Control mechanics updates: as suggested in previous comment, control is now based on viewport coordinates (
clientX
) instead of aggregatingMouseMove
event vectors (movementX
) and adjusting in v1. Scroll is also tracked across whole right panel. Detailed explanation below. [24c3d02] -
Scrollbar queue: to accommodate longer waveform generation time, a counter (
WaveSimModel.ScrollbarQueueIsEmpty
) to denote whether last generation request has completed was introduced. Detailed explanation below. [3bf323b] -
Miscellaneous improvements:
- Highlighted cycle flickering when moving: instead of using start and end of viewed cycles, previous current cycle or closest viewed cycle is used.
- Weird behaviour when zoomed out at start of waveform: instead of using "max viewed cycle" as number of cycles represented, max of "max viewed cycle" and "twice the number of shown cycles" is used. Shown cycles are also removed from save data, in order to force background representation cycles to be recalculated everything a project is opened. Steps to reproduced issue and solution discussed in private emails between @samuelpswang and @tomcl. [7f18aaf]
- Max simulation cycle increased: to 10k cycles, from 2k, from 1k. [14ec821, 8524161]
Discarded Features
- Variable scroll: scrolling slower when shift key is pressed in order to scroll by 1 cycle in high zoom.
- This was implemented: but breaks the constant offset relationship between thumb and cursor.
- This was mitigated by: adjusting the scrollbar buttons to move view window by 1 cycle instead of shifting.
Deep Dive: Control Mechanics Updates
The new control mechanics can be described as the following points:
- When scrolling starts: offset between thumb position and cursor was recorded in
WaveSimModel.ScrollbarTbOffset
. - During scrolling: attempts to move thumb to the same offset as initially recorded, which was done by adding the cursor position to the recorded offset. Note here that
clientX
was used instead ofscreenX
as the latter gives absolute pixels AFTER scaling. - When scrolling ends: clear offset.
This logic was implemented across the whole of the right panel, in order for scrolling to continue even if the cursor moved off of the scrollbar:
- Scrollbar thumb:
-
OnMouseDown
= start scroll, -
OnMouseMove
= move only if in scroll, -
OnMouseUp
= end scroll only if in scroll.
-
- Right view panel:
-
OnMouseMove
= move only if in scroll, -
OnMouseUp
= end scroll only if in scroll.
-
This meant that the following structure are now redundant and were removed:
- Buffer zones before and after scrollbar thumb were removed: because the new control mechanism will attempt to move the thumb as close to the cursor as possible.
- Counter to track incomplete cycles was removed: because "skid" between thumb and cursor will not stack up anymore, since absolute coordinates was used.
Deep Dive: Scrollbar Queue
During scrolling, mouse coordinate updates are small and frequent. This would allow smooth scrolling behaviour if waveform SVG generation is sufficiently fast. However when multiple waveforms are fully zoomed out (e.g. 50+ waves at 80+ cycles), the scrolling behaviour was observed to be much slower but catches up to the cursor eventually.
To mitigate this, a queueing system was devised in order to: not dispatch ScrollbarMouseMsg
faster than they could be processed. This was done with the following logic:
- Counter tracking if a mouse message is being processed:
WaveSimModel.ScrollbarQueueIsEmpty
. - When a mouse event is detected...
- If there IS NOT a event being processed (queue is empty), we dispatch a message to update queue counter to be NOT empty and a message to update the scrollbar.
- If there IS a event being processed (queue is NOT empty), the current mouse event is ignored.
- When a message to update the scrollbar is received...
- It moves the thumb as close to the current cursor position as possible and clears the counter.
- This can be seen on the screen as a jumping motion or like dropped frames.
While this cannot avoid the choppy motion when there are large number of waveforms present, it reduces resource usage which allowed for slightly better user experience.
Better SVG generation, including:
- generating only visible bits of SVG,
- displaying just 2 copies of numeric values,
- displaying grey polygons when there is no space,
was implemented in PR #456. A scrollbar was also added, as per the reach goal for #448 (this issue) and #332.